From e5423511492b9a1fe995e226d6c8b043ef1fd2e3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Jul 2024 01:34:27 +0800 Subject: [PATCH] itest: add `testSendSelectedCoins` to check selected utxos --- itest/list_on_test.go | 8 ++ itest/lnd_misc_test.go | 192 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 94bc958cd..063460d21 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -49,6 +49,14 @@ var allTestCases = []*lntest.TestCase{ Name: "send all coins", TestFunc: testSendAllCoins, }, + { + Name: "send selected coins", + TestFunc: testSendSelectedCoins, + }, + { + Name: "send selected coins channel reserve", + TestFunc: testSendSelectedCoinsChannelReserve, + }, { Name: "disconnecting target peer", TestFunc: testDisconnectingTargetPeer, diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go index 7687738fd..ae0cbcd54 100644 --- a/itest/lnd_misc_test.go +++ b/itest/lnd_misc_test.go @@ -1308,3 +1308,195 @@ func testNativeSQLNoMigration(ht *lntest.HarnessTest) { alice.SetExtraArgs(nil) require.NoError(ht, alice.Start(ht.Context())) } + +// testSendSelectedCoins tests that we're able to properly send the selected +// coins from the wallet to a single target address. +func testSendSelectedCoins(ht *lntest.HarnessTest) { + // First, we'll make a new node, Alice who'll we'll use to test wallet + // sweeping. + alice := ht.NewNode("Alice", nil) + + // Next, we'll give Alice exactly 3 utxos of 1 BTC of different address + // types. + ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) + ht.FundCoinsNP2WKH(btcutil.SatoshiPerBitcoin, alice) + ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, alice) + + // Get all the utxos in the wallet and assert there are three. + utxos := ht.AssertNumUTXOs(alice, 3) + + // Ensure that we can't send duplicate coins. + // + // Create duplciate outpoints. + dupOutpoints := []*lnrpc.OutPoint{ + utxos[0].Outpoint, + utxos[0].Outpoint, + } + + // Send the duplicate outpoints and assert there's an error. + err := alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{ + Addr: ht.NewMinerAddress().String(), + Outpoints: dupOutpoints, + }) + require.ErrorContains(ht, err, "selected outpoints contain duplicate") + + // Send a selected coin with a specific amount. + // + // We'll send the first utxo with an amount of 0.5 BTC. + amt := btcutil.Amount(0.5 * btcutil.SatoshiPerBitcoin) + alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{ + Addr: ht.NewMinerAddress().String(), + Outpoints: []*lnrpc.OutPoint{ + utxos[0].Outpoint, + }, + Amount: int64(amt), + }) + + // We expect to see the above tx in the mempool. + tx := ht.GetNumTxsFromMempool(1)[0] + + // Assert the tx has the expected shape. It should have 1 input and 2 + // outputs - the input is the selected UTXO, and the outputs are the + // specified amount and the change amount. + require.Len(ht, tx.TxIn, 1) + require.Len(ht, tx.TxOut, 2) + + // Check it's using the selected UTXO as input. + require.Equal(ht, utxos[0].Outpoint.TxidStr, + tx.TxIn[0].PreviousOutPoint.Hash.String()) + + // Mine a block to confirm the above tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // We now test we can send the selected coins to a single target + // address with `SendAll` flag. + // + // Get all the utxos in the wallet and assert there are three. + utxos = ht.AssertNumUTXOs(alice, 3) + + // Select the first two coins and send them to a single target address. + alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{ + Addr: ht.NewMinerAddress().String(), + Outpoints: []*lnrpc.OutPoint{ + utxos[0].Outpoint, + utxos[1].Outpoint, + }, + SendAll: true, + }) + + // We expect to see the above tx in the mempool. + tx = ht.GetNumTxsFromMempool(1)[0] + + // Assert the tx has the expected shape. It should have 2 inputs and 1 + // output. + require.Len(ht, tx.TxIn, 2) + require.Len(ht, tx.TxOut, 1) + + // Check it's using the selected UTXOs as inputs. + prevOutpoint1 := tx.TxIn[0].PreviousOutPoint.Hash.String() + prevOutpoint2 := tx.TxIn[1].PreviousOutPoint.Hash.String() + + if prevOutpoint1 == utxos[0].Outpoint.TxidStr { + require.Equal(ht, utxos[1].Outpoint.TxidStr, prevOutpoint2) + } else { + require.Equal(ht, utxos[1].Outpoint.TxidStr, prevOutpoint1) + require.Equal(ht, utxos[0].Outpoint.TxidStr, prevOutpoint2) + } + + // Mine a block to confirm the above tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Since the first two UTXOs have been sent to an address outside her + // wallet, Alice should see a single UTXO now. + ht.AssertNumUTXOs(alice, 1) +} + +// testSendSelectedCoinsChannelReserve tests that if sending selected coins +// would violate the channel reserve requirement the RPC call will fail. It +// also checks that change outputs will be created automatically if `SendAll` +// flag is set. +func testSendSelectedCoinsChannelReserve(ht *lntest.HarnessTest) { + chanAmt := btcutil.Amount(100_000) + + // Create a two-hop network: Alice -> Bob. + // + // NOTE: Alice will have one UTXO after the funding. + _, nodes := createSimpleNetwork( + ht, []string{"--protocol.anchors"}, 2, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + alice := nodes[0] + + // Fund Alice one more UTXO. + ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) + + // Get all the utxos in the wallet and assert there are two: + // - one from the above `FundCoins`. + // - one from the change output after the channel funding. + utxos := ht.AssertNumUTXOs(alice, 2) + + // Get all the outpoints. + outpoints := []*lnrpc.OutPoint{ + utxos[0].Outpoint, + utxos[1].Outpoint, + } + + // Calculate the total amount of the two UTXOs. + totalAmt := utxos[0].AmountSat + utxos[1].AmountSat + + // Send the total amount of the two UTXOs and expect an error since + // fees cannot be covered. + err := alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{ + Addr: ht.NewMinerAddress().String(), + Outpoints: outpoints, + Amount: totalAmt, + }) + require.ErrorContains(ht, err, "insufficient funds available to "+ + "construct transaction") + + // Get the required reserve amount. + resp := alice.RPC.RequiredReserve( + &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + + // Calculate an amount which, after sending it, would violate the + // channel reserve requirement. + amt := totalAmt - resp.RequiredReserve + + // Send the amount and expect an error since the channel reserve cannot + // be covered. + err = alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{ + Addr: ht.NewMinerAddress().String(), + Outpoints: outpoints, + Amount: amt, + }) + require.ErrorContains(ht, err, "reserved wallet balance invalidated") + + // Finally, check that we can send all the selected coins with the help + // of the `SendAll` flag as it will automatically handle reserving + // change outputs based on the channel reserve requirements. + alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{ + Addr: ht.NewMinerAddress().String(), + Outpoints: outpoints, + SendAll: true, + }) + + // We expect to see the above tx in the mempool. + tx := ht.GetNumTxsFromMempool(1)[0] + + // Assert the tx has the expected shape. It should have 2 inputs and 2 + // outputs. + require.Len(ht, tx.TxIn, 2) + require.Len(ht, tx.TxOut, 2) + + // Mine a block to confirm the above tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Alice should have one reserved UTXO now. + ht.AssertNumUTXOs(alice, 1) +}