From a76ff79adc6f107312a54ac1b4a2226eff41bfed Mon Sep 17 00:00:00 2001 From: yyforyongyu <yong2452@gmail.com> Date: Fri, 8 Nov 2024 20:39:44 +0800 Subject: [PATCH] itest: break down utxo selection funding tests --- itest/list_on_test.go | 5 +- ...lnd_channel_funding_utxo_selection_test.go | 405 +++++++++++++----- 2 files changed, 287 insertions(+), 123 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 0d618108f..218471e02 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -556,10 +556,6 @@ var allTestCases = []*lntest.TestCase{ Name: "custom features", TestFunc: testCustomFeatures, }, - { - Name: "utxo selection funding", - TestFunc: testChannelUtxoSelection, - }, { Name: "update pending open channels on funder side", TestFunc: testUpdateOnFunderPendingOpenChannels, @@ -689,4 +685,5 @@ func init() { allTestCases = append(allTestCases, psbtFundingTestCases...) allTestCases = append(allTestCases, remoteSignerTestCases...) allTestCases = append(allTestCases, channelRestoreTestCases...) + allTestCases = append(allTestCases, fundUtxoSelectionTestCases...) } diff --git a/itest/lnd_channel_funding_utxo_selection_test.go b/itest/lnd_channel_funding_utxo_selection_test.go index f840b8ac9..7868b7333 100644 --- a/itest/lnd_channel_funding_utxo_selection_test.go +++ b/itest/lnd_channel_funding_utxo_selection_test.go @@ -15,6 +15,37 @@ import ( "github.com/stretchr/testify/require" ) +var fundUtxoSelectionTestCases = []*lntest.TestCase{ + { + Name: "utxo selection funding error", + TestFunc: testChannelUtxoSelectionError, + }, + { + Name: "utxo selection selected valid chan size", + TestFunc: testUtxoSelectionSelectedValidChanSize, + }, + { + Name: "utxo selection selected valid chan reserve", + TestFunc: testUtxoSelectionSelectedValidChanReserve, + }, + { + Name: "utxo selection selected reserve from selected", + TestFunc: testUtxoSelectionReserveFromSelected, + }, + { + Name: "utxo selection fundmax", + TestFunc: testUtxoSelectionFundmax, + }, + { + Name: "utxo selection fundmax reserve", + TestFunc: testUtxoSelectionFundmaxReserve, + }, + { + Name: "utxo selection reused utxo", + TestFunc: testUtxoSelectionReuseUTXO, + }, +} + type chanFundUtxoSelectionTestCase struct { // name is the name of the target test case. name string @@ -57,9 +88,10 @@ type chanFundUtxoSelectionTestCase struct { reuseUtxo bool } -// testChannelUtxoSelection checks various channel funding scenarios where the -// user instructed the wallet to use a selection funds available in the wallet. -func testChannelUtxoSelection(ht *lntest.HarnessTest) { +// testChannelUtxoSelectionError checks various channel funding error scenarios +// where the user instructed the wallet to use a selection funds available in +// the wallet. +func testChannelUtxoSelectionError(ht *lntest.HarnessTest) { // Create two new nodes that open a channel between each other for these // tests. args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) @@ -115,73 +147,6 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) { "create funding transaction, need 0.00210337 " + "BTC only have 0.00100000 BTC available", }, - // We are spending two selected coins partially out of three - // available in the wallet and expect a change output and the - // unselected coin as remaining wallet balance. - { - name: "selected, local amount > " + - "min chan size", - initialCoins: []btcutil.Amount{ - 200_000, 50_000, 100_000, - }, - selectedCoins: []btcutil.Amount{ - 200_000, 100_000, - }, - localAmt: btcutil.Amount(250_000), - expectedBalance: btcutil.Amount(250_000), - remainingWalletBalance: btcutil.Amount(350_000) - - btcutil.Amount(250_000) - fundingFee(2, true), - }, - // We are spending the entirety of two selected coins out of - // three available in the wallet and expect no change output and - // the unselected coin as remaining wallet balance. - { - name: "fundmax, local amount > min " + - "chan size", - initialCoins: []btcutil.Amount{ - 200_000, 100_000, 50_000, - }, - selectedCoins: []btcutil.Amount{ - 200_000, 50_000, - }, - expectedBalance: btcutil.Amount(200_000) + - btcutil.Amount(50_000) - fundingFee(2, false), - remainingWalletBalance: btcutil.Amount(100_000), - }, - // Select all coins in wallet and use the maximum available - // local amount to fund an anchor channel. - { - name: "selected, local amount leaves sufficient " + - "reserve", - initialCoins: []btcutil.Amount{ - 200_000, 100_000, - }, - selectedCoins: []btcutil.Amount{200_000, 100_000}, - commitmentType: lnrpc.CommitmentType_ANCHORS, - localAmt: btcutil.Amount(300_000) - - reserveAmount - fundingFee(2, true), - expectedBalance: btcutil.Amount(300_000) - - reserveAmount - fundingFee(2, true), - remainingWalletBalance: reserveAmount, - }, - // Select all coins in wallet towards local amount except for an - // anchor reserve portion. Because the UTXOs are sorted by size - // by default, the reserve amount is just left in the wallet. - { - name: "selected, reserve from selected", - initialCoins: []btcutil.Amount{ - 200_000, reserveAmount, 100_000, - }, - selectedCoins: []btcutil.Amount{ - 200_000, reserveAmount, 100_000, - }, - commitmentType: lnrpc.CommitmentType_ANCHORS, - localAmt: btcutil.Amount(300_000) - - fundingFee(2, true), - expectedBalance: btcutil.Amount(300_000) - - fundingFee(2, true), - remainingWalletBalance: reserveAmount, - }, // Select all coins in wallet and use more than the maximum // available local amount to fund an anchor channel. { @@ -200,43 +165,6 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) { "insufficient funds for fee bumping anchor " + "channel closings", }, - // We fund an anchor channel with a single coin and just keep - // enough funds in the wallet to cover for the anchor reserve. - { - name: "fundmax, sufficient reserve", - initialCoins: []btcutil.Amount{ - 200_000, reserveAmount, - }, - selectedCoins: []btcutil.Amount{200_000}, - commitmentType: lnrpc.CommitmentType_ANCHORS, - expectedBalance: btcutil.Amount(200_000) - - fundingFee(1, false), - remainingWalletBalance: reserveAmount, - }, - // We fund an anchor channel with a single coin and expect the - // reserve amount left in the wallet. - { - name: "fundmax, sufficient reserve from channel " + - "balance carve out", - initialCoins: []btcutil.Amount{ - 200_000, - }, - selectedCoins: []btcutil.Amount{200_000}, - commitmentType: lnrpc.CommitmentType_ANCHORS, - expectedBalance: btcutil.Amount(200_000) - - reserveAmount - fundingFee(1, true), - remainingWalletBalance: reserveAmount, - }, - // Confirm that already spent outputs can't be reused to fund - // another channel. - { - name: "output already spent", - initialCoins: []btcutil.Amount{ - 200_000, - }, - selectedCoins: []btcutil.Amount{200_000}, - reuseUtxo: true, - }, } for _, tc := range tcs { @@ -255,24 +183,258 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) { } } +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionSelectedValidChanSize(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) + bob := ht.NewNode("Bob", args) + + // 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) + + // We are spending two selected coins partially out of three available + // in the wallet and expect a change output and the unselected coin as + // remaining wallet balance. + tc := &chanFundUtxoSelectionTestCase{ + name: "selected, local amount > min chan size", + initialCoins: []btcutil.Amount{ + 200_000, 50_000, 100_000, + }, + selectedCoins: []btcutil.Amount{ + 200_000, 100_000, + }, + localAmt: btcutil.Amount(250_000), + expectedBalance: btcutil.Amount(250_000), + remainingWalletBalance: btcutil.Amount(350_000) - + btcutil.Amount(250_000) - fundingFee(2, true), + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionSelectedValidChanReserve(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) + bob := ht.NewNode("Bob", args) + + // 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) + + // Select all coins in wallet and use the maximum available + // local amount to fund an anchor channel. + tc := &chanFundUtxoSelectionTestCase{ + name: "selected, local amount leaves sufficient reserve", + initialCoins: []btcutil.Amount{ + 200_000, 100_000, + }, + selectedCoins: []btcutil.Amount{200_000, 100_000}, + commitmentType: lnrpc.CommitmentType_ANCHORS, + localAmt: btcutil.Amount(300_000) - + reserveAmount - fundingFee(2, true), + expectedBalance: btcutil.Amount(300_000) - + reserveAmount - fundingFee(2, true), + remainingWalletBalance: reserveAmount, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionReserveFromSelected(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) + bob := ht.NewNode("Bob", args) + + // 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) + + // Select all coins in wallet towards local amount except for an anchor + // reserve portion. Because the UTXOs are sorted by size by default, + // the reserve amount is just left in the wallet. + tc := &chanFundUtxoSelectionTestCase{ + name: "selected, reserve from selected", + initialCoins: []btcutil.Amount{ + 200_000, reserveAmount, 100_000, + }, + selectedCoins: []btcutil.Amount{ + 200_000, reserveAmount, 100_000, + }, + commitmentType: lnrpc.CommitmentType_ANCHORS, + localAmt: btcutil.Amount(300_000) - + fundingFee(2, true), + expectedBalance: btcutil.Amount(300_000) - + fundingFee(2, true), + remainingWalletBalance: reserveAmount, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionFundmax(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) + bob := ht.NewNode("Bob", args) + + // 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) + + // We fund an anchor channel with a single coin and just keep enough + // funds in the wallet to cover for the anchor reserve. + tc := &chanFundUtxoSelectionTestCase{ + name: "fundmax, sufficient reserve", + initialCoins: []btcutil.Amount{ + 200_000, reserveAmount, + }, + selectedCoins: []btcutil.Amount{200_000}, + commitmentType: lnrpc.CommitmentType_ANCHORS, + expectedBalance: btcutil.Amount(200_000) - + fundingFee(1, false), + remainingWalletBalance: reserveAmount, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionFundmaxReserve(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) + bob := ht.NewNode("Bob", args) + + // 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) + + // We fund an anchor channel with a single coin and expect the reserve + // amount left in the wallet. + tc := &chanFundUtxoSelectionTestCase{ + name: "fundmax, sufficient reserve from channel " + + "balance carve out", + initialCoins: []btcutil.Amount{ + 200_000, + }, + selectedCoins: []btcutil.Amount{200_000}, + commitmentType: lnrpc.CommitmentType_ANCHORS, + expectedBalance: btcutil.Amount(200_000) - + reserveAmount - fundingFee(1, true), + remainingWalletBalance: reserveAmount, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionReuseUTXO(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) + bob := ht.NewNode("Bob", args) + + // 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) + + // Confirm that already spent outputs can't be reused to fund another + // channel. + tc := &chanFundUtxoSelectionTestCase{ + name: "output already spent", + initialCoins: []btcutil.Amount{ + 200_000, + }, + selectedCoins: []btcutil.Amount{200_000}, + reuseUtxo: true, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + // runUtxoSelectionTestCase runs a single test case asserting that test // conditions are met. func runUtxoSelectionTestCase(ht *lntest.HarnessTest, alice, bob *node.HarnessNode, tc *chanFundUtxoSelectionTestCase, reserveAmount btcutil.Amount) { - // fund initial coins + // Fund initial coins. for _, initialCoin := range tc.initialCoins { ht.FundCoins(initialCoin, alice) } - defer func() { - // Fund additional coins to sweep in case the wallet contains - // dust. - ht.FundCoins(100_000, alice) - - // Remove all funds from Alice. - sweepNodeWalletAndAssert(ht, alice) - }() // Create an outpoint lookup for each unique amount. lookup := make(map[int64]*lnrpc.OutPoint) @@ -314,9 +476,14 @@ func runUtxoSelectionTestCase(ht *lntest.HarnessTest, alice, // successful, simply check for an error. if tc.chanOpenShouldFail { expectedErr := errors.New(tc.expectedErrStr) - ht.OpenChannelAssertErr( - alice, bob, chanParams, expectedErr, - ) + ht.OpenChannelAssertErr(alice, bob, chanParams, expectedErr) + + // Fund additional coins to sweep in case the wallet contains + // dust. + ht.FundCoins(100_000, alice) + + // Remove all funds from Alice. + sweepNodeWalletAndAssert(ht, alice) return }