From 6f55a7af0570c284c88c854c847aef9677fd50a1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 11 Apr 2024 16:58:24 +0800 Subject: [PATCH] itest: add new test to check `BumpFee` and `PendingSweeps` --- itest/list_on_test.go | 4 +- itest/lnd_onchain_test.go | 115 ------------ itest/lnd_remote_signer_test.go | 4 +- itest/lnd_sweep_test.go | 317 ++++++++++++++++++++++++++++++++ lntest/harness.go | 24 +++ 5 files changed, 345 insertions(+), 119 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 98a4cccce..c8fa5674d 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -459,8 +459,8 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testSignVerifyMessage, }, { - Name: "cpfp", - TestFunc: testCPFP, + Name: "bumpfee", + TestFunc: testBumpFee, }, { Name: "taproot", diff --git a/itest/lnd_onchain_test.go b/itest/lnd_onchain_test.go index 9ed952fbf..5d137e87e 100644 --- a/itest/lnd_onchain_test.go +++ b/itest/lnd_onchain_test.go @@ -16,7 +16,6 @@ import ( "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lnwallet" - "github.com/lightningnetwork/lnd/sweep" "github.com/stretchr/testify/require" ) @@ -210,120 +209,6 @@ func testChainKitSendOutputsAnchorReserve(ht *lntest.HarnessTest) { ht.CloseChannel(charlie, outpoint) } -// testCPFP ensures that the daemon can bump an unconfirmed transaction's fee -// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction. -// -// TODO(wilmer): Add RBF case once btcd supports it. -func testCPFP(ht *lntest.HarnessTest) { - runCPFP(ht, ht.Alice, ht.Bob) -} - -// runCPFP ensures that the daemon can bump an unconfirmed transaction's fee -// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction. -func runCPFP(ht *lntest.HarnessTest, alice, bob *node.HarnessNode) { - // TODO(yy): fix the test when `BumpFee` is updated. - ht.Skipf("skipped") - - // Skip this test for neutrino, as it's not aware of mempool - // transactions. - if ht.IsNeutrinoBackend() { - ht.Skipf("skipping CPFP test for neutrino backend") - } - - // We'll start the test by sending Alice some coins, which she'll use - // to send to Bob. - ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) - - // Create an address for Bob to send the coins to. - req := &lnrpc.NewAddressRequest{ - Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, - } - resp := bob.RPC.NewAddress(req) - - // Send the coins from Alice to Bob. We should expect a transaction to - // be broadcast and seen in the mempool. - sendReq := &lnrpc.SendCoinsRequest{ - Addr: resp.Address, - Amount: btcutil.SatoshiPerBitcoin, - TargetConf: 6, - } - alice.RPC.SendCoins(sendReq) - txid := ht.Miner.AssertNumTxsInMempool(1)[0] - - // We'll then extract the raw transaction from the mempool in order to - // determine the index of Bob's output. - tx := ht.Miner.GetRawTransaction(txid) - bobOutputIdx := -1 - for i, txOut := range tx.MsgTx().TxOut { - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - txOut.PkScript, ht.Miner.ActiveNet, - ) - require.NoErrorf(ht, err, "unable to extract address "+ - "from pkScript=%x: %v", txOut.PkScript, err) - - if addrs[0].String() == resp.Address { - bobOutputIdx = i - } - } - require.NotEqual(ht, -1, bobOutputIdx, "bob's output was not found "+ - "within the transaction") - - // Wait until bob has seen the tx and considers it as owned. - op := &lnrpc.OutPoint{ - TxidBytes: txid[:], - OutputIndex: uint32(bobOutputIdx), - } - ht.AssertUTXOInWallet(bob, op, "") - - // We'll attempt to bump the fee of this transaction by performing a - // CPFP from Alice's point of view. - maxFeeRate := uint64(sweep.DefaultMaxFeeRate) - bumpFeeReq := &walletrpc.BumpFeeRequest{ - Outpoint: op, - // We use a higher fee rate than the default max and expect the - // sweeper to cap the fee rate at the max value. - SatPerVbyte: maxFeeRate * 2, - // We use a force param to create the sweeping tx immediately. - Immediate: true, - } - bob.RPC.BumpFee(bumpFeeReq) - - // We should now expect to see two transactions within the mempool, a - // parent and its child. - ht.Miner.AssertNumTxsInMempool(2) - - // We should also expect to see the output being swept by the - // UtxoSweeper. We'll ensure it's using the fee rate specified. - pendingSweepsResp := bob.RPC.PendingSweeps() - require.Len(ht, pendingSweepsResp.PendingSweeps, 1, - "expected to find 1 pending sweep") - pendingSweep := pendingSweepsResp.PendingSweeps[0] - require.Equal(ht, pendingSweep.Outpoint.TxidBytes, op.TxidBytes, - "output txid not matched") - require.Equal(ht, pendingSweep.Outpoint.OutputIndex, op.OutputIndex, - "output index not matched") - - // Also validate that the fee rate is capped at the max value. - require.Equalf(ht, maxFeeRate, pendingSweep.SatPerVbyte, - "sweep sat per vbyte not matched, want %v, got %v", - maxFeeRate, pendingSweep.SatPerVbyte) - - // Mine a block to clean up the unconfirmed transactions. - ht.MineBlocksAndAssertNumTxes(1, 2) - - // The input used to CPFP should no longer be pending. - err := wait.NoError(func() error { - resp := bob.RPC.PendingSweeps() - if len(resp.PendingSweeps) != 0 { - return fmt.Errorf("expected 0 pending sweeps, found %d", - len(resp.PendingSweeps)) - } - - return nil - }, defaultTimeout) - require.NoError(ht, err, "timeout checking bob's pending sweeps") -} - // testAnchorReservedValue tests that we won't allow sending transactions when // that would take the value we reserve for anchor fee bumping out of our // wallet. diff --git a/itest/lnd_remote_signer_test.go b/itest/lnd_remote_signer_test.go index b9c96bb00..e18e5cb03 100644 --- a/itest/lnd_remote_signer_test.go +++ b/itest/lnd_remote_signer_test.go @@ -114,10 +114,10 @@ func testRemoteSigner(ht *lntest.HarnessTest) { runDeriveSharedKey(tt, wo) }, }, { - name: "cpfp", + name: "bumpfee", sendCoins: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { - runCPFP(tt, wo, carol) + runBumpFee(tt, wo) }, }, { name: "psbt", diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index a9e838088..3747b9b82 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -11,11 +11,14 @@ import ( "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/routing" + "github.com/lightningnetwork/lnd/sweep" "github.com/stretchr/testify/require" ) @@ -857,3 +860,317 @@ func createSimpleNetwork(ht *lntest.HarnessTest, nodeCfg []string, return resp, nodes } + +// testBumpFee checks that when a new input is requested, it's first bumped via +// CPFP, then RBF. Along the way, we check the `BumpFee` can properly update +// the fee function used by supplying new params. +func testBumpFee(ht *lntest.HarnessTest) { + runBumpFee(ht, ht.Alice) +} + +// runBumpFee checks the `BumpFee` RPC can properly bump the fee of a given +// input. +func runBumpFee(ht *lntest.HarnessTest, alice *node.HarnessNode) { + // Skip this test for neutrino, as it's not aware of mempool + // transactions. + if ht.IsNeutrinoBackend() { + ht.Skipf("skipping BumpFee test for neutrino backend") + } + + // startFeeRate is the min fee rate in sats/vbyte. This value should be + // used as the starting fee rate when the default no deadline is used. + startFeeRate := uint64(1) + + // We'll start the test by sending Alice some coins, which she'll use + // to send to Bob. + ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) + + // Alice sends a coin to herself. + tx := ht.SendCoins(alice, alice, btcutil.SatoshiPerBitcoin) + txid := tx.TxHash() + + // Alice now tries to bump the first output on this tx. + op := &lnrpc.OutPoint{ + TxidBytes: txid[:], + OutputIndex: uint32(0), + } + value := btcutil.Amount(tx.TxOut[0].Value) + + // assertPendingSweepResp is a helper closure that asserts the response + // from `PendingSweep` RPC is returned with expected values. It also + // returns the sweeping tx for further checks. + assertPendingSweepResp := func(broadcastAttempts uint32, budget uint64, + deadline uint32, startingFeeRate uint64) *wire.MsgTx { + + // Alice should still have one pending sweep. + pendingSweep := ht.AssertNumPendingSweeps(alice, 1)[0] + + // Validate all fields returned from `PendingSweeps` are as + // expected. + require.Equal(ht, op.TxidBytes, pendingSweep.Outpoint.TxidBytes) + require.Equal(ht, op.OutputIndex, + pendingSweep.Outpoint.OutputIndex) + require.Equal(ht, walletrpc.WitnessType_TAPROOT_PUB_KEY_SPEND, + pendingSweep.WitnessType) + require.EqualValuesf(ht, value, pendingSweep.AmountSat, + "amount not matched: want=%d, got=%d", value, + pendingSweep.AmountSat) + require.True(ht, pendingSweep.Immediate) + + require.Equal(ht, broadcastAttempts, + pendingSweep.BroadcastAttempts) + require.EqualValuesf(ht, budget, pendingSweep.Budget, + "budget not matched: want=%d, got=%d", budget, + pendingSweep.Budget) + + // Since the request doesn't specify a deadline, we expect the + // existing deadline to be used. + require.Equalf(ht, deadline, pendingSweep.DeadlineHeight, + "deadline height not matched: want=%d, got=%d", + deadline, pendingSweep.DeadlineHeight) + + // Since the request specifies a starting fee rate, we expect + // that to be used as the starting fee rate. + require.Equalf(ht, startingFeeRate, + pendingSweep.RequestedSatPerVbyte, "requested "+ + "starting fee rate not matched: want=%d, "+ + "got=%d", startingFeeRate, + pendingSweep.RequestedSatPerVbyte) + + // We expect to see Alice's original tx and her CPFP tx in the + // mempool. + txns := ht.Miner.GetNumTxsFromMempool(2) + + // Find the sweeping tx - assume it's the first item, if it has + // the same txid as the parent tx, use the second item. + sweepTx := txns[0] + if sweepTx.TxHash() == tx.TxHash() { + sweepTx = txns[1] + } + + return sweepTx + } + + // assertFeeRateEqual is a helper closure that asserts the fee rate of + // the pending sweep tx is equal to the expected fee rate. + assertFeeRateEqual := func(expected uint64) { + err := wait.NoError(func() error { + // Alice should still have one pending sweep. + pendingSweep := ht.AssertNumPendingSweeps(alice, 1)[0] + + if pendingSweep.SatPerVbyte == expected { + return nil + } + + return fmt.Errorf("expected current fee rate %d, got "+ + "%d", expected, pendingSweep.SatPerVbyte) + }, wait.DefaultTimeout) + require.NoError(ht, err, "fee rate not updated") + } + + // assertFeeRateGreater is a helper closure that asserts the fee rate + // of the pending sweep tx is greater than the expected fee rate. + assertFeeRateGreater := func(expected uint64) { + err := wait.NoError(func() error { + // Alice should still have one pending sweep. + pendingSweep := ht.AssertNumPendingSweeps(alice, 1)[0] + + if pendingSweep.SatPerVbyte > expected { + return nil + } + + return fmt.Errorf("expected current fee rate greater "+ + "than %d, got %d", expected, + pendingSweep.SatPerVbyte) + }, wait.DefaultTimeout) + require.NoError(ht, err, "fee rate not updated") + } + + // First bump request - we'll specify nothing except `Immediate` to let + // the sweeper handle the fee, and we expect a fee func that has, + // - starting fee rate: 1 sat/vbyte (min relay fee rate). + // - deadline: 1008 (default deadline). + // - budget: 50% of the input value. + bumpFeeReq := &walletrpc.BumpFeeRequest{ + Outpoint: op, + // We use a force param to create the sweeping tx immediately. + Immediate: true, + } + alice.RPC.BumpFee(bumpFeeReq) + + // Since the request doesn't specify a deadline, we expect the default + // deadline to be used. + _, currentHeight := ht.Miner.GetBestBlock() + deadline := uint32(currentHeight + sweep.DefaultDeadlineDelta) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 1. + // - starting fee rate: 1 sat/vbyte (min relay fee rate). + // - deadline: 1008 (default deadline). + // - budget: 50% of the input value. + sweepTx1 := assertPendingSweepResp(1, uint64(value/2), deadline, 0) + + // Since the request doesn't specify a starting fee rate, we expect the + // min relay fee rate is used as the current fee rate. + assertFeeRateEqual(startFeeRate) + + // testFeeRate sepcifies a starting fee rate in sat/vbyte. + const testFeeRate = uint64(100) + + // Second bump request - we will specify the fee rate and expect a fee + // func that has, + // - starting fee rate: 100 sat/vbyte. + // - deadline: 1008 (default deadline). + // - budget: 50% of the input value. + bumpFeeReq = &walletrpc.BumpFeeRequest{ + Outpoint: op, + // We use a force param to create the sweeping tx immediately. + Immediate: true, + SatPerVbyte: testFeeRate, + } + alice.RPC.BumpFee(bumpFeeReq) + + // Alice's old sweeping tx should be replaced. + ht.Miner.AssertTxNotInMempool(sweepTx1.TxHash()) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 2. + // - starting fee rate: 100 sat/vbyte. + // - deadline: 1008 (default deadline). + // - budget: 50% of the input value. + sweepTx2 := assertPendingSweepResp( + 2, uint64(value/2), deadline, testFeeRate, + ) + + // We expect the requested starting fee rate to be the current fee + // rate. + assertFeeRateEqual(testFeeRate) + + // testBudget specifies a budget in sats. + testBudget := uint64(float64(value) * 0.1) + + // Third bump request - we will specify the budget and expect a fee + // func that has, + // - starting fee rate: 100 sat/vbyte, stays unchanged. + // - deadline: 1008 (default deadline). + // - budget: 10% of the input value. + bumpFeeReq = &walletrpc.BumpFeeRequest{ + Outpoint: op, + // We use a force param to create the sweeping tx immediately. + Immediate: true, + Budget: testBudget, + } + alice.RPC.BumpFee(bumpFeeReq) + + // Alice's old sweeping tx should be replaced. + ht.Miner.AssertTxNotInMempool(sweepTx2.TxHash()) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 3. + // - starting fee rate: 100 sat/vbyte, stays unchanged. + // - deadline: 1008 (default deadline). + // - budget: 10% of the input value. + sweepTx3 := assertPendingSweepResp(3, testBudget, deadline, 0) + + // We expect the current fee rate to be increased because we ensure the + // initial broadcast always succeeds. + assertFeeRateGreater(testFeeRate) + + // Create a test deadline delta to use in the next test. + testDeadlineDelta := uint32(100) + deadlineHeight := uint32(currentHeight) + testDeadlineDelta + + // Fourth bump request - we will specify the deadline and expect a fee + // func that has, + // - starting fee rate: 100 sat/vbyte, stays unchanged. + // - deadline: 100. + // - budget: 10% of the input value, stays unchanged. + bumpFeeReq = &walletrpc.BumpFeeRequest{ + Outpoint: op, + // We use a force param to create the sweeping tx immediately. + Immediate: true, + TargetConf: testDeadlineDelta, + } + alice.RPC.BumpFee(bumpFeeReq) + + // Alice's old sweeping tx should be replaced. + ht.Miner.AssertTxNotInMempool(sweepTx3.TxHash()) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 4. + // - starting fee rate: 100 sat/vbyte, stays unchanged. + // - deadline: 100. + // - budget: 10% of the input value, stays unchanged. + sweepTx4 := assertPendingSweepResp(4, testBudget, deadlineHeight, 0) + + // We expect the current fee rate to be increased because we ensure the + // initial broadcast always succeeds. + assertFeeRateGreater(testFeeRate) + + // Fifth bump request - we test the behavior of `Immediate` - every + // time it's called, the fee function will keep increasing the fee rate + // until the broadcast can succeed. The fee func that has, + // - starting fee rate: 100 sat/vbyte, stays unchanged. + // - deadline: 100, stays unchanged. + // - budget: 10% of the input value, stays unchanged. + bumpFeeReq = &walletrpc.BumpFeeRequest{ + Outpoint: op, + // We use a force param to create the sweeping tx immediately. + Immediate: true, + } + alice.RPC.BumpFee(bumpFeeReq) + + // Alice's old sweeping tx should be replaced. + ht.Miner.AssertTxNotInMempool(sweepTx4.TxHash()) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 5. + // - starting fee rate: 100 sat/vbyte, stays unchanged. + // - deadline: 100, stays unchanged. + // - budget: 10% of the input value, stays unchanged. + sweepTx5 := assertPendingSweepResp(5, testBudget, deadlineHeight, 0) + + // We expect the current fee rate to be increased because we ensure the + // initial broadcast always succeeds. + assertFeeRateGreater(testFeeRate) + + smallBudget := uint64(1000) + + // Finally, we test the behavior of lowering the fee rate. The fee func + // that has, + // - starting fee rate: 1 sat/vbyte. + // - deadline: 1008. + // - budget: 1000 sats. + bumpFeeReq = &walletrpc.BumpFeeRequest{ + Outpoint: op, + // We use a force param to create the sweeping tx immediately. + Immediate: true, + SatPerVbyte: startFeeRate, + Budget: smallBudget, + TargetConf: uint32(sweep.DefaultDeadlineDelta), + } + alice.RPC.BumpFee(bumpFeeReq) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 6. + // - starting fee rate: 1 sat/vbyte. + // - deadline: 1008. + // - budget: 1000 sats. + sweepTx6 := assertPendingSweepResp( + 6, smallBudget, deadline, startFeeRate, + ) + + // Since this budget is too small to cover the RBF, we expect the + // sweeping attempt to fail. + // + require.Equal(ht, sweepTx5.TxHash(), sweepTx6.TxHash(), "tx5 should "+ + "not be replaced: tx5=%v, tx6=%v", sweepTx5.TxHash(), + sweepTx6.TxHash()) + + // We expect the current fee rate to be increased because we ensure the + // initial broadcast always succeeds. + assertFeeRateGreater(testFeeRate) + + // Clean up the mempol. + ht.MineBlocksAndAssertNumTxes(1, 2) +} diff --git a/lntest/harness.go b/lntest/harness.go index 784323ce4..3593e76d1 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -2289,3 +2289,27 @@ func (h *HarnessTest) GetOutputIndex(txid *chainhash.Hash, addr string) int { return p2trOutputIndex } + +// SendCoins sends a coin from node A to node B with the given amount, returns +// the sending tx. +func (h *HarnessTest) SendCoins(a, b *node.HarnessNode, + amt btcutil.Amount) *wire.MsgTx { + + // Create an address for Bob receive the coins. + req := &lnrpc.NewAddressRequest{ + Type: lnrpc.AddressType_TAPROOT_PUBKEY, + } + resp := b.RPC.NewAddress(req) + + // Send the coins from Alice to Bob. We should expect a tx to be + // broadcast and seen in the mempool. + sendReq := &lnrpc.SendCoinsRequest{ + Addr: resp.Address, + Amount: int64(amt), + TargetConf: 6, + } + a.RPC.SendCoins(sendReq) + tx := h.Miner.GetNumTxsFromMempool(1)[0] + + return tx +}