diff --git a/itest/lnd_bump_fee.go b/itest/lnd_bump_fee.go new file mode 100644 index 000000000..3a4d46cc7 --- /dev/null +++ b/itest/lnd_bump_fee.go @@ -0,0 +1,589 @@ +package itest + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "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/lntest/wait" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/sweep" + "github.com/stretchr/testify/require" +) + +// testBumpFeeLowBudget checks that when the requested ideal budget cannot be +// met, the sweeper still sweeps the input with the actual budget. +func testBumpFeeLowBudget(ht *lntest.HarnessTest) { + // Create a new node with a large `maxfeerate` so it's easier to run the + // test. + alice := ht.NewNode("Alice", []string{ + "--sweeper.maxfeerate=10000", + }) + + // Fund Alice 2 UTXOs, each has 100k sats. One of the UTXOs will be used + // to create a tx which she sends some coins to herself. The other will + // be used as the budget when CPFPing the above tx. + coin := btcutil.Amount(100_000) + ht.FundCoins(coin, alice) + ht.FundCoins(coin, alice) + + // Alice sends 50k sats to herself. + tx := ht.SendCoins(alice, alice, coin/2) + txid := tx.TxHash() + + // Get Alice's wallet balance to calculate the fees used in the above + // tx. + resp := alice.RPC.WalletBalance() + + // balance is the expected final balance. Alice's initial balance is + // 200k sats, with 100k sats as the budget for the sweeping tx, which + // means her final balance should be 100k sats minus the mining fees + // used in the above `SendCoins`. + balance := btcutil.Amount( + resp.UnconfirmedBalance + resp.ConfirmedBalance, + ) + fee := coin*2 - balance + ht.Logf("Alice's expected final balance=%v, fee=%v", balance, fee) + + // 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(budget uint64, + deadline uint32) *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.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) + + // We expect to see Alice's original tx and her CPFP tx in the + // mempool. + txns := ht.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 + } + + // Use a budget that Alice cannot cover using her wallet UTXOs. + budget := coin * 2 + + // Use a deadlineDelta of 3 such that the fee func is initialized as, + // - starting fee rate: 1 sat/vbyte + // - deadline: 3 + // - budget: 200% of Alice's available funds. + deadlineDelta := 3 + + // First bump request - we expect it to succeed as Alice's current funds + // can cover the fees used here given the position of the fee func is at + // 0. + bumpFeeReq := &walletrpc.BumpFeeRequest{ + Outpoint: op, + Budget: uint64(budget), + Immediate: true, + DeadlineDelta: uint32(deadlineDelta), + } + alice.RPC.BumpFee(bumpFeeReq) + + // Calculate the deadline height. + deadline := ht.CurrentHeight() + uint32(deadlineDelta) + + // Assert the pending sweep is created with the expected values: + // - deadline: 3+current height. + // - budget: 2x the wallet balance. + sweepTx1 := assertPendingSweepResp(uint64(budget), deadline) + + // Mine a block to trigger Alice's sweeper to fee bump the tx. + // + // Second bump request - we expect it to succeed as Alice's current + // funds can cover the fees used here, which is 66.7% of her available + // funds given the position of the fee func is at 1. + ht.MineEmptyBlocks(1) + + // Assert the old sweeping tx has been replaced. + ht.AssertTxNotInMempool(sweepTx1.TxHash()) + + // Assert a new sweeping tx is made. + sweepTx2 := assertPendingSweepResp(uint64(budget), deadline) + + // Mine a block to trigger Alice's sweeper to fee bump the tx. + // + // Third bump request - we expect it to fail as Alice's current funds + // cannot cover the fees now, which is 133.3% of her available funds + // given the position of the fee func is at 2. + ht.MineEmptyBlocks(1) + + // Assert the above sweeping tx is still in the mempool. + ht.AssertTxInMempool(sweepTx2.TxHash()) + + // Fund Alice 200k sats, which will be used to cover the budget. + // + // TODO(yy): We are funding Alice more than enough - at this stage Alice + // has a confirmed UTXO of `coin` amount in her wallet, so ideally we + // should only fund another UTXO of `coin` amount. However, since the + // confirmed wallet UTXO has already been used in sweepTx2, there's no + // easy way to tell her wallet to reuse that UTXO in the upcoming + // sweeping tx. + // To properly fix it, we should provide more granular UTXO management + // here by leveraing `LeaseOutput` - whenever we use a wallet UTXO, we + // should lock it first. And when the sweeping attempt fails, we should + // release it so the UTXO can be used again in another batch. + walletTx := ht.FundCoinsUnconfirmed(coin*2, alice) + + // Mine a block to confirm the above funding coin. + // + // Fourth bump request - we expect it to succeed as Alice's current + // funds can cover the full budget. + ht.MineBlockWithTx(walletTx) + + flakeRaceInBitcoinClientNotifications(ht) + + // Assert Alice's previous sweeping tx has been replaced. + ht.AssertTxNotInMempool(sweepTx2.TxHash()) + + // Assert the pending sweep is created with the expected values: + // - deadline: 3+current height. + // - budget: 2x the wallet balance. + sweepTx3 := assertPendingSweepResp(uint64(budget), deadline) + require.NotEqual(ht, sweepTx2.TxHash(), sweepTx3.TxHash()) + + // Mine the sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 2) + + // Assert Alice's wallet balance. a + ht.WaitForBalanceConfirmed(alice, balance) +} + +// 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) { + alice := ht.NewNodeWithCoins("Alice", nil) + + runBumpFee(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 herself. + 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 { + + err := wait.NoError(func() error { + // Alice should still have one pending sweep. + ps := ht.AssertNumPendingSweeps(alice, 1)[0] + + // Validate all fields returned from `PendingSweeps` are + // as expected. + // + // These fields should stay the same during the test so + // we assert the values without wait. + require.Equal(ht, op.TxidBytes, ps.Outpoint.TxidBytes) + require.Equal(ht, op.OutputIndex, + ps.Outpoint.OutputIndex) + require.Equal(ht, + walletrpc.WitnessType_TAPROOT_PUB_KEY_SPEND, + ps.WitnessType) + require.EqualValuesf(ht, value, ps.AmountSat, + "amount not matched: want=%d, got=%d", value, + ps.AmountSat) + + // The following fields can change during the test so we + // return an error if they don't match, which will be + // checked again in this wait call. + if !ps.Immediate { + return fmt.Errorf("immediate should be true") + } + + if broadcastAttempts != ps.BroadcastAttempts { + return fmt.Errorf("broadcastAttempts not "+ + "matched: want=%d, got=%d", + broadcastAttempts, ps.BroadcastAttempts) + } + if budget != ps.Budget { + return fmt.Errorf("budget not matched: "+ + "want=%d, got=%d", budget, ps.Budget) + } + + // Since the request doesn't specify a deadline, we + // expect the existing deadline to be used. + if deadline != ps.DeadlineHeight { + return fmt.Errorf("deadline height not "+ + "matched: want=%d, got=%d", deadline, + ps.DeadlineHeight) + } + + // Since the request specifies a starting fee rate, we + // expect that to be used as the starting fee rate. + if startingFeeRate != ps.RequestedSatPerVbyte { + return fmt.Errorf("requested starting fee "+ + "rate not matched: want=%d, got=%d", + startingFeeRate, + ps.RequestedSatPerVbyte) + } + + return nil + }, wait.DefaultTimeout) + require.NoError(ht, err, "timeout checking pending sweep") + + // We expect to see Alice's original tx and her CPFP tx in the + // mempool. + txns := ht.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 := int32(ht.CurrentHeight()) + 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) + + // First we test the case where we specify the conf target to increase + // the starting fee rate of the fee function. + confTargetFeeRate := chainfee.SatPerVByte(50) + ht.SetFeeEstimateWithConf(confTargetFeeRate.FeePerKWeight(), 3) + + // Second bump request - we will specify the conf target and expect a + // starting fee rate that is estimated using the provided estimator. + // - starting fee rate: 50 sat/vbyte (conf target 3). + // - 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, + TargetConf: 3, + } + + alice.RPC.BumpFee(bumpFeeReq) + + // Alice's old sweeping tx should be replaced. + ht.AssertTxNotInMempool(sweepTx1.TxHash()) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 2. + // - starting fee rate: 50 sat/vbyte (conf target 3). + // - deadline: 1008 (default deadline). + // - budget: 50% of the input value. + sweepTx2 := assertPendingSweepResp( + 2, uint64(value/2), deadline, uint64(confTargetFeeRate), + ) + + // testFeeRate sepcifies a starting fee rate in sat/vbyte. + const testFeeRate = uint64(100) + + // Third bump request - we will specify the fee rate and expect a fee + // func to change the starting fee rate of the fee function, + // - 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.AssertTxNotInMempool(sweepTx2.TxHash()) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 3. + // - starting fee rate: 100 sat/vbyte. + // - deadline: 1008 (default deadline). + // - budget: 50% of the input value. + sweepTx3 := assertPendingSweepResp( + 3, 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) + + // Fourth 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.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: 1008 (default deadline). + // - budget: 10% of the input value. + sweepTx4 := assertPendingSweepResp(4, testBudget, deadline, testFeeRate) + + // 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 + + // Fifth 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, + DeadlineDelta: testDeadlineDelta, + Budget: testBudget, + } + alice.RPC.BumpFee(bumpFeeReq) + + // Alice's old sweeping tx should be replaced. + ht.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. + // - budget: 10% of the input value, stays unchanged. + sweepTx5 := assertPendingSweepResp( + 5, testBudget, deadlineHeight, testFeeRate, + ) + + // We expect the current fee rate to be increased because we ensure the + // initial broadcast always succeeds. + assertFeeRateGreater(testFeeRate) + + // Sixth 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.AssertTxNotInMempool(sweepTx5.TxHash()) + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 6. + // - starting fee rate: 100 sat/vbyte, stays unchanged. + // - deadline: 100, stays unchanged. + // - budget: 10% of the input value, stays unchanged. + sweepTx6 := assertPendingSweepResp( + 6, testBudget, deadlineHeight, testFeeRate, + ) + + // 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: 1. + // - budget: 1000 sats. + bumpFeeReq = &walletrpc.BumpFeeRequest{ + Outpoint: op, + // We use a force param to create the sweeping tx immediately. + Immediate: true, + SatPerVbyte: startFeeRate, + // The budget and the deadline delta must be set together. + Budget: smallBudget, + DeadlineDelta: 1, + } + alice.RPC.BumpFee(bumpFeeReq) + + // Calculate the ending fee rate, which is used in the above fee bump + // when fee function's max posistion is reached. + txWeight := ht.CalculateTxWeight(sweepTx6) + endingFeeRate := chainfee.NewSatPerKWeight( + btcutil.Amount(smallBudget), txWeight, + ) + + // Since the fee function has been maxed out, the starting fee rate for + // the next sweep attempt should be the ending fee rate. + // + // TODO(yy): The weight estimator used in the sweeper gives a different + // result than the weight calculated here, which is the result from + // `blockchain.GetTransactionWeight`. For this particular tx: + // - result from the `weightEstimator`: 445 wu + // - result from `GetTransactionWeight`: 444 wu + // + // This means the fee rates are different, + // - `weightEstimator`: 2247 sat/kw, or 8 sat/vb (8.988 round down) + // - here we have 2252 sat/kw, or 9 sat/vb (9.008 round down) + // + // We should investigate and check whether if it's possible to make the + // `weightEstimator` more accurate. + expectedStartFeeRate := uint64(endingFeeRate.FeePerVByte()) - 1 + + // Assert the pending sweep is created with the expected values: + // - broadcast attempts: 7. + // - starting fee rate: 8 sat/vbyte. + // - deadline: 1. + // - budget: 1000 sats. + sweepTx7 := assertPendingSweepResp( + 7, smallBudget, uint32(currentHeight+1), expectedStartFeeRate, + ) + + // Since this budget is too small to cover the RBF, we expect the + // sweeping attempt to fail. + require.Equal(ht, sweepTx6.TxHash(), sweepTx7.TxHash(), "tx6 should "+ + "not be replaced: tx6=%v, tx7=%v", sweepTx6.TxHash(), + sweepTx7.TxHash()) + + // We expect the current fee rate to be increased because we ensure the + // initial broadcast always succeeds. + assertFeeRateGreater(testFeeRate) + + // Clean up the mempool. + ht.MineBlocksAndAssertNumTxes(1, 2) +} diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index a74726488..7d2c1156f 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -15,9 +15,7 @@ import ( "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/rpc" - "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/routing" @@ -1569,404 +1567,6 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { ht.MineBlocksAndAssertNumTxes(1, 2) } -// 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) { - alice := ht.NewNodeWithCoins("Alice", nil) - - runBumpFee(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 { - - err := wait.NoError(func() error { - // Alice should still have one pending sweep. - ps := ht.AssertNumPendingSweeps(alice, 1)[0] - - // Validate all fields returned from `PendingSweeps` are - // as expected. - // - // These fields should stay the same during the test so - // we assert the values without wait. - require.Equal(ht, op.TxidBytes, ps.Outpoint.TxidBytes) - require.Equal(ht, op.OutputIndex, - ps.Outpoint.OutputIndex) - require.Equal(ht, - walletrpc.WitnessType_TAPROOT_PUB_KEY_SPEND, - ps.WitnessType) - require.EqualValuesf(ht, value, ps.AmountSat, - "amount not matched: want=%d, got=%d", value, - ps.AmountSat) - - // The following fields can change during the test so we - // return an error if they don't match, which will be - // checked again in this wait call. - if ps.Immediate != true { - return fmt.Errorf("Immediate should be true") - } - - if broadcastAttempts != ps.BroadcastAttempts { - return fmt.Errorf("broadcastAttempts not "+ - "matched: want=%d, got=%d", - broadcastAttempts, ps.BroadcastAttempts) - } - if budget != ps.Budget { - return fmt.Errorf("budget not matched: "+ - "want=%d, got=%d", budget, ps.Budget) - } - - // Since the request doesn't specify a deadline, we - // expect the existing deadline to be used. - if deadline != ps.DeadlineHeight { - return fmt.Errorf("deadline height not "+ - "matched: want=%d, got=%d", deadline, - ps.DeadlineHeight) - } - - // Since the request specifies a starting fee rate, we - // expect that to be used as the starting fee rate. - if startingFeeRate != ps.RequestedSatPerVbyte { - return fmt.Errorf("requested starting fee "+ - "rate not matched: want=%d, got=%d", - startingFeeRate, - ps.RequestedSatPerVbyte) - } - - return nil - }, wait.DefaultTimeout) - require.NoError(ht, err, "timeout checking pending sweep") - - // We expect to see Alice's original tx and her CPFP tx in the - // mempool. - txns := ht.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 := int32(ht.CurrentHeight()) - 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) - - // First we test the case where we specify the conf target to increase - // the starting fee rate of the fee function. - confTargetFeeRate := chainfee.SatPerVByte(50) - ht.SetFeeEstimateWithConf(confTargetFeeRate.FeePerKWeight(), 3) - - // Second bump request - we will specify the conf target and expect a - // starting fee rate that is estimated using the provided estimator. - // - starting fee rate: 50 sat/vbyte (conf target 3). - // - 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, - TargetConf: 3, - } - - alice.RPC.BumpFee(bumpFeeReq) - - // Alice's old sweeping tx should be replaced. - ht.AssertTxNotInMempool(sweepTx1.TxHash()) - - // Assert the pending sweep is created with the expected values: - // - broadcast attempts: 2. - // - starting fee rate: 50 sat/vbyte (conf target 3). - // - deadline: 1008 (default deadline). - // - budget: 50% of the input value. - sweepTx2 := assertPendingSweepResp( - 2, uint64(value/2), deadline, uint64(confTargetFeeRate), - ) - - // testFeeRate sepcifies a starting fee rate in sat/vbyte. - const testFeeRate = uint64(100) - - // Third bump request - we will specify the fee rate and expect a fee - // func to change the starting fee rate of the fee function, - // - 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.AssertTxNotInMempool(sweepTx2.TxHash()) - - // Assert the pending sweep is created with the expected values: - // - broadcast attempts: 3. - // - starting fee rate: 100 sat/vbyte. - // - deadline: 1008 (default deadline). - // - budget: 50% of the input value. - sweepTx3 := assertPendingSweepResp( - 3, 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) - - // Fourth 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.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: 1008 (default deadline). - // - budget: 10% of the input value. - sweepTx4 := assertPendingSweepResp(4, testBudget, deadline, testFeeRate) - - // 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 - - // Fifth 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, - DeadlineDelta: testDeadlineDelta, - Budget: testBudget, - } - alice.RPC.BumpFee(bumpFeeReq) - - // Alice's old sweeping tx should be replaced. - ht.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. - // - budget: 10% of the input value, stays unchanged. - sweepTx5 := assertPendingSweepResp( - 5, testBudget, deadlineHeight, testFeeRate, - ) - - // We expect the current fee rate to be increased because we ensure the - // initial broadcast always succeeds. - assertFeeRateGreater(testFeeRate) - - // Sixth 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.AssertTxNotInMempool(sweepTx5.TxHash()) - - // Assert the pending sweep is created with the expected values: - // - broadcast attempts: 6. - // - starting fee rate: 100 sat/vbyte, stays unchanged. - // - deadline: 100, stays unchanged. - // - budget: 10% of the input value, stays unchanged. - sweepTx6 := assertPendingSweepResp( - 6, testBudget, deadlineHeight, testFeeRate, - ) - - // 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: 1. - // - budget: 1000 sats. - bumpFeeReq = &walletrpc.BumpFeeRequest{ - Outpoint: op, - // We use a force param to create the sweeping tx immediately. - Immediate: true, - SatPerVbyte: startFeeRate, - // The budget and the deadline delta must be set together. - Budget: smallBudget, - DeadlineDelta: 1, - } - alice.RPC.BumpFee(bumpFeeReq) - - // Calculate the ending fee rate, which is used in the above fee bump - // when fee function's max posistion is reached. - txWeight := ht.CalculateTxWeight(sweepTx6) - endingFeeRate := chainfee.NewSatPerKWeight( - btcutil.Amount(smallBudget), txWeight, - ) - - // Since the fee function has been maxed out, the starting fee rate for - // the next sweep attempt should be the ending fee rate. - // - // TODO(yy): The weight estimator used in the sweeper gives a different - // result than the weight calculated here, which is the result from - // `blockchain.GetTransactionWeight`. For this particular tx: - // - result from the `weightEstimator`: 445 wu - // - result from `GetTransactionWeight`: 444 wu - // - // This means the fee rates are different, - // - `weightEstimator`: 2247 sat/kw, or 8 sat/vb (8.988 round down) - // - here we have 2252 sat/kw, or 9 sat/vb (9.008 round down) - // - // We should investigate and check whether if it's possible to make the - // `weightEstimator` more accurate. - expectedStartFeeRate := uint64(endingFeeRate.FeePerVByte()) - 1 - - // Assert the pending sweep is created with the expected values: - // - broadcast attempts: 7. - // - starting fee rate: 8 sat/vbyte. - // - deadline: 1. - // - budget: 1000 sats. - sweepTx7 := assertPendingSweepResp( - 7, smallBudget, uint32(currentHeight+1), expectedStartFeeRate, - ) - - // Since this budget is too small to cover the RBF, we expect the - // sweeping attempt to fail. - require.Equal(ht, sweepTx6.TxHash(), sweepTx7.TxHash(), "tx6 should "+ - "not be replaced: tx6=%v, tx7=%v", sweepTx6.TxHash(), - sweepTx7.TxHash()) - - // We expect the current fee rate to be increased because we ensure the - // initial broadcast always succeeds. - assertFeeRateGreater(testFeeRate) - - // Clean up the mempool. - ht.MineBlocksAndAssertNumTxes(1, 2) -} - // testBumpForceCloseFee tests that when a force close transaction, in // particular a commitment which has no HTLCs at stake, can be bumped via the // rpc endpoint `BumpForceCloseFee`. @@ -2385,178 +1985,3 @@ func testFeeReplacement(ht *lntest.HarnessTest) { // Finally, clean the mempool. ht.MineBlocksAndAssertNumTxes(1, 1) } - -// testBumpFeeLowBudget checks that when the requested ideal budget cannot be -// met, the sweeper still sweeps the input with the actual budget. -func testBumpFeeLowBudget(ht *lntest.HarnessTest) { - // Create a new node with a large `maxfeerate` so it's easier to run the - // test. - alice := ht.NewNode("Alice", []string{ - "--sweeper.maxfeerate=10000", - }) - - // Fund Alice 2 UTXOs, each has 100k sats. One of the UTXOs will be used - // to create a tx which she sends some coins to herself. The other will - // be used as the budget when CPFPing the above tx. - coin := btcutil.Amount(100_000) - ht.FundCoins(coin, alice) - ht.FundCoins(coin, alice) - - // Alice sends 50k sats to herself. - tx := ht.SendCoins(alice, alice, coin/2) - txid := tx.TxHash() - - // Get Alice's wallet balance to calculate the fees used in the above - // tx. - resp := alice.RPC.WalletBalance() - - // balance is the expected final balance. Alice's initial balance is - // 200k sats, with 100k sats as the budget for the sweeping tx, which - // means her final balance should be 100k sats minus the mining fees - // used in the above `SendCoins`. - balance := btcutil.Amount( - resp.UnconfirmedBalance + resp.ConfirmedBalance, - ) - fee := coin*2 - balance - ht.Logf("Alice's expected final balance=%v, fee=%v", balance, fee) - - // 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(budget uint64, - deadline uint32) *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.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) - - // We expect to see Alice's original tx and her CPFP tx in the - // mempool. - txns := ht.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 - } - - // Use a budget that Alice cannot cover using her wallet UTXOs. - budget := coin * 2 - - // Use a deadlineDelta of 3 such that the fee func is initialized as, - // - starting fee rate: 1 sat/vbyte - // - deadline: 3 - // - budget: 200% of Alice's available funds. - deadlineDelta := 3 - - // First bump request - we expect it to succeed as Alice's current funds - // can cover the fees used here given the position of the fee func is at - // 0. - bumpFeeReq := &walletrpc.BumpFeeRequest{ - Outpoint: op, - Budget: uint64(budget), - Immediate: true, - DeadlineDelta: uint32(deadlineDelta), - } - alice.RPC.BumpFee(bumpFeeReq) - - // Calculate the deadline height. - deadline := ht.CurrentHeight() + uint32(deadlineDelta) - - // Assert the pending sweep is created with the expected values: - // - deadline: 3+current height. - // - budget: 2x the wallet balance. - sweepTx1 := assertPendingSweepResp(uint64(budget), deadline) - - // Mine a block to trigger Alice's sweeper to fee bump the tx. - // - // Second bump request - we expect it to succeed as Alice's current - // funds can cover the fees used here, which is 66.7% of her available - // funds given the position of the fee func is at 1. - ht.MineEmptyBlocks(1) - - // Assert the old sweeping tx has been replaced. - ht.AssertTxNotInMempool(sweepTx1.TxHash()) - - // Assert a new sweeping tx is made. - sweepTx2 := assertPendingSweepResp(uint64(budget), deadline) - - // Mine a block to trigger Alice's sweeper to fee bump the tx. - // - // Third bump request - we expect it to fail as Alice's current funds - // cannot cover the fees now, which is 133.3% of her available funds - // given the position of the fee func is at 2. - ht.MineEmptyBlocks(1) - - // Assert the above sweeping tx is still in the mempool. - ht.AssertTxInMempool(sweepTx2.TxHash()) - - // Fund Alice 200k sats, which will be used to cover the budget. - // - // TODO(yy): We are funding Alice more than enough - at this stage Alice - // has a confirmed UTXO of `coin` amount in her wallet, so ideally we - // should only fund another UTXO of `coin` amount. However, since the - // confirmed wallet UTXO has already been used in sweepTx2, there's no - // easy way to tell her wallet to reuse that UTXO in the upcoming - // sweeping tx. - // To properly fix it, we should provide more granular UTXO management - // here by leveraing `LeaseOutput` - whenever we use a wallet UTXO, we - // should lock it first. And when the sweeping attempt fails, we should - // release it so the UTXO can be used again in another batch. - walletTx := ht.FundCoinsUnconfirmed(coin*2, alice) - - // Mine a block to confirm the above funding coin. - // - // Fourth bump request - we expect it to succeed as Alice's current - // funds can cover the full budget. - ht.MineBlockWithTx(walletTx) - - flakeRaceInBitcoinClientNotifications(ht) - - // Assert Alice's previous sweeping tx has been replaced. - ht.AssertTxNotInMempool(sweepTx2.TxHash()) - - // Assert the pending sweep is created with the expected values: - // - deadline: 3+current height. - // - budget: 2x the wallet balance. - sweepTx3 := assertPendingSweepResp(uint64(budget), deadline) - require.NotEqual(ht, sweepTx2.TxHash(), sweepTx3.TxHash()) - - // Mine the sweeping tx. - ht.MineBlocksAndAssertNumTxes(1, 2) - - // Assert Alice's wallet balance. a - ht.WaitForBalanceConfirmed(alice, balance) -}