diff --git a/cmd/lncli/cmd_open_channel.go b/cmd/lncli/cmd_open_channel.go index 4544235c724..b36676cef07 100644 --- a/cmd/lncli/cmd_open_channel.go +++ b/cmd/lncli/cmd_open_channel.go @@ -110,6 +110,13 @@ var openChannelCommand = cli.Command{ Usage: "the number of satoshis the wallet should " + "commit to the channel", }, + cli.BoolFlag{ + Name: "fundmax", + Usage: "if set, the wallet will attempt to commit " + + "the maximum possible local amount to the " + + "channel. This must not be set at the same " + + "time as local_amt", + }, cli.Uint64Flag{ Name: "base_fee_msat", Usage: "the base fee in milli-satoshis that will " + @@ -292,6 +299,7 @@ func openChannel(ctx *cli.Context) error { ZeroConf: ctx.Bool("zero_conf"), ScidAlias: ctx.Bool("scid_alias"), RemoteChanReserveSat: ctx.Uint64("remote_reserve_sats"), + FundMax: ctx.Bool("fundmax"), } switch { @@ -350,8 +358,23 @@ func openChannel(ctx *cli.Context) error { return fmt.Errorf("unable to decode local amt: %v", err) } args = args.Tail() - default: - return fmt.Errorf("local amt argument missing") + case !ctx.Bool("fundmax"): + return fmt.Errorf("either local_amt or fundmax must be " + + "specified") + } + + // The fundmax flag is NOT allowed to be combined with local_amt above. + // It is allowed to be combined with push_amt, but only if explicitly + // set. + if ctx.Bool("fundmax") && req.LocalFundingAmount != 0 { + return fmt.Errorf("local amount cannot be set if attempting " + + "to commit the maximum amount out of the wallet") + } + + // The fundmax flag is NOT allowed to be combined with the psbt flag. + if ctx.Bool("fundmax") && ctx.Bool("psbt") { + return fmt.Errorf("psbt cannot be set if attempting " + + "to commit the maximum amount out of the wallet") } if ctx.IsSet("push_amt") { diff --git a/docs/release-notes/release-notes-0.16.1.md b/docs/release-notes/release-notes-0.16.1.md index d155745f704..f3d1c48a57a 100644 --- a/docs/release-notes/release-notes-0.16.1.md +++ b/docs/release-notes/release-notes-0.16.1.md @@ -12,6 +12,10 @@ restore single channels](https://github.com/lightningnetwork/lnd/pull/7437) to and from a file on disk. +* [Add a `fundmax` flag to `openchannel` to allow for the allocation of all + funds in a wallet](https://github.com/lightningnetwork/lnd/pull/6903) towards + a new channel opening. + ## Watchtowers * [Fix Address iterator @@ -66,6 +70,7 @@ available](https://github.com/lightningnetwork/lnd/pull/7529). * ardevd * Elle Mouton +* hieblmi * Oliver Gugger * Tommy Volk * Yong Yu diff --git a/funding/manager.go b/funding/manager.go index 83c9a1d2164..c55cbba327d 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -266,6 +266,15 @@ type InitFundingMsg struct { // peer. MaxLocalCsv uint16 + // FundUpToMaxAmt is the maximum amount to try to commit to. If set, the + // MinFundAmt field denotes the acceptable minimum amount to commit to, + // while trying to commit as many coins as possible up to this value. + FundUpToMaxAmt btcutil.Amount + + // MinFundAmt must be set iff FundUpToMaxAmt is set. It denotes the + // minimum amount to commit to. + MinFundAmt btcutil.Amount + // ChanFunder is an optional channel funder that allows the caller to // control exactly how the channel funding is carried out. If not // specified, then the default chanfunding.WalletAssembler will be @@ -4079,23 +4088,26 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { } req := &lnwallet.InitFundingReserveMsg{ - ChainHash: &msg.ChainHash, - PendingChanID: chanID, - NodeID: peerKey, - NodeAddr: msg.Peer.Address(), - SubtractFees: msg.SubtractFees, - LocalFundingAmt: localAmt, - RemoteFundingAmt: 0, - CommitFeePerKw: commitFeePerKw, - FundingFeePerKw: msg.FundingFeePerKw, - PushMSat: msg.PushAmt, - Flags: channelFlags, - MinConfs: msg.MinConfs, - CommitType: commitType, - ChanFunder: msg.ChanFunder, - ZeroConf: zeroConf, - OptionScidAlias: scid, - ScidAliasFeature: scidFeatureVal, + ChainHash: &msg.ChainHash, + PendingChanID: chanID, + NodeID: peerKey, + NodeAddr: msg.Peer.Address(), + SubtractFees: msg.SubtractFees, + LocalFundingAmt: localAmt, + RemoteFundingAmt: 0, + FundUpToMaxAmt: msg.FundUpToMaxAmt, + MinFundAmt: msg.MinFundAmt, + RemoteChanReserve: chanReserve, + CommitFeePerKw: commitFeePerKw, + FundingFeePerKw: msg.FundingFeePerKw, + PushMSat: msg.PushAmt, + Flags: channelFlags, + MinConfs: msg.MinConfs, + CommitType: commitType, + ChanFunder: msg.ChanFunder, + ZeroConf: zeroConf, + OptionScidAlias: scid, + ScidAliasFeature: scidFeatureVal, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) diff --git a/funding/manager_test.go b/funding/manager_test.go index dc40c4d8f65..43798b23039 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -710,7 +710,7 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt, *wire.OutPoint, *wire.MsgTx) { publ := fundChannel( - t, alice, bob, localFundingAmt, pushAmt, false, numConfs, + t, alice, bob, localFundingAmt, pushAmt, false, 0, 0, numConfs, updateChan, announceChan, nil, ) fundingOutPoint := &wire.OutPoint{ @@ -723,7 +723,8 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt, // fundChannel takes the funding process to the point where the funding // transaction is confirmed on-chain. Returns the funding tx. func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, - pushAmt btcutil.Amount, subtractFees bool, numConfs uint32, + pushAmt btcutil.Amount, subtractFees bool, fundUpToMaxAmt, + minFundAmt btcutil.Amount, numConfs uint32, //nolint:unparam updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool, chanType *lnwire.ChannelType) *wire.MsgTx { @@ -734,6 +735,8 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, TargetPubkey: bob.privKey.PubKey(), ChainHash: *fundingNetParams.GenesisHash, SubtractFees: subtractFees, + FundUpToMaxAmt: fundUpToMaxAmt, + MinFundAmt: minFundAmt, LocalFundingAmt: localFundingAmt, PushAmt: lnwire.NewMSatFromSatoshis(pushAmt), FundingFeePerKw: 1000, @@ -3730,7 +3733,7 @@ func TestFundingManagerFundAll(t *testing.T) { // Initiate a fund channel, and inspect the funding tx. pushAmt := btcutil.Amount(0) fundingTx := fundChannel( - t, alice, bob, test.spendAmt, pushAmt, true, 1, + t, alice, bob, test.spendAmt, pushAmt, true, 0, 0, 1, updateChan, true, nil, ) @@ -3761,6 +3764,157 @@ func TestFundingManagerFundAll(t *testing.T) { } } +// TestFundingManagerFundMax tests that we can initiate a funding request to use +// the maximum allowed funds remaining in the wallet. +func TestFundingManagerFundMax(t *testing.T) { + t.Parallel() + + // Helper function to create a test utxos + constructTestUtxos := func(values ...btcutil.Amount) []*lnwallet.Utxo { + var utxos []*lnwallet.Utxo + for _, value := range values { + utxos = append(utxos, &lnwallet.Utxo{ + AddressType: lnwallet.WitnessPubKey, + Value: value, + PkScript: mock.CoinPkScript, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }) + } + + return utxos + } + + tests := []struct { + name string + coins []*lnwallet.Utxo + fundUpToMaxAmt btcutil.Amount + minFundAmt btcutil.Amount + pushAmt btcutil.Amount + change bool + }{ + { + // We will spend all the funds in the wallet, and expect + // no change output due to the dust limit. + coins: constructTestUtxos( + MaxBtcFundingAmount + 1, + ), + fundUpToMaxAmt: MaxBtcFundingAmount, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: false, + }, + { + // We spend less than the funds in the wallet, so a + // change output should be created. + coins: constructTestUtxos( + 2 * MaxBtcFundingAmount, + ), + fundUpToMaxAmt: MaxBtcFundingAmount, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: true, + }, + { + // We spend less than the funds in the wallet when + // setting a smaller channel size, so a change output + // should be created. + coins: constructTestUtxos( + MaxBtcFundingAmount, + ), + fundUpToMaxAmt: MaxBtcFundingAmount / 2, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: true, + }, + { + // We are using the entirety of two inputs for the + // funding of a channel, hence expect no change output. + coins: constructTestUtxos( + MaxBtcFundingAmount/2, MaxBtcFundingAmount/2, + ), + fundUpToMaxAmt: MaxBtcFundingAmount, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: false, + }, + { + // We are using a fraction of two inputs for the funding + // of our channel, hence expect a change output. + coins: constructTestUtxos( + MaxBtcFundingAmount/2, MaxBtcFundingAmount/2, + ), + fundUpToMaxAmt: MaxBtcFundingAmount / 2, + minFundAmt: MinChanFundingSize, + pushAmt: 0, + change: true, + }, + { + // We are funding a channel with half of the balance in + // our wallet hence expect a change output. Furthermore + // we push half of the funding amount to the remote end + // which we expect to succeed. + coins: constructTestUtxos(MaxBtcFundingAmount), + fundUpToMaxAmt: MaxBtcFundingAmount / 2, + minFundAmt: MinChanFundingSize / 4, + pushAmt: MaxBtcFundingAmount / 4, + change: true, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // We set up our mock wallet to control a list of UTXOs + // that sum to more than the max channel size. + addFunds := func(fundingCfg *Config) { + wc := fundingCfg.Wallet.WalletController + mockWc, ok := wc.(*mock.WalletController) + if ok { + mockWc.Utxos = test.coins + } + } + alice, bob := setupFundingManagers(t, addFunds) + defer tearDownFundingManagers(t, alice, bob) + + // We will consume the channel updates as we go, so no + // buffering is needed. + updateChan := make(chan *lnrpc.OpenStatusUpdate) + + // Initiate a fund channel, and inspect the funding tx. + pushAmt := test.pushAmt + fundingTx := fundChannel( + t, alice, bob, 0, pushAmt, false, + test.fundUpToMaxAmt, test.minFundAmt, 1, + updateChan, true, nil, + ) + + // Check whether the expected change output is present. + if test.change { + require.EqualValues(t, 2, len(fundingTx.TxOut)) + } + + if !test.change { + require.EqualValues(t, 1, len(fundingTx.TxOut)) + } + + // Inputs should be all funds in the wallet. + require.Equal(t, len(test.coins), len(fundingTx.TxIn)) + + for i, txIn := range fundingTx.TxIn { + require.Equal( + t, test.coins[i].OutPoint, + txIn.PreviousOutPoint, + ) + } + }) + } +} + // TestGetUpfrontShutdown tests different combinations of inputs for getting a // shutdown script. It varies whether the peer has the feature set, whether // the user has provided a script and our local configuration to test that @@ -4212,8 +4366,8 @@ func TestFundingManagerZeroConf(t *testing.T) { // Call fundChannel with the zero-conf ChannelType. fundingTx := fundChannel( - t, alice, bob, fundingAmt, pushAmt, false, 1, updateChan, true, - &channelType, + t, alice, bob, fundingAmt, pushAmt, false, 0, 0, 1, updateChan, + true, &channelType, ) fundingOp := &wire.OutPoint{ Hash: fundingTx.TxHash(), @@ -4309,3 +4463,35 @@ func TestFundingManagerZeroConf(t *testing.T) { // have been deleted from the database, as the channel is announced. assertNoFwdingPolicy(t, alice, bob, fundingOp) } + +// TestCommitmentTypeFundmaxSanityCheck was introduced as a way of reminding +// developers of new channel commitment types to also consider the channel +// opening behavior with a specified fundmax flag. To give a hypothetical +// example, if ANCHOR types had been introduced after the fundmax flag had been +// activated, the developer would have had to code for the anchor reserve in the +// funding manager in the context of public and private channels. Otherwise +// inconsistent bahvior would have resulted when specifying fundmax for +// different types of channel openings. +// To ensure consistency this test compares a map of locally defined channel +// commitment types to the list of channel types that are defined in the proto +// files. It fails if the proto files contain additional commitment types. Once +// the developer considered the new channel type behavior it can be added in +// this test to the map `allCommitmentTypes`. +func TestCommitmentTypeFundmaxSanityCheck(t *testing.T) { + t.Parallel() + allCommitmentTypes := map[string]int{ + "UNKNOWN_COMMITMENT_TYPE": 0, + "LEGACY": 1, + "STATIC_REMOTE_KEY": 2, + "ANCHORS": 3, + "SCRIPT_ENFORCED_LEASE": 4, + } + + for commitmentType := range lnrpc.CommitmentType_value { + if _, ok := allCommitmentTypes[commitmentType]; !ok { + t.Fatalf("Commitment type %s hasn't been considered "+ + "in the context of the --fundmax flag for "+ + "channel openings.", commitmentType) + } + } +} diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 19f083cbbba..1814d629205 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -519,4 +519,8 @@ var allTestCases = []*lntest.TestCase{ Name: "watchtower session management", TestFunc: testWatchtowerSessionManagement, }, + { + Name: "channel fundmax", + TestFunc: testChannelFundMax, + }, } diff --git a/itest/lnd_channel_funding_fund_max_test.go b/itest/lnd_channel_funding_fund_max_test.go new file mode 100644 index 00000000000..759df01cdf4 --- /dev/null +++ b/itest/lnd_channel_funding_fund_max_test.go @@ -0,0 +1,336 @@ +package itest + +import ( + "context" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +type chanFundMaxTestCase struct { + // name is the name of the target test case. + name string + + // initialWalletBalance is the amount in Alice's wallet. + initialWalletBalance btcutil.Amount + + // pushAmt is the amount to be pushed to Bob. + pushAmt btcutil.Amount + + // feeRate is an optional fee in satoshi/bytes used when opening a + // channel. + feeRate btcutil.Amount + + // expectedBalanceAlice is Alice's expected balance in her channel. + expectedBalanceAlice btcutil.Amount + + // chanOpenShouldFail denotes if we expect the channel opening to fail. + chanOpenShouldFail bool + + // expectedErrStr contains the expected error in case chanOpenShouldFail + // is set to true. + expectedErrStr string + + // commitmentType allows to define the exact type when opening the + // channel. + commitmentType lnrpc.CommitmentType + + // private denotes if the channel opening is announced to the network or + // not. + private bool +} + +// testChannelFundMax checks various channel funding scenarios where the user +// instructed the wallet to use all remaining funds. +func testChannelFundMax(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + defer ht.Shutdown(alice) + + bob := ht.NewNode("Bob", args) + defer ht.Shutdown(bob) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + var testCases = []*chanFundMaxTestCase{ + { + name: "wallet amount is dust", + initialWalletBalance: 2_000, + chanOpenShouldFail: true, + feeRate: 20, + expectedErrStr: "output amount(-0.00000435 BTC) " + + "after subtracting fees(0.00002435 BTC) " + + "below dust limit(0.0000033 BTC)", + }, + { + name: "wallet amount < min chan size " + + "(~18000sat)", + initialWalletBalance: 18_000, + // Using a feeRate of 1 sat/vByte ensures that we test + // for min chan size and not excessive fees. + feeRate: 1, + chanOpenShouldFail: true, + expectedErrStr: "available funds(0.00017877 BTC) " + + "below the minimum amount(0.0002 BTC)", + }, + { + name: "wallet amount > min chan " + + "size (37000sat)", + initialWalletBalance: 37_000, + // The transaction fee to open the channel must be + // subtracted from Alice's balance. + // (since wallet balance < max-chan-size) + expectedBalanceAlice: btcutil.Amount(37_000) - + fundingFee(1, false), + }, + { + name: "wallet amount > max chan size " + + "(20000000sat)", + initialWalletBalance: 20_000_000, + expectedBalanceAlice: lnd.MaxFundingAmount, + }, + // Expects, that if the maximum funding amount for a channel is + // pushed to the remote side, then the funding flow is failing + // because the push amount has to be less than the local channel + // amount. + { + name: "wallet amount > max chan size, " + + "push amount == max-chan-size", + initialWalletBalance: 20_000_000, + pushAmt: lnd.MaxFundingAmount, + chanOpenShouldFail: true, + expectedErrStr: "amount pushed to remote peer for " + + "initial state must be below the local " + + "funding amount", + }, + // Expects that if the maximum funding amount for a channel is + // pushed to the remote side then the funding flow is failing + // due to insufficient funds in the local balance to cover for + // fees in the channel opening. By that the test also ensures + // that the fees are not covered by the remaining wallet + // balance. + { + name: "wallet amount > max chan size, " + + "push amount == max-chan-size - 1_000", + initialWalletBalance: 20_000_000, + pushAmt: lnd.MaxFundingAmount - 1_000, + chanOpenShouldFail: true, + expectedErrStr: "funder balance too small (-8050000) " + + "with fee=9050 sat, minimum=708 sat required", + }, + { + name: "wallet amount > max chan size, " + + "push amount 16766000", + initialWalletBalance: 20_000_000, + pushAmt: 16_766_000, + expectedBalanceAlice: lnd.MaxFundingAmount - 16_766_000, + }, + + { + name: "anchor reserved value", + initialWalletBalance: 100_000, + commitmentType: lnrpc.CommitmentType_ANCHORS, + expectedBalanceAlice: btcutil.Amount(100_000) - + fundingFee(1, true) - reserveAmount, + }, + // Funding a private anchor channel should omit the achor + // reserve and produce no change output. + { + name: "private anchor no reserved " + + "value", + private: true, + initialWalletBalance: 100_000, + commitmentType: lnrpc.CommitmentType_ANCHORS, + expectedBalanceAlice: btcutil.Amount(100_000) - + fundingFee(1, false), + }, + } + + for _, testCase := range testCases { + success := ht.Run( + testCase.name, func(tt *testing.T) { + runFundMaxTestCase( + ht, tt, alice, bob, testCase, + reserveAmount, + ) + }, + ) + + // Stop at the first failure. Mimic behavior of original test + // framework. + if !success { + break + } + } +} + +// runTestCase runs a single test case asserting that test conditions are met. +func runFundMaxTestCase(ht *lntest.HarnessTest, t *testing.T, alice, + bob *node.HarnessNode, testCase *chanFundMaxTestCase, + reserveAmount btcutil.Amount) { + + ht.FundCoins(testCase.initialWalletBalance, alice) + + defer func() { + if testCase.initialWalletBalance <= 2_000 { + // Add additional funds to sweep "dust" UTXO. + ht.FundCoins(100_000, alice) + } + + // Remove all funds from Alice. + sweepNodeWalletAndAssert(ht, alice) + }() + + commitType := testCase.commitmentType + if commitType == lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE { + commitType = lnrpc.CommitmentType_STATIC_REMOTE_KEY + } + + // The parameters to try opening the channel with. + chanParams := lntest.OpenChannelParams{ + Amt: 0, + PushAmt: testCase.pushAmt, + SatPerVByte: testCase.feeRate, + CommitmentType: commitType, + FundMax: true, + Private: testCase.private, + } + + // If we don't expect the channel opening to be + // successful, simply check for an error. + if testCase.chanOpenShouldFail { + expectedErr := fmt.Errorf(testCase.expectedErrStr) + ht.OpenChannelAssertErr( + alice, bob, chanParams, expectedErr, + ) + + return + } + + // Otherwise, if we expect to open a channel use the helper function. + chanPoint := ht.OpenChannel(alice, bob, chanParams) + + // Close the channel between Alice and Bob, asserting + // that the channel has been properly closed on-chain. + defer ht.CloseChannel(alice, chanPoint) + + cType := ht.GetChannelCommitType(alice, chanPoint) + + // Alice's balance should be her amount subtracted by the commitment + // transaction fee. + checkChannelBalance( + ht, alice, + testCase.expectedBalanceAlice-lntest.CalcStaticFee(cType, 0), + testCase.pushAmt, + ) + + // Ensure Bob's balance within the channel is equal to the push amount. + checkChannelBalance( + ht, bob, testCase.pushAmt, + testCase.expectedBalanceAlice-lntest.CalcStaticFee(cType, 0), + ) + + if lntest.CommitTypeHasAnchors(testCase.commitmentType) && + !testCase.private { + + ht.AssertWalletAccountBalance( + alice, lnwallet.DefaultAccountName, + int64(reserveAmount), 0, + ) + } +} + +// Creates a helper closure to be used below which asserts the proper +// response to a channel balance RPC. +func checkChannelBalance(ht *lntest.HarnessTest, node *node.HarnessNode, + local, remote btcutil.Amount) { + + expectedResponse := &lnrpc.ChannelBalanceResponse{ + LocalBalance: &lnrpc.Amount{ + Sat: uint64(local), + Msat: uint64(lnwire.NewMSatFromSatoshis(local)), + }, + RemoteBalance: &lnrpc.Amount{ + Sat: uint64(remote), + Msat: uint64(lnwire.NewMSatFromSatoshis( + remote, + )), + }, + UnsettledLocalBalance: &lnrpc.Amount{}, + UnsettledRemoteBalance: &lnrpc.Amount{}, + PendingOpenLocalBalance: &lnrpc.Amount{}, + PendingOpenRemoteBalance: &lnrpc.Amount{}, + // Deprecated fields. + Balance: int64(local), + } + ht.AssertChannelBalanceResp(node, expectedResponse) +} + +// fundingFee returns the fee estimate used for a tx with the given number of +// inputs and the optional change output. This matches the estimate done by the +// wallet. +func fundingFee(numInput int, change bool) btcutil.Amount { + var weightEstimate input.TxWeightEstimator + + // The standard fee rate used for a funding transaction. + var feeRate = chainfee.SatPerKWeight(12500) + + // All inputs. + for i := 0; i < numInput; i++ { + weightEstimate.AddP2WKHInput() + } + + // The multisig funding output. + weightEstimate.AddP2WSHOutput() + + // Optionally count a change output. + if change { + weightEstimate.AddP2TROutput() + } + + totalWeight := int64(weightEstimate.Weight()) + + return feeRate.FeeForWeight(totalWeight) +} + +// sweepNodeWalletAndAssert sweeps funds from a node wallet. +func sweepNodeWalletAndAssert(ht *lntest.HarnessTest, node *node.HarnessNode) { + // New miner address we will sweep all funds to. + minerAddr, err := ht.Miner.NewAddress() + require.NoError(ht, err) + + // Send all funds back to the miner node. + node.RPC.SendCoins(&lnrpc.SendCoinsRequest{ + Addr: minerAddr.String(), + SendAll: true, + }) + + // Ensures we don't leave any transaction in the mempool after sweeping. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Ensure that the node's balance is 0 + checkChannelBalance(ht, node, 0, 0) +} diff --git a/itest/log_substitutions.txt b/itest/log_substitutions.txt index 6f24b09f8b9..da286d6a81d 100644 --- a/itest/log_substitutions.txt +++ b/itest/log_substitutions.txt @@ -6,6 +6,9 @@ s/[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2} [[:digit:]]{2}:[[:digit:]]{2}:[[: s/[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}T[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{1,18}Z/