package itest import ( "fmt" "math" "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lncfg" "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" ) // testSweepCPFPAnchorOutgoingTimeout checks when a channel is force closed by // a local node due to the outgoing HTLC times out, the anchor output is used // for CPFPing the force close tx. // // Setup: // 1. Fund Alice with 1 UTXO - she only needs one for the funding process, // 2. Fund Bob with 1 UTXO - he only needs one for the funding process, and // the change output will be used for sweeping his anchor on local commit. // 3. Create a linear network from Alice -> Bob -> Carol. // 4. Alice pays an invoice to Carol through Bob, with Carol holding the // settlement. // 5. Carol goes offline. // // Test: // 1. Bob force closes the channel with Carol, using the anchor output for // CPFPing the force close tx. // 2. Bob's anchor output is swept and fee bumped based on its deadline and // budget. func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) { // Setup testing params. // // Invoice is 100k sats. invoiceAmt := btcutil.Amount(100_000) // Use the smallest CLTV so we can mine fewer blocks. cltvDelta := routing.MinCLTVDelta // deadlineDeltaAnchor is the expected deadline delta for the CPFP // anchor sweeping tx. deadlineDeltaAnchor := uint32(cltvDelta / 2) // startFeeRateAnchor is the starting fee rate for the CPFP anchor // sweeping tx. startFeeRateAnchor := chainfee.SatPerKWeight(2500) // Set up the fee estimator to return the testing fee rate when the // conf target is the deadline. // // TODO(yy): switch to conf when `blockbeat` is in place. // ht.SetFeeEstimateWithConf(startFeeRateAnchor, deadlineDeltaAnchor) ht.SetFeeEstimate(startFeeRateAnchor) // htlcValue is the outgoing HTLC's value. htlcValue := invoiceAmt // htlcBudget is the budget used to sweep the outgoing HTLC. htlcBudget := htlcValue.MulF64(contractcourt.DefaultBudgetRatio) // cpfpBudget is the budget used to sweep the CPFP anchor. // In addition to the htlc amount to protect we also need to include // the anchor amount itself for the budget. cpfpBudget := (htlcValue - htlcBudget).MulF64( contractcourt.DefaultBudgetRatio, ) + contractcourt.AnchorOutputValue // Create a preimage, that will be held by Carol. var preimage lntypes.Preimage copy(preimage[:], ht.Random32Bytes()) payHash := preimage.Hash() // We now set up the force close scenario. We will create a network // from Alice -> Bob -> Carol, where Alice will send a payment to Carol // via Bob, Carol goes offline. We expect Bob to sweep his anchor and // outgoing HTLC. // // Prepare params. cfg := []string{ "--protocol.anchors", // Use a small CLTV to mine less blocks. fmt.Sprintf("--bitcoin.timelockdelta=%d", cltvDelta), // Use a very large CSV, this way to_local outputs are never // swept so we can focus on testing HTLCs. fmt.Sprintf("--bitcoin.defaultremotedelay=%v", cltvDelta*10), } openChannelParams := lntest.OpenChannelParams{ Amt: invoiceAmt * 10, } // Create a three hop network: Alice -> Bob -> Carol. chanPoints, nodes := createSimpleNetwork(ht, cfg, 3, openChannelParams) // Unwrap the results. abChanPoint, bcChanPoint := chanPoints[0], chanPoints[1] alice, bob, carol := nodes[0], nodes[1], nodes[2] // For neutrino backend, we need one more UTXO for Bob to create his // sweeping txns. if ht.IsNeutrinoBackend() { ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) } // Subscribe the invoice. streamCarol := carol.RPC.SubscribeSingleInvoice(payHash[:]) // With the network active, we'll now add a hodl invoice at Carol's // end. invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ Value: int64(invoiceAmt), CltvExpiry: finalCltvDelta, Hash: payHash[:], } invoice := carol.RPC.AddHoldInvoice(invoiceReq) // Let Alice pay the invoices. req := &routerrpc.SendPaymentRequest{ PaymentRequest: invoice.PaymentRequest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } // Assert the payments are inflight. ht.SendPaymentAndAssertStatus(alice, req, lnrpc.Payment_IN_FLIGHT) // Wait for Carol to mark invoice as accepted. There is a small gap to // bridge between adding the htlc to the channel and executing the exit // hop logic. ht.AssertInvoiceState(streamCarol, lnrpc.Invoice_ACCEPTED) // At this point, all 3 nodes should now have an active channel with // the created HTLCs pending on all of them. // // Alice should have one outgoing HTLCs on channel Alice -> Bob. ht.AssertOutgoingHTLCActive(alice, abChanPoint, payHash[:]) // Bob should have one incoming HTLC on channel Alice -> Bob, and one // outgoing HTLC on channel Bob -> Carol. ht.AssertIncomingHTLCActive(bob, abChanPoint, payHash[:]) ht.AssertOutgoingHTLCActive(bob, bcChanPoint, payHash[:]) // Carol should have one incoming HTLC on channel Bob -> Carol. ht.AssertIncomingHTLCActive(carol, bcChanPoint, payHash[:]) // Let Carol go offline so we can focus on testing Bob's sweeping // behavior. ht.Shutdown(carol) // We'll now mine enough blocks to trigger Bob to force close channel // Bob->Carol due to his outgoing HTLC is about to timeout. With the // default outgoing broadcast delta of zero, this will be the same // height as the outgoing htlc's expiry height. numBlocks := padCLTV(uint32( invoiceReq.CltvExpiry - lncfg.DefaultOutgoingBroadcastDelta, )) ht.MineEmptyBlocks(int(numBlocks)) // Assert Bob's force closing tx has been broadcast. closeTxid := ht.AssertNumTxsInMempool(1)[0] // Remember the force close height so we can calculate the deadline // height. forceCloseHeight := ht.CurrentHeight() // Bob should have two pending sweeps, // - anchor sweeping from his local commitment. // - anchor sweeping from his remote commitment (invalid). // // TODO(yy): consider only sweeping the anchor from the local // commitment. Previously we would sweep up to three versions of // anchors because we don't know which one will be confirmed - if we // only broadcast the local anchor sweeping, our peer can broadcast // their commitment tx and replaces ours. With the new fee bumping, we // should be safe to only sweep our local anchor since we RBF it on // every new block, which destroys the remote's ability to pin us. sweeps := ht.AssertNumPendingSweeps(bob, 2) // The two anchor sweeping should have the same deadline height. deadlineHeight := forceCloseHeight + deadlineDeltaAnchor require.Equal(ht, deadlineHeight, sweeps[0].DeadlineHeight) require.Equal(ht, deadlineHeight, sweeps[1].DeadlineHeight) // Remember the deadline height for the CPFP anchor. anchorDeadline := sweeps[0].DeadlineHeight // Mine a block so Bob's force closing tx stays in the mempool, which // also triggers the CPFP anchor sweep. ht.MineEmptyBlocks(1) // Bob should still have two pending sweeps, // - anchor sweeping from his local commitment. // - anchor sweeping from his remote commitment (invalid). ht.AssertNumPendingSweeps(bob, 2) // We now check the expected fee and fee rate are used for Bob's anchor // sweeping tx. // // We should see Bob's anchor sweeping tx triggered by the above // block, along with his force close tx. txns := ht.GetNumTxsFromMempool(2) // Find the sweeping tx. sweepTx := ht.FindSweepingTxns(txns, 1, closeTxid)[0] // Get the weight for Bob's anchor sweeping tx. txWeight := ht.CalculateTxWeight(sweepTx) // Bob should start with the initial fee rate of 2500 sat/kw. startFeeAnchor := startFeeRateAnchor.FeeForWeight(txWeight) // Calculate the fee and fee rate of Bob's sweeping tx. fee := uint64(ht.CalculateTxFee(sweepTx)) feeRate := uint64(ht.CalculateTxFeeRate(sweepTx)) // feeFuncWidth is the width of the fee function. By the time we got // here, we've already mined one block, and the fee function maxes // out one block before the deadline, so the width is the original // deadline minus 2. feeFuncWidth := deadlineDeltaAnchor - 2 // Calculate the expected delta increased per block. feeDelta := (cpfpBudget - startFeeAnchor).MulF64( 1 / float64(feeFuncWidth), ) // We expect the startingFee and startingFeeRate being used. Allow some // deviation because weight estimates during tx generation are // estimates. // // TODO(yy): unify all the units and types re int vs uint! require.InEpsilonf(ht, uint64(startFeeAnchor), fee, 0.01, "want %d, got %d", startFeeAnchor, fee) require.InEpsilonf(ht, uint64(startFeeRateAnchor), feeRate, 0.01, "want %d, got %d", startFeeRateAnchor, fee) // We now mine deadline-2 empty blocks. For each block mined, Bob // should perform an RBF on his CPFP anchor sweeping tx. By the end of // this iteration, we expect Bob to use up his CPFP budget after one // more block. for i := uint32(1); i <= feeFuncWidth-1; i++ { // Mine an empty block. Since the sweeping tx is not confirmed, // Bob's fee bumper should increase its fees. ht.MineEmptyBlocks(1) // Bob should still have two pending sweeps, // - anchor sweeping from his local commitment. // - anchor sweeping from his remote commitment (invalid). ht.AssertNumPendingSweeps(bob, 2) // Make sure Bob's old sweeping tx has been removed from the // mempool. ht.AssertTxNotInMempool(sweepTx.TxHash()) // We expect to see two txns in the mempool, // - Bob's force close tx. // - Bob's anchor sweep tx. ht.AssertNumTxsInMempool(2) // We expect the fees to increase by i*delta. expectedFee := startFeeAnchor + feeDelta.MulF64(float64(i)) expectedFeeRate := chainfee.NewSatPerKWeight( expectedFee, txWeight, ) // We should see Bob's anchor sweeping tx being fee bumped // since it's not confirmed, along with his force close tx. txns = ht.GetNumTxsFromMempool(2) // Find the sweeping tx. sweepTx = ht.FindSweepingTxns(txns, 1, closeTxid)[0] // Calculate the fee rate of Bob's new sweeping tx. feeRate = uint64(ht.CalculateTxFeeRate(sweepTx)) // Calculate the fee of Bob's new sweeping tx. fee = uint64(ht.CalculateTxFee(sweepTx)) ht.Logf("Bob(position=%v): txWeight=%v, expected: [fee=%d, "+ "feerate=%v], got: [fee=%v, feerate=%v]", feeFuncWidth-i, txWeight, expectedFee, expectedFeeRate, fee, feeRate) // Assert Bob's tx has the expected fee and fee rate. require.InEpsilonf(ht, uint64(expectedFee), fee, 0.01, "deadline=%v, want %d, got %d", i, expectedFee, fee) require.InEpsilonf(ht, uint64(expectedFeeRate), feeRate, 0.01, "deadline=%v, want %d, got %d", i, expectedFeeRate, feeRate) } // We now check the budget has been used up at the deadline-1 block. // // Once out of the above loop, we expect to be 2 blocks before the CPFP // deadline. currentHeight := ht.CurrentHeight() require.Equal(ht, int(anchorDeadline-2), int(currentHeight)) // Mine one more block, we'd use up all the CPFP budget. ht.MineEmptyBlocks(1) // Make sure Bob's old sweeping tx has been removed from the mempool. ht.AssertTxNotInMempool(sweepTx.TxHash()) // Get the last sweeping tx - we should see two txns here, Bob's anchor // sweeping tx and his force close tx. txns = ht.GetNumTxsFromMempool(2) // Find the sweeping tx. sweepTx = ht.FindSweepingTxns(txns, 1, closeTxid)[0] // Calculate the fee of Bob's new sweeping tx. fee = uint64(ht.CalculateTxFee(sweepTx)) // Assert the budget is now used up. require.InEpsilonf(ht, uint64(cpfpBudget), fee, 0.01, "want %d, got %d", cpfpBudget, fee) // Mine one more block. Since Bob's budget has been used up, there // won't be any more sweeping attempts. We now assert this by checking // that the sweeping tx stayed unchanged. ht.MineEmptyBlocks(1) // Get the current sweeping tx and assert it stays unchanged. // // We expect two txns here, one for the anchor sweeping, the other for // the force close tx. txns = ht.GetNumTxsFromMempool(2) // Find the sweeping tx. currentSweepTx := ht.FindSweepingTxns(txns, 1, closeTxid)[0] // Assert the anchor sweep tx stays unchanged. require.Equal(ht, sweepTx.TxHash(), currentSweepTx.TxHash()) // Mine a block to confirm Bob's sweeping and force close txns, this is // needed to clean up the mempool. ht.MineBlocksAndAssertNumTxes(1, 2) // The above mined block should confirm Bob's force close tx, and his // contractcourt will offer the HTLC to his sweeper. We are not testing // the HTLC sweeping behaviors so we just perform a simple check and // exit the test. ht.AssertNumPendingSweeps(bob, 1) // Finally, clean the mempool for the next test. ht.CleanShutDown() } // testSweepCPFPAnchorIncomingTimeout checks when a channel is force closed by // a local node due to the incoming HTLC is about to time out, the anchor // output is used for CPFPing the force close tx. // // Setup: // 1. Fund Alice with 1 UTXOs - she only needs one for the funding process, // 2. Fund Bob with 1 UTXO - he only needs one for the funding process, and // the change output will be used for sweeping his anchor on local commit. // 3. Create a linear network from Alice -> Bob -> Carol. // 4. Alice pays an invoice to Carol through Bob. // 5. Alice goes offline. // 6. Carol settles the invoice. // // Test: // 1. Bob force closes the channel with Alice, using the anchor output for // CPFPing the force close tx. // 2. Bob's anchor output is swept and fee bumped based on its deadline and // budget. func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) { // Setup testing params. // // Invoice is 100k sats. invoiceAmt := btcutil.Amount(100_000) // Use the smallest CLTV so we can mine fewer blocks. cltvDelta := routing.MinCLTVDelta // goToChainDelta is the broadcast delta of Bob's incoming HTLC. When // the block height is at CLTV-goToChainDelta, Bob will force close the // channel Alice=>Bob. goToChainDelta := uint32(lncfg.DefaultIncomingBroadcastDelta) // deadlineDeltaAnchor is the expected deadline delta for the CPFP // anchor sweeping tx. deadlineDeltaAnchor := goToChainDelta / 2 // startFeeRateAnchor is the starting fee rate for the CPFP anchor // sweeping tx. startFeeRateAnchor := chainfee.SatPerKWeight(2500) // Set up the fee estimator to return the testing fee rate when the // conf target is the deadline. // // TODO(yy): switch to conf when `blockbeat` is in place. // ht.SetFeeEstimateWithConf(startFeeRateAnchor, deadlineDeltaAnchor) ht.SetFeeEstimate(startFeeRateAnchor) // Create a preimage, that will be held by Carol. var preimage lntypes.Preimage copy(preimage[:], ht.Random32Bytes()) payHash := preimage.Hash() // We now set up the force close scenario. We will create a network // from Alice -> Bob -> Carol, where Alice will send a payment to Carol // via Bob, Alice goes offline, Carol settles the payment. We expect // Bob to sweep his anchor and incoming HTLC. // // Prepare params. cfg := []string{ "--protocol.anchors", // Use a small CLTV to mine less blocks. fmt.Sprintf("--bitcoin.timelockdelta=%d", cltvDelta), // Use a very large CSV, this way to_local outputs are never // swept so we can focus on testing HTLCs. fmt.Sprintf("--bitcoin.defaultremotedelay=%v", cltvDelta*10), } openChannelParams := lntest.OpenChannelParams{ Amt: invoiceAmt * 10, } // Create a three hop network: Alice -> Bob -> Carol. chanPoints, nodes := createSimpleNetwork(ht, cfg, 3, openChannelParams) // Unwrap the results. abChanPoint, bcChanPoint := chanPoints[0], chanPoints[1] alice, bob, carol := nodes[0], nodes[1], nodes[2] // For neutrino backend, we need one more UTXO for Bob to create his // sweeping txns. if ht.IsNeutrinoBackend() { ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) } // Subscribe the invoice. streamCarol := carol.RPC.SubscribeSingleInvoice(payHash[:]) // With the network active, we'll now add a hodl invoice at Carol's // end. invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ Value: int64(invoiceAmt), CltvExpiry: finalCltvDelta, Hash: payHash[:], } invoice := carol.RPC.AddHoldInvoice(invoiceReq) // Let Alice pay the invoices. req := &routerrpc.SendPaymentRequest{ PaymentRequest: invoice.PaymentRequest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } // Assert the payments are inflight. ht.SendPaymentAndAssertStatus(alice, req, lnrpc.Payment_IN_FLIGHT) // Wait for Carol to mark invoice as accepted. There is a small gap to // bridge between adding the htlc to the channel and executing the exit // hop logic. ht.AssertInvoiceState(streamCarol, lnrpc.Invoice_ACCEPTED) // At this point, all 3 nodes should now have an active channel with // the created HTLCs pending on all of them. // // Alice should have one outgoing HTLCs on channel Alice -> Bob. ht.AssertOutgoingHTLCActive(alice, abChanPoint, payHash[:]) // Bob should have one incoming HTLC on channel Alice -> Bob, and one // outgoing HTLC on channel Bob -> Carol. htlc := ht.AssertIncomingHTLCActive(bob, abChanPoint, payHash[:]) ht.AssertOutgoingHTLCActive(bob, bcChanPoint, payHash[:]) // Calculate the budget used for Bob's anchor sweeping. // // htlcValue is the incoming HTLC's value. htlcValue := btcutil.Amount(htlc.Amount) // htlcBudget is the budget used to sweep the incoming HTLC. htlcBudget := htlcValue.MulF64(contractcourt.DefaultBudgetRatio) // cpfpBudget is the budget used to sweep the CPFP anchor. // In addition to the htlc amount to protect we also need to include // the anchor amount itself for the budget. cpfpBudget := (htlcValue - htlcBudget).MulF64( contractcourt.DefaultBudgetRatio, ) + contractcourt.AnchorOutputValue // Carol should have one incoming HTLC on channel Bob -> Carol. ht.AssertIncomingHTLCActive(carol, bcChanPoint, payHash[:]) // Let Alice go offline. Once Bob later learns the preimage, he // couldn't settle it with Alice so he has to go onchain to collect it. ht.Shutdown(alice) // Carol settles invoice. carol.RPC.SettleInvoice(preimage[:]) // Bob should have settled his outgoing HTLC with Carol. ht.AssertHTLCNotActive(bob, bcChanPoint, payHash[:]) // We'll now mine enough blocks to trigger Bob to force close channel // Alice->Bob due to his incoming HTLC is about to timeout. With the // default incoming broadcast delta of 10, this will be the same // height as the incoming htlc's expiry height minus 10. forceCloseHeight := htlc.ExpirationHeight - goToChainDelta // Mine till the goToChainHeight is reached. currentHeight := ht.CurrentHeight() numBlocks := forceCloseHeight - currentHeight ht.MineEmptyBlocks(int(numBlocks)) // Assert Bob's force closing tx has been broadcast. closeTxid := ht.AssertNumTxsInMempool(1)[0] // Bob should have two pending sweeps, // - anchor sweeping from his local commitment. // - anchor sweeping from his remote commitment (invalid). sweeps := ht.AssertNumPendingSweeps(bob, 2) // The two anchor sweeping should have the same deadline height. deadlineHeight := forceCloseHeight + deadlineDeltaAnchor require.Equal(ht, deadlineHeight, sweeps[0].DeadlineHeight) require.Equal(ht, deadlineHeight, sweeps[1].DeadlineHeight) // Remember the deadline height for the CPFP anchor. anchorDeadline := sweeps[0].DeadlineHeight // Mine a block so Bob's force closing tx stays in the mempool, which // also triggers the CPFP anchor sweep. ht.MineEmptyBlocks(1) // Bob should still have two pending sweeps, // - anchor sweeping from his local commitment. // - anchor sweeping from his remote commitment (invalid). ht.AssertNumPendingSweeps(bob, 2) // We now check the expected fee and fee rate are used for Bob's anchor // sweeping tx. // // We should see Bob's anchor sweeping tx triggered by the above // block, along with his force close tx. txns := ht.GetNumTxsFromMempool(2) // Find the sweeping tx. sweepTx := ht.FindSweepingTxns(txns, 1, closeTxid)[0] // Get the weight for Bob's anchor sweeping tx. txWeight := ht.CalculateTxWeight(sweepTx) // Bob should start with the initial fee rate of 2500 sat/kw. startFeeAnchor := startFeeRateAnchor.FeeForWeight(txWeight) // Calculate the fee and fee rate of Bob's sweeping tx. fee := uint64(ht.CalculateTxFee(sweepTx)) feeRate := uint64(ht.CalculateTxFeeRate(sweepTx)) // feeFuncWidth is the width of the fee function. By the time we got // here, we've already mined one block, and the fee function maxes // out one block before the deadline, so the width is the original // deadline minus 2. feeFuncWidth := deadlineDeltaAnchor - 2 // Calculate the expected delta increased per block. feeDelta := (cpfpBudget - startFeeAnchor).MulF64( 1 / float64(feeFuncWidth), ) // We expect the startingFee and startingFeeRate being used. Allow some // deviation because weight estimates during tx generation are // estimates. // // TODO(yy): unify all the units and types re int vs uint! require.InEpsilonf(ht, uint64(startFeeAnchor), fee, 0.01, "want %d, got %d", startFeeAnchor, fee) require.InEpsilonf(ht, uint64(startFeeRateAnchor), feeRate, 0.01, "want %d, got %d", startFeeRateAnchor, fee) // We now mine deadline-2 empty blocks. For each block mined, Bob // should perform an RBF on his CPFP anchor sweeping tx. By the end of // this iteration, we expect Bob to use up his CPFP budget after one // more block. for i := uint32(1); i <= feeFuncWidth-1; i++ { // Mine an empty block. Since the sweeping tx is not confirmed, // Bob's fee bumper should increase its fees. ht.MineEmptyBlocks(1) // Bob should still have two pending sweeps, // - anchor sweeping from his local commitment. // - anchor sweeping from his remote commitment (invalid). ht.AssertNumPendingSweeps(bob, 2) // Make sure Bob's old sweeping tx has been removed from the // mempool. ht.AssertTxNotInMempool(sweepTx.TxHash()) // We expect to see two txns in the mempool, // - Bob's force close tx. // - Bob's anchor sweep tx. ht.AssertNumTxsInMempool(2) // We expect the fees to increase by i*delta. expectedFee := startFeeAnchor + feeDelta.MulF64(float64(i)) expectedFeeRate := chainfee.NewSatPerKWeight( expectedFee, txWeight, ) // We should see Bob's anchor sweeping tx being fee bumped // since it's not confirmed, along with his force close tx. txns = ht.GetNumTxsFromMempool(2) // Find the sweeping tx. sweepTx = ht.FindSweepingTxns(txns, 1, closeTxid)[0] // Calculate the fee rate of Bob's new sweeping tx. feeRate = uint64(ht.CalculateTxFeeRate(sweepTx)) // Calculate the fee of Bob's new sweeping tx. fee = uint64(ht.CalculateTxFee(sweepTx)) ht.Logf("Bob(position=%v): txWeight=%v, expected: [fee=%d, "+ "feerate=%v], got: [fee=%v, feerate=%v]", feeFuncWidth-i, txWeight, expectedFee, expectedFeeRate, fee, feeRate) // Assert Bob's tx has the expected fee and fee rate. require.InEpsilonf(ht, uint64(expectedFee), fee, 0.01, "deadline=%v, want %d, got %d", i, expectedFee, fee) require.InEpsilonf(ht, uint64(expectedFeeRate), feeRate, 0.01, "deadline=%v, want %d, got %d", i, expectedFeeRate, feeRate) } // We now check the budget has been used up at the deadline-1 block. // // Once out of the above loop, we expect to be 2 blocks before the CPFP // deadline. currentHeight = ht.CurrentHeight() require.Equal(ht, int(anchorDeadline-2), int(currentHeight)) // Mine one more block, we'd use up all the CPFP budget. ht.MineEmptyBlocks(1) // Make sure Bob's old sweeping tx has been removed from the mempool. ht.AssertTxNotInMempool(sweepTx.TxHash()) // Get the last sweeping tx - we should see two txns here, Bob's anchor // sweeping tx and his force close tx. txns = ht.GetNumTxsFromMempool(2) // Find the sweeping tx. sweepTx = ht.FindSweepingTxns(txns, 1, closeTxid)[0] // Calculate the fee of Bob's new sweeping tx. fee = uint64(ht.CalculateTxFee(sweepTx)) // Assert the budget is now used up. require.InEpsilonf(ht, uint64(cpfpBudget), fee, 0.01, "want %d, got %d", cpfpBudget, fee) // Mine one more block. Since Bob's budget has been used up, there // won't be any more sweeping attempts. We now assert this by checking // that the sweeping tx stayed unchanged. ht.MineEmptyBlocks(1) // Get the current sweeping tx and assert it stays unchanged. // // We expect two txns here, one for the anchor sweeping, the other for // the force close tx. txns = ht.GetNumTxsFromMempool(2) // Find the sweeping tx. currentSweepTx := ht.FindSweepingTxns(txns, 1, closeTxid)[0] // Assert the anchor sweep tx stays unchanged. require.Equal(ht, sweepTx.TxHash(), currentSweepTx.TxHash()) // Mine a block to confirm Bob's sweeping and force close txns, this is // needed to clean up the mempool. ht.MineBlocksAndAssertNumTxes(1, 2) // The above mined block should confirm Bob's force close tx, and his // contractcourt will offer the HTLC to his sweeper. We are not testing // the HTLC sweeping behaviors so we just perform a simple check and // exit the test. ht.AssertNumPendingSweeps(bob, 1) // Finally, clean the mempool for the next test. ht.CleanShutDown() } // testSweepHTLCs checks the sweeping behavior for HTLC outputs. Since HTLCs // are time-sensitive, we expect to see both the incoming and outgoing HTLCs // are fee bumped properly based on their budgets and deadlines. // // Setup: // 1. Fund Alice with 1 UTXOs - she only needs one for the funding process, // 2. Fund Bob with 3 UTXOs - he needs one for the funding process, one for // his CPFP anchor sweeping, and one for sweeping his outgoing HTLC. // 3. Create a linear network from Alice -> Bob -> Carol. // 4. Alice pays two invoices to Carol, with Carol holding the settlement. // 5. Alice goes offline. // 7. Carol settles one of the invoices with Bob, so Bob has an incoming HTLC // that he can claim onchain since he has the preimage. // 8. Carol goes offline. // 9. Assert Bob sweeps his incoming and outgoing HTLCs with the expected fee // rates. // // Test: // 1. Bob's outgoing HTLC is swept and fee bumped based on its deadline and // budget. // 2. Bob's incoming HTLC is swept and fee bumped based on its deadline and // budget. func testSweepHTLCs(ht *lntest.HarnessTest) { // Setup testing params. // // Invoice is 100k sats. invoiceAmt := btcutil.Amount(100_000) // Use the smallest CLTV so we can mine fewer blocks. cltvDelta := routing.MinCLTVDelta // Start tracking the deadline delta of Bob's HTLCs. We need one block // for the CSV lock, and another block to trigger the sweeper to sweep. outgoingHTLCDeadline := int32(cltvDelta - 2) incomingHTLCDeadline := int32(lncfg.DefaultIncomingBroadcastDelta - 2) // startFeeRate1 and startFeeRate2 are returned by the fee estimator in // sat/kw. They will be used as the starting fee rate for the linear // fee func used by Bob. The values are chosen from calling the cli in // bitcoind: // - `estimatesmartfee 18 conservative`. // - `estimatesmartfee 10 conservative`. startFeeRate1 := chainfee.SatPerKWeight(2500) startFeeRate2 := chainfee.SatPerKWeight(3000) // Set up the fee estimator to return the testing fee rate when the // conf target is the deadline. ht.SetFeeEstimateWithConf(startFeeRate1, uint32(outgoingHTLCDeadline)) ht.SetFeeEstimateWithConf(startFeeRate2, uint32(incomingHTLCDeadline)) // Create two preimages, one that will be settled, the other be hold. var preimageSettled, preimageHold lntypes.Preimage copy(preimageSettled[:], ht.Random32Bytes()) copy(preimageHold[:], ht.Random32Bytes()) payHashSettled := preimageSettled.Hash() payHashHold := preimageHold.Hash() // We now set up the force close scenario. We will create a network // from Alice -> Bob -> Carol, where Alice will send two payments to // Carol via Bob, Alice goes offline, then Carol settles the first // payment, goes offline. We expect Bob to sweep his incoming and // outgoing HTLCs. // // Prepare params. cfg := []string{ "--protocol.anchors", // Use a small CLTV to mine less blocks. fmt.Sprintf("--bitcoin.timelockdelta=%d", cltvDelta), // Use a very large CSV, this way to_local outputs are never // swept so we can focus on testing HTLCs. fmt.Sprintf("--bitcoin.defaultremotedelay=%v", cltvDelta*10), } openChannelParams := lntest.OpenChannelParams{ Amt: invoiceAmt * 10, } // Create a three hop network: Alice -> Bob -> Carol. chanPoints, nodes := createSimpleNetwork(ht, cfg, 3, openChannelParams) // Unwrap the results. abChanPoint, bcChanPoint := chanPoints[0], chanPoints[1] alice, bob, carol := nodes[0], nodes[1], nodes[2] // Bob needs two more wallet utxos: // - when sweeping anchors, he needs one utxo for each sweep. // - when sweeping HTLCs, he needs one utxo for each sweep. ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) // For neutrino backend, we need two more UTXOs for Bob to create his // sweeping txns. if ht.IsNeutrinoBackend() { ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) } // Subscribe the invoices. stream1 := carol.RPC.SubscribeSingleInvoice(payHashSettled[:]) stream2 := carol.RPC.SubscribeSingleInvoice(payHashHold[:]) // With the network active, we'll now add two hodl invoices at Carol's // end. invoiceReqSettle := &invoicesrpc.AddHoldInvoiceRequest{ Value: int64(invoiceAmt), CltvExpiry: finalCltvDelta, Hash: payHashSettled[:], } invoiceSettle := carol.RPC.AddHoldInvoice(invoiceReqSettle) invoiceReqHold := &invoicesrpc.AddHoldInvoiceRequest{ Value: int64(invoiceAmt), CltvExpiry: finalCltvDelta, Hash: payHashHold[:], } invoiceHold := carol.RPC.AddHoldInvoice(invoiceReqHold) // Let Alice pay the invoices. req1 := &routerrpc.SendPaymentRequest{ PaymentRequest: invoiceSettle.PaymentRequest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } req2 := &routerrpc.SendPaymentRequest{ PaymentRequest: invoiceHold.PaymentRequest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } // Assert the payments are inflight. ht.SendPaymentAndAssertStatus(alice, req1, lnrpc.Payment_IN_FLIGHT) ht.SendPaymentAndAssertStatus(alice, req2, lnrpc.Payment_IN_FLIGHT) // Wait for Carol to mark invoice as accepted. There is a small gap to // bridge between adding the htlc to the channel and executing the exit // hop logic. ht.AssertInvoiceState(stream1, lnrpc.Invoice_ACCEPTED) ht.AssertInvoiceState(stream2, lnrpc.Invoice_ACCEPTED) // At this point, all 3 nodes should now have an active channel with // the created HTLCs pending on all of them. // // Alice should have two outgoing HTLCs on channel Alice -> Bob. ht.AssertOutgoingHTLCActive(alice, abChanPoint, payHashSettled[:]) ht.AssertOutgoingHTLCActive(alice, abChanPoint, payHashHold[:]) // Bob should have two incoming HTLCs on channel Alice -> Bob, and two // outgoing HTLCs on channel Bob -> Carol. ht.AssertIncomingHTLCActive(bob, abChanPoint, payHashSettled[:]) ht.AssertIncomingHTLCActive(bob, abChanPoint, payHashHold[:]) ht.AssertOutgoingHTLCActive(bob, bcChanPoint, payHashSettled[:]) ht.AssertOutgoingHTLCActive(bob, bcChanPoint, payHashHold[:]) // Carol should have two incoming HTLCs on channel Bob -> Carol. ht.AssertIncomingHTLCActive(carol, bcChanPoint, payHashSettled[:]) ht.AssertIncomingHTLCActive(carol, bcChanPoint, payHashHold[:]) // Let Alice go offline. Once Bob later learns the preimage, he // couldn't settle it with Alice so he has to go onchain to collect it. ht.Shutdown(alice) // Carol settles the first invoice. carol.RPC.SettleInvoice(preimageSettled[:]) // Let Carol go offline so we can focus on testing Bob's sweeping // behavior. ht.Shutdown(carol) // Bob should have settled his outgoing HTLC with Carol. ht.AssertHTLCNotActive(bob, bcChanPoint, payHashSettled[:]) // We'll now mine enough blocks to trigger Bob to force close channel // Bob->Carol due to his outgoing HTLC is about to timeout. With the // default outgoing broadcast delta of zero, this will be the same // height as the htlc expiry height. numBlocks := padCLTV(uint32( invoiceReqHold.CltvExpiry - lncfg.DefaultOutgoingBroadcastDelta, )) ht.MineBlocks(int(numBlocks)) // Before we mine empty blocks to check the RBF behavior, we need to be // aware that Bob's incoming HTLC will expire before his outgoing HTLC // deadline is reached. This happens because the incoming HTLC is sent // onchain at CLTVDelta-BroadcastDelta=18-10=8, which means after 8 // blocks are mined, we expect Bob force closes the channel Alice->Bob. blocksTillIncomingSweep := cltvDelta - lncfg.DefaultIncomingBroadcastDelta // Bob should now have two pending sweeps, one for the anchor on the // local commitment, the other on the remote commitment. ht.AssertNumPendingSweeps(bob, 2) // Assert Bob's force closing tx has been broadcast. ht.AssertNumTxsInMempool(1) // Mine the force close tx, which triggers Bob's contractcourt to offer // his outgoing HTLC to his sweeper. // // NOTE: HTLC outputs are only offered to sweeper when the force close // tx is confirmed and the CSV has reached. ht.MineBlocksAndAssertNumTxes(1, 1) // Update the blocks left till Bob force closes Alice->Bob. blocksTillIncomingSweep-- // Bob should have two pending sweeps, one for the anchor sweeping, the // other for the outgoing HTLC. ht.AssertNumPendingSweeps(bob, 2) // Mine one block to confirm Bob's anchor sweeping tx, which will // trigger his sweeper to publish the HTLC sweeping tx. ht.MineBlocksAndAssertNumTxes(1, 1) // Update the blocks left till Bob force closes Alice->Bob. blocksTillIncomingSweep-- // Bob should now have one sweep and one sweeping tx in the mempool. ht.AssertNumPendingSweeps(bob, 1) outgoingSweep := ht.GetNumTxsFromMempool(1)[0] // Check the shape of the sweeping tx - we expect it to be // 2-input-2-output as a wallet utxo is used and a required output is // made. require.Len(ht, outgoingSweep.TxIn, 2) require.Len(ht, outgoingSweep.TxOut, 2) // Calculate the ending fee rate. // // TODO(yy): the budget we use to sweep the first-level outgoing HTLC // is twice its value. This is a temporary mitigation to prevent // cascading FCs and the test should be updated once it's properly // fixed. outgoingBudget := 2 * invoiceAmt outgoingTxSize := ht.CalculateTxWeight(outgoingSweep) outgoingEndFeeRate := chainfee.NewSatPerKWeight( outgoingBudget, outgoingTxSize, ) // Assert the initial sweeping tx is using the start fee rate. outgoingStartFeeRate := ht.CalculateTxFeeRate(outgoingSweep) require.InEpsilonf(ht, uint64(startFeeRate1), uint64(outgoingStartFeeRate), 0.01, "want %d, got %d", startFeeRate1, outgoingStartFeeRate) // Now the start fee rate is checked, we can calculate the fee rate // delta. outgoingFeeRateDelta := (outgoingEndFeeRate - outgoingStartFeeRate) / chainfee.SatPerKWeight(outgoingHTLCDeadline-1) // outgoingFuncPosition records the position of Bob's fee function used // for his outgoing HTLC sweeping tx. outgoingFuncPosition := int32(0) // assertSweepFeeRate is a helper closure that asserts the expected fee // rate is used at the given position for a sweeping tx. assertSweepFeeRate := func(sweepTx *wire.MsgTx, startFeeRate, delta chainfee.SatPerKWeight, txSize lntypes.WeightUnit, deadline, position int32, desc string) { // Bob's HTLC sweeping tx should be fee bumped. feeRate := ht.CalculateTxFeeRate(sweepTx) expectedFeeRate := startFeeRate + delta*chainfee.SatPerKWeight( position, ) ht.Logf("Bob's %s HTLC (deadline=%v): txWeight=%v, want "+ "feerate=%v, got feerate=%v, delta=%v", desc, deadline-position, txSize, expectedFeeRate, feeRate, delta) require.InEpsilonf(ht, uint64(expectedFeeRate), uint64(feeRate), 0.01, "want %v, got %v in tx=%v", expectedFeeRate, feeRate, sweepTx.TxHash()) } // We now mine enough blocks to trigger Bob to force close channel // Alice->Bob. Along the way, we will check his outgoing HTLC sweeping // tx is RBFed as expected. for i := 0; i < blocksTillIncomingSweep-1; i++ { // Mine an empty block. Since the sweeping tx is not confirmed, // Bob's fee bumper should increase its fees. ht.MineEmptyBlocks(1) // Update Bob's fee function position. outgoingFuncPosition++ // We should see Bob's sweeping tx in the mempool. ht.AssertNumTxsInMempool(1) // Make sure Bob's old sweeping tx has been removed from the // mempool. ht.AssertTxNotInMempool(outgoingSweep.TxHash()) // Bob should still have the outgoing HTLC sweep. ht.AssertNumPendingSweeps(bob, 1) // We should see Bob's replacement tx in the mempool. outgoingSweep = ht.GetNumTxsFromMempool(1)[0] // Bob's outgoing HTLC sweeping tx should be fee bumped. assertSweepFeeRate( outgoingSweep, outgoingStartFeeRate, outgoingFeeRateDelta, outgoingTxSize, outgoingHTLCDeadline, outgoingFuncPosition, "Outgoing", ) } // Once exited the above loop and mine one more block, we'd have mined // enough blocks to trigger Bob to force close his channel with Alice. ht.MineEmptyBlocks(1) // Update Bob's fee function position. outgoingFuncPosition++ // Bob should now have three pending sweeps: // 1. the outgoing HTLC output. // 2. the anchor output from his local commitment. // 3. the anchor output from his remote commitment. ht.AssertNumPendingSweeps(bob, 3) // We should see two txns in the mempool: // 1. Bob's outgoing HTLC sweeping tx. // 2. Bob's force close tx for Alice->Bob. txns := ht.GetNumTxsFromMempool(2) // Find the force close tx - we expect it to have a single input. closeTx := txns[0] if len(closeTx.TxIn) != 1 { closeTx = txns[1] } // We don't care the behavior of the anchor sweep in this test, so we // mine the force close tx to trigger Bob's contractcourt to offer his // incoming HTLC to his sweeper. ht.MineBlockWithTx(closeTx) // Update Bob's fee function position. outgoingFuncPosition++ // Bob should now have three pending sweeps: // 1. the outgoing HTLC output on Bob->Carol. // 2. the incoming HTLC output on Alice->Bob. // 3. the anchor sweeping on Alice-> Bob. ht.AssertNumPendingSweeps(bob, 3) // Mine one block, which will trigger his sweeper to publish his // incoming HTLC sweeping tx. ht.MineEmptyBlocks(1) // Update the fee function's positions. outgoingFuncPosition++ // We should see three txns in the mempool: // 1. the outgoing HTLC sweeping tx. // 2. the incoming HTLC sweeping tx. // 3. the anchor sweeping tx. txns = ht.GetNumTxsFromMempool(3) abCloseTxid := closeTx.TxHash() // Identify the sweeping txns spent from Alice->Bob. txns = ht.FindSweepingTxns(txns, 2, abCloseTxid) // Identify the anchor and incoming HTLC sweeps - if the tx has 1 // output, then it's the anchor sweeping tx. var incomingSweep, anchorSweep = txns[0], txns[1] if len(anchorSweep.TxOut) != 1 { incomingSweep, anchorSweep = anchorSweep, incomingSweep } // Calculate the ending fee rate for the incoming HTLC sweep. incomingBudget := invoiceAmt.MulF64(contractcourt.DefaultBudgetRatio) incomingTxSize := ht.CalculateTxWeight(incomingSweep) incomingEndFeeRate := chainfee.NewSatPerKWeight( incomingBudget, incomingTxSize, ) // Assert the initial sweeping tx is using the start fee rate. incomingStartFeeRate := ht.CalculateTxFeeRate(incomingSweep) require.InEpsilonf(ht, uint64(startFeeRate2), uint64(incomingStartFeeRate), 0.01, "want %d, got %d in tx=%v", startFeeRate2, incomingStartFeeRate, incomingSweep.TxHash()) // Now the start fee rate is checked, we can calculate the fee rate // delta. incomingFeeRateDelta := (incomingEndFeeRate - incomingStartFeeRate) / chainfee.SatPerKWeight(incomingHTLCDeadline-1) // incomingFuncPosition records the position of Bob's fee function used // for his incoming HTLC sweeping tx. incomingFuncPosition := int32(0) // Mine the anchor sweeping tx to reduce noise in this test. ht.MineBlockWithTx(anchorSweep) // Update the fee function's positions. outgoingFuncPosition++ incomingFuncPosition++ // identifySweepTxns is a helper closure that identifies the incoming // and outgoing HTLC sweeping txns. It always assumes there are two // sweeping txns in the mempool, and returns the incoming HTLC sweep // first. identifySweepTxns := func() (*wire.MsgTx, *wire.MsgTx) { // We should see two txns in the mempool: // 1. the outgoing HTLC sweeping tx. // 2. the incoming HTLC sweeping tx. txns = ht.GetNumTxsFromMempool(2) var incoming, outgoing *wire.MsgTx // The sweeping tx has two inputs, one from wallet, the other // from the force close tx. We now check whether the first tx // spends from the force close tx of Alice->Bob. found := fn.Any(func(inp *wire.TxIn) bool { return inp.PreviousOutPoint.Hash == abCloseTxid }, txns[0].TxIn) // If the first tx spends an outpoint from the force close tx // of Alice->Bob, then it must be the incoming HTLC sweeping // tx. if found { incoming, outgoing = txns[0], txns[1] } else { // Otherwise the second tx must be the incoming HTLC // sweep. incoming, outgoing = txns[1], txns[0] } return incoming, outgoing } //nolint:lll // For neutrino backend, we need to give it more time to sync the // blocks. There's a potential bug we need to fix: // 2024-04-18 23:36:07.046 [ERR] NTFN: unable to get missed blocks: starting height 487 is greater than ending height 486 // // TODO(yy): investigate and fix it. time.Sleep(10 * time.Second) // We should see Bob's sweeping txns in the mempool. incomingSweep, outgoingSweep = identifySweepTxns() // We now mine enough blocks till we reach the end of the outgoing // HTLC's deadline. Along the way, we check the expected fee rates are // used for both incoming and outgoing HTLC sweeping txns. // // NOTE: We need to subtract 1 from the deadline as the budget must be // used up before the deadline. blocksLeft := outgoingHTLCDeadline - outgoingFuncPosition - 1 for i := int32(0); i < blocksLeft; i++ { // Mine an empty block. ht.MineEmptyBlocks(1) // Update Bob's fee function position. outgoingFuncPosition++ incomingFuncPosition++ // We should see two txns in the mempool, // - the incoming HTLC sweeping tx. // - the outgoing HTLC sweeping tx. ht.AssertNumTxsInMempool(2) // Make sure Bob's old sweeping txns have been removed from the // mempool. ht.AssertTxNotInMempool(outgoingSweep.TxHash()) ht.AssertTxNotInMempool(incomingSweep.TxHash()) // Bob should have two pending sweeps: // 1. the outgoing HTLC output on Bob->Carol. // 2. the incoming HTLC output on Alice->Bob. ht.AssertNumPendingSweeps(bob, 2) // We should see Bob's replacement txns in the mempool. incomingSweep, outgoingSweep = identifySweepTxns() // Bob's outgoing HTLC sweeping tx should be fee bumped. assertSweepFeeRate( outgoingSweep, outgoingStartFeeRate, outgoingFeeRateDelta, outgoingTxSize, outgoingHTLCDeadline, outgoingFuncPosition, "Outgoing", ) // Bob's incoming HTLC sweeping tx should be fee bumped. assertSweepFeeRate( incomingSweep, incomingStartFeeRate, incomingFeeRateDelta, incomingTxSize, incomingHTLCDeadline, incomingFuncPosition, "Incoming", ) } // Mine an empty block. ht.MineEmptyBlocks(1) // We should see Bob's old txns in the mempool. currentIncomingSweep, currentOutgoingSweep := identifySweepTxns() require.Equal(ht, incomingSweep.TxHash(), currentIncomingSweep.TxHash()) require.Equal(ht, outgoingSweep.TxHash(), currentOutgoingSweep.TxHash()) // Mine a block to confirm the HTLC sweeps. ht.MineBlocksAndAssertNumTxes(1, 2) } // testSweepCommitOutputAndAnchor checks when a channel is force closed without // any time-sensitive HTLCs, the anchor output is swept without any CPFP // attempts. In addition, the to_local output should be swept using the // specified deadline and budget. // // Setup: // 1. Fund Alice with 1 UTXOs - she only needs one for the funding process, // and no wallet utxos are needed for her sweepings. // 2. Fund Bob with no UTXOs - his sweeping txns don't need wallet utxos as he // doesn't need to sweep any time-sensitive outputs. // 3. Alice opens a channel with Bob, and successfully sends him an HTLC. // 4. Alice force closes the channel. // // Test: // 1. Alice's anchor sweeping is not attempted, instead, it should be swept // together with her to_local output using the no deadline path. // 2. Bob would also sweep his anchor and to_local outputs in a single // sweeping tx using the no deadline path. // 3. Both Alice and Bob's RBF attempts are using the fee rates calculated // from the deadline and budget. // 4. Wallet UTXOs requirements are met - neither Alice nor Bob needs wallet // utxos to finish their sweeps. func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { // Setup testing params for Alice. // // deadline is the expected deadline when sweeping the anchor and // to_local output. We will use a customized deadline to test the // config. deadline := uint32(1000) // The actual deadline used by the fee function will be one block off // from the deadline configured as we require one block to be mined to // trigger the sweep. deadlineA, deadlineB := deadline-1, deadline-1 // startFeeRate is returned by the fee estimator in sat/kw. This // will be used as the starting fee rate for the linear fee func used // by Alice. Since there are no time-sensitive HTLCs, Alice's sweeper // should start with the above default deadline, which will result in // the min relay fee rate being used since it's >= MaxBlockTarget. startFeeRate := chainfee.FeePerKwFloor // Set up the fee estimator to return the testing fee rate when the // conf target is the deadline. ht.SetFeeEstimateWithConf(startFeeRate, deadlineA) // toLocalCSV is the CSV delay for Alice's to_local output. We use a // small value to save us from mining blocks. // // NOTE: once the force close tx is confirmed, we expect anchor // sweeping starts. Then two more block later the commit output // sweeping starts. // // NOTE: The CSV value is chosen to be 3 instead of 2, to reduce the // possibility of flakes as there is a race between the two goroutines: // G1 - Alice's sweeper receives the commit output. // G2 - Alice's sweeper receives the new block mined. // G1 is triggered by the same block being received by Alice's // contractcourt, deciding the commit output is mature and offering it // to her sweeper. Normally, we'd expect G2 to be finished before G1 // because it's the same block processed by both contractcourt and // sweeper. However, if G2 is delayed (maybe the sweeper is slow in // finishing its previous round), G1 may finish before G2. This will // cause the sweeper to add the commit output to its pending inputs, // and once G2 fires, it will then start sweeping this output, // resulting a valid sweep tx being created using her commit and anchor // outputs. // // TODO(yy): fix the above issue by making sure subsystems share the // same view on current block height. toLocalCSV := 3 // htlcAmt is the amount of the HTLC in sats, this should be Alice's // to_remote amount that goes to Bob. htlcAmt := int64(100_000) // fundAmt is the funding amount. fundAmt := btcutil.Amount(1_000_000) // We now set up testing params for Bob. // // bobBalance is the push amount when Alice opens the channel with Bob. // We will use zero here so Bob's balance equals to the htlc amount by // the time Alice force closes. bobBalance := btcutil.Amount(0) // We now set up the force close scenario. Alice will open a channel // with Bob, send an HTLC, Bob settles it, and then Alice force closes // the channel without any pending HTLCs. // // Prepare node params. cfg := []string{ "--protocol.anchors", fmt.Sprintf("--sweeper.nodeadlineconftarget=%v", deadline), fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV), } openChannelParams := lntest.OpenChannelParams{ Amt: fundAmt, PushAmt: bobBalance, } // Create a two hop network: Alice -> Bob. chanPoints, nodes := createSimpleNetwork(ht, cfg, 2, openChannelParams) // Unwrap the results. chanPoint := chanPoints[0] alice, bob := nodes[0], nodes[1] invoice := &lnrpc.Invoice{ Memo: "bob", Value: htlcAmt, CltvExpiry: finalCltvDelta, } resp := bob.RPC.AddInvoice(invoice) // Send a payment with a specified finalCTLVDelta, and assert it's // succeeded. req := &routerrpc.SendPaymentRequest{ PaymentRequest: resp.PaymentRequest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } ht.SendPaymentAssertSettled(alice, req) // Assert Alice's to_remote (Bob's to_local) output is the htlc amount. ht.AssertChannelLocalBalance(bob, chanPoint, htlcAmt) bobToLocal := htlcAmt // Get Alice's channel to calculate Alice's to_local output amount. aliceChan := ht.GetChannelByChanPoint(alice, chanPoint) expectedToLocal := int64(fundAmt) - aliceChan.CommitFee - htlcAmt - 330*2 // Assert Alice's to_local output is correct. aliceToLocal := aliceChan.LocalBalance require.EqualValues(ht, expectedToLocal, aliceToLocal) // Alice force closes the channel. ht.CloseChannelAssertPending(alice, chanPoint, true) // Now that the channel has been force closed, it should show up in the // PendingChannels RPC under the waiting close section. ht.AssertChannelWaitingClose(alice, chanPoint) // Alice should see 2 anchor sweeps for the local and remote commitment. // Even without HTLCs at stake the anchors are registered with the // sweeper subsytem. ht.AssertNumPendingSweeps(alice, 2) // Bob did not force close the channel therefore he should have no // pending sweeps. ht.AssertNumPendingSweeps(bob, 0) // Mine a block to confirm Alice's force closing tx. Once it's // confirmed, we should see both Alice and Bob's anchors being offered // to their sweepers. ht.MineBlocksAndAssertNumTxes(1, 1) // Alice should have one pending sweep, // - anchor sweeping from her local commitment. ht.AssertNumPendingSweeps(alice, 1) // Bob should have two pending sweeps, // - anchor sweeping from the remote anchor on Alice's commit tx. // - commit sweeping from the to_remote on Alice's commit tx. ht.AssertNumPendingSweeps(bob, 2) // Mine one more empty block should trigger Bob's sweeping. Since we // use a CSV of 3, this means Alice's to_local output is one block away // from being mature. ht.MineEmptyBlocks(1) // We expect to see one sweeping tx in the mempool: // - Alice's anchor sweeping tx must have been failed due to the fee // rate chosen in this test - the anchor sweep tx has no output. // - Bob's sweeping tx, which sweeps both his anchor and commit outputs. bobSweepTx := ht.GetNumTxsFromMempool(1)[0] // We expect two pending sweeps for Bob - anchor and commit outputs. pendingSweepBob := ht.AssertNumPendingSweeps(bob, 2)[0] // The sweeper may be one block behind contractcourt, so we double // check the actual deadline. // // TODO(yy): assert they are equal once blocks are synced via // `blockbeat`. currentHeight := int32(ht.CurrentHeight()) actualDeadline := int32(pendingSweepBob.DeadlineHeight) - currentHeight if actualDeadline != int32(deadlineB) { ht.Logf("!!! Found unsynced block between sweeper and "+ "contractcourt, expected deadline=%v, got=%v", deadlineB, actualDeadline) deadlineB = uint32(actualDeadline) } // Alice should still have one pending sweep - the anchor output. ht.AssertNumPendingSweeps(alice, 1) // We now check Bob's sweeping tx. // // Bob's sweeping tx should have 2 inputs, one from his commit output, // the other from his anchor output. require.Len(ht, bobSweepTx.TxIn, 2) // Because Bob is sweeping without deadline pressure, the starting fee // rate should be the min relay fee rate. bobStartFeeRate := ht.CalculateTxFeeRate(bobSweepTx) require.InEpsilonf(ht, uint64(chainfee.FeePerKwFloor), uint64(bobStartFeeRate), 0.01, "want %v, got %v", chainfee.FeePerKwFloor, bobStartFeeRate) // With Bob's starting fee rate being validated, we now calculate his // ending fee rate and fee rate delta. // // Bob sweeps two inputs - anchor and commit, so the starting budget // should come from the sum of these two. bobValue := btcutil.Amount(bobToLocal + 330) bobBudget := bobValue.MulF64(contractcourt.DefaultBudgetRatio) // Calculate the ending fee rate and fee rate delta used in his fee // function. bobTxWeight := ht.CalculateTxWeight(bobSweepTx) bobEndingFeeRate := chainfee.NewSatPerKWeight(bobBudget, bobTxWeight) bobFeeRateDelta := (bobEndingFeeRate - bobStartFeeRate) / chainfee.SatPerKWeight(deadlineB-1) // Mine an empty block, which should trigger Alice's contractcourt to // offer her commit output to the sweeper. ht.MineEmptyBlocks(1) // Alice should have both anchor and commit as the pending sweep // requests. aliceSweeps := ht.AssertNumPendingSweeps(alice, 2) aliceAnchor, aliceCommit := aliceSweeps[0], aliceSweeps[1] if aliceAnchor.AmountSat > aliceCommit.AmountSat { aliceAnchor, aliceCommit = aliceCommit, aliceAnchor } // The sweeper may be one block behind contractcourt, so we double // check the actual deadline. // // TODO(yy): assert they are equal once blocks are synced via // `blockbeat`. currentHeight = int32(ht.CurrentHeight()) actualDeadline = int32(aliceCommit.DeadlineHeight) - currentHeight if actualDeadline != int32(deadlineA) { ht.Logf("!!! Found unsynced block between Alice's sweeper and "+ "contractcourt, expected deadline=%v, got=%v", deadlineA, actualDeadline) deadlineA = uint32(actualDeadline) } // We now wait for 30 seconds to overcome the flake - there's a block // race between contractcourt and sweeper, causing the sweep to be // broadcast earlier. // // TODO(yy): remove this once `blockbeat` is in place. aliceStartPosition := 0 var aliceFirstSweepTx *wire.MsgTx err := wait.NoError(func() error { mem := ht.GetRawMempool() if len(mem) != 2 { return fmt.Errorf("want 2, got %v in mempool: %v", len(mem), mem) } // If there are two txns, it means Alice's sweep tx has been // created and published. aliceStartPosition = 1 txns := ht.GetNumTxsFromMempool(2) aliceFirstSweepTx = txns[0] // Reassign if the second tx is larger. if txns[1].TxOut[0].Value > aliceFirstSweepTx.TxOut[0].Value { aliceFirstSweepTx = txns[1] } return nil }, wait.DefaultTimeout) ht.Logf("Checking mempool got: %v", err) // Mine an empty block, which should trigger Alice's sweeper to publish // her commit sweep along with her anchor output. ht.MineEmptyBlocks(1) // If Alice has already published her initial sweep tx, the above mined // block would trigger an RBF. We now need to assert the mempool has // removed the replaced tx. if aliceFirstSweepTx != nil { ht.AssertTxNotInMempool(aliceFirstSweepTx.TxHash()) } // We also remember the positions of fee functions used by Alice and // Bob. They will be used to calculate the expected fee rates later. // // Alice's sweeping tx has just been created, so she is at the starting // position. For Bob, due to the above mined blocks, his fee function // is now at position 2. alicePosition, bobPosition := uint32(aliceStartPosition), uint32(2) // We should see two txns in the mempool: // - Alice's sweeping tx, which sweeps her commit output at the // starting fee rate - Alice's anchor output won't be swept with her // commit output together because they have different deadlines. // - Bob's previous sweeping tx, which sweeps both his anchor and // commit outputs, at the starting fee rate. txns := ht.GetNumTxsFromMempool(2) // Assume the first tx is Alice's sweeping tx, if the second tx has a // larger output value, then that's Alice's as her to_local value is // much gearter. aliceSweepTx := txns[0] bobSweepTx = txns[1] // Swap them if bobSweepTx is smaller. if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value { aliceSweepTx, bobSweepTx = bobSweepTx, aliceSweepTx } // We now check Alice's sweeping tx. // // Alice's sweeping tx should have a shape of 1-in-1-out since it's not // used for CPFP, so it shouldn't take any wallet utxos. require.Len(ht, aliceSweepTx.TxIn, 1) require.Len(ht, aliceSweepTx.TxOut, 1) // We now check Alice's sweeping tx to see if it's already published. // // TODO(yy): remove this check once we have better block control. aliceSweeps = ht.AssertNumPendingSweeps(alice, 2) aliceCommit = aliceSweeps[0] if aliceCommit.AmountSat < aliceSweeps[1].AmountSat { aliceCommit = aliceSweeps[1] } if aliceCommit.BroadcastAttempts > 1 { ht.Logf("!!! Alice's commit sweep has already been broadcast, "+ "broadcast_attempts=%v", aliceCommit.BroadcastAttempts) alicePosition = aliceCommit.BroadcastAttempts } // Alice's sweeping tx should use the min relay fee rate as there's no // deadline pressure. aliceStartingFeeRate := chainfee.FeePerKwFloor // With Alice's starting fee rate being validated, we now calculate her // ending fee rate and fee rate delta. // // Alice sweeps two inputs - anchor and commit, so the starting budget // should come from the sum of these two. However, due to the value // being too large, the actual ending fee rate used should be the // sweeper's max fee rate configured. aliceTxWeight := uint64(ht.CalculateTxWeight(aliceSweepTx)) aliceEndingFeeRate := sweep.DefaultMaxFeeRate.FeePerKWeight() aliceFeeRateDelta := (aliceEndingFeeRate - aliceStartingFeeRate) / chainfee.SatPerKWeight(deadlineA-1) aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx) expectedFeeRateAlice := aliceStartingFeeRate + aliceFeeRateDelta*chainfee.SatPerKWeight(alicePosition) require.InEpsilonf(ht, uint64(expectedFeeRateAlice), uint64(aliceFeeRate), 0.02, "want %v, got %v", expectedFeeRateAlice, aliceFeeRate) // We now check Bob' sweeping tx. // // The above mined block will trigger Bob's sweeper to RBF his previous // sweeping tx, which will fail due to RBF rule#4 - the additional fees // paid are not sufficient. This happens as our default incremental // relay fee rate is 1 sat/vb, with the tx size of 771 weight units, or // 192 vbytes, we need to pay at least 192 sats more to be able to RBF. // However, since Bob's budget delta is (100_000 + 330) * 0.5 / 1008 = // 49.77 sats, it means Bob can only perform a successful RBF every 4 // blocks. // // Assert Bob's sweeping tx is not RBFed. bobFeeRate := ht.CalculateTxFeeRate(bobSweepTx) expectedFeeRateBob := bobStartFeeRate require.InEpsilonf(ht, uint64(expectedFeeRateBob), uint64(bobFeeRate), 0.01, "want %d, got %d", expectedFeeRateBob, bobFeeRate) // reloclateAlicePosition is a temp hack to find the actual fee // function position used for Alice. Due to block sync issue among the // subsystems, we can end up having this situation: // - sweeper is at block 2, starts sweeping an input with deadline 100. // - fee bumper is at block 1, and thinks the conf target is 99. // - new block 3 arrives, the func now is at position 2. // // TODO(yy): fix it using `blockbeat`. reloclateAlicePosition := func() { // Mine an empty block to trigger the possible RBF attempts. ht.MineEmptyBlocks(1) // Increase the positions for both fee functions. alicePosition++ bobPosition++ // We expect two pending sweeps for both nodes as we are mining // empty blocks. ht.AssertNumPendingSweeps(alice, 2) ht.AssertNumPendingSweeps(bob, 2) // We expect to see both Alice's and Bob's sweeping txns in the // mempool. ht.AssertNumTxsInMempool(2) // Make sure Alice's old sweeping tx has been removed from the // mempool. ht.AssertTxNotInMempool(aliceSweepTx.TxHash()) // We should see two txns in the mempool: // - Alice's sweeping tx, which sweeps both her anchor and // commit outputs, using the increased fee rate. // - Bob's previous sweeping tx, which sweeps both his anchor // and commit outputs, at the possible increased fee rate. txns = ht.GetNumTxsFromMempool(2) // Assume the first tx is Alice's sweeping tx, if the second tx // has a larger output value, then that's Alice's as her // to_local value is much gearter. aliceSweepTx = txns[0] bobSweepTx = txns[1] // Swap them if bobSweepTx is smaller. if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value { aliceSweepTx, bobSweepTx = bobSweepTx, aliceSweepTx } // Alice's sweeping tx should be increased. aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx) expectedFeeRate := aliceStartingFeeRate + aliceFeeRateDelta*chainfee.SatPerKWeight(alicePosition) ht.Logf("Alice(deadline=%v): txWeight=%v, want feerate=%v, "+ "got feerate=%v, delta=%v", deadlineA-alicePosition, aliceTxWeight, expectedFeeRate, aliceFeeRate, aliceFeeRateDelta) nextPosition := alicePosition + 1 nextFeeRate := aliceStartingFeeRate + aliceFeeRateDelta*chainfee.SatPerKWeight(nextPosition) // Calculate the distances. delta := math.Abs(float64(aliceFeeRate - expectedFeeRate)) deltaNext := math.Abs(float64(aliceFeeRate - nextFeeRate)) // Exit early if the first distance is smaller - it means we // are at the right fee func position. if delta < deltaNext { require.InEpsilonf(ht, uint64(expectedFeeRate), uint64(aliceFeeRate), 0.02, "want %v, got %v "+ "in tx=%v", expectedFeeRate, aliceFeeRate, aliceSweepTx.TxHash()) return } alicePosition++ ht.Logf("Jump position for Alice(deadline=%v): txWeight=%v, "+ "want feerate=%v, got feerate=%v, delta=%v", deadlineA-alicePosition, aliceTxWeight, nextFeeRate, aliceFeeRate, aliceFeeRateDelta) require.InEpsilonf(ht, uint64(nextFeeRate), uint64(aliceFeeRate), 0.02, "want %v, got %v in tx=%v", nextFeeRate, aliceFeeRate, aliceSweepTx.TxHash()) } reloclateAlicePosition() // We now mine 7 empty blocks. For each block mined, we'd see Alice's // sweeping tx being RBFed. For Bob, he performs a fee bump every // block, but will only publish a tx every 4 blocks mined as some of // the fee bumps is not sufficient to meet the fee requirements // enforced by RBF. Since his fee function is already at position 1, // mining 7 more blocks means he will RBF his sweeping tx twice. for i := 1; i < 7; i++ { // Mine an empty block to trigger the possible RBF attempts. ht.MineEmptyBlocks(1) // Increase the positions for both fee functions. alicePosition++ bobPosition++ // We expect two pending sweeps for both nodes as we are mining // empty blocks. ht.AssertNumPendingSweeps(alice, 2) ht.AssertNumPendingSweeps(bob, 2) // We expect to see both Alice's and Bob's sweeping txns in the // mempool. ht.AssertNumTxsInMempool(2) // Make sure Alice's old sweeping tx has been removed from the // mempool. ht.AssertTxNotInMempool(aliceSweepTx.TxHash()) // Make sure Bob's old sweeping tx has been removed from the // mempool. Since Bob's sweeping tx will only be successfully // RBFed every 4 blocks, his old sweeping tx only will be // removed when there are 4 blocks increased. if bobPosition%4 == 0 { ht.AssertTxNotInMempool(bobSweepTx.TxHash()) } // We should see two txns in the mempool: // - Alice's sweeping tx, which sweeps both her anchor and // commit outputs, using the increased fee rate. // - Bob's previous sweeping tx, which sweeps both his anchor // and commit outputs, at the possible increased fee rate. txns := ht.GetNumTxsFromMempool(2) // Assume the first tx is Alice's sweeping tx, if the second tx // has a larger output value, then that's Alice's as her // to_local value is much gearter. aliceSweepTx = txns[0] bobSweepTx = txns[1] // Swap them if bobSweepTx is smaller. if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value { aliceSweepTx, bobSweepTx = bobSweepTx, aliceSweepTx } // We now check Alice's sweeping tx. // // Alice's sweeping tx should have a shape of 1-in-1-out since // it's not used for CPFP, so it shouldn't take any wallet // utxos. require.Len(ht, aliceSweepTx.TxIn, 1) require.Len(ht, aliceSweepTx.TxOut, 1) // Alice's sweeping tx should be increased. aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx) expectedFeeRateAlice := aliceStartingFeeRate + aliceFeeRateDelta*chainfee.SatPerKWeight(alicePosition) ht.Logf("Alice(deadline=%v): txWeight=%v, want feerate=%v, "+ "got feerate=%v, delta=%v", deadlineA-alicePosition, aliceTxWeight, expectedFeeRateAlice, aliceFeeRate, aliceFeeRateDelta) require.InEpsilonf(ht, uint64(expectedFeeRateAlice), uint64(aliceFeeRate), 0.02, "want %v, got %v in tx=%v", expectedFeeRateAlice, aliceFeeRate, aliceSweepTx.TxHash()) // We now check Bob' sweeping tx. bobFeeRate := ht.CalculateTxFeeRate(bobSweepTx) // accumulatedDelta is the delta that Bob has accumulated so // far. This will only be added when there's a successful RBF // attempt. accumulatedDelta := bobFeeRateDelta * chainfee.SatPerKWeight(bobPosition) // Bob's sweeping tx will only be successfully RBFed every 4 // blocks. if bobPosition%4 == 0 { expectedFeeRateBob = bobStartFeeRate + accumulatedDelta } ht.Logf("Bob(deadline=%v): txWeight=%v, want feerate=%v, "+ "got feerate=%v, delta=%v", deadlineB-bobPosition, bobTxWeight, expectedFeeRateBob, bobFeeRate, bobFeeRateDelta) require.InEpsilonf(ht, uint64(expectedFeeRateBob), uint64(bobFeeRate), 0.02, "want %d, got %d in tx=%v", expectedFeeRateBob, bobFeeRate, bobSweepTx.TxHash()) } // Mine a block to confirm both sweeping txns, this is needed to clean // up the mempool. ht.MineBlocksAndAssertNumTxes(1, 2) } // createSimpleNetwork creates the specified number of nodes and makes a // topology of `node1 -> node2 -> node3...`. Each node is created using the // specified config, the neighbors are connected, and the channels are opened. // Each node will be funded with a single UTXO of 1 BTC except the last one. func createSimpleNetwork(ht *lntest.HarnessTest, nodeCfg []string, numNodes int, p lntest.OpenChannelParams) ([]*lnrpc.ChannelPoint, []*node.HarnessNode) { // Make a slice of nodes. nodes := make([]*node.HarnessNode, numNodes) // Create new nodes. for i := range nodes { nodeName := fmt.Sprintf("Node%q", string(rune('A'+i))) n := ht.NewNode(nodeName, nodeCfg) nodes[i] = n } // Connect the nodes in a chain. for i := 1; i < len(nodes); i++ { nodeA := nodes[i-1] nodeB := nodes[i] ht.EnsureConnected(nodeA, nodeB) } // Fund all the nodes expect the last one. for i := 0; i < len(nodes)-1; i++ { node := nodes[i] ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, node) } // Mine 1 block to get the above coins confirmed. ht.MineBlocksAndAssertNumTxes(1, numNodes-1) // Open channels in batch to save blocks mined. reqs := make([]*lntest.OpenChannelRequest, 0, len(nodes)-1) for i := 0; i < len(nodes)-1; i++ { nodeA := nodes[i] nodeB := nodes[i+1] req := &lntest.OpenChannelRequest{ Local: nodeA, Remote: nodeB, Param: p, } reqs = append(reqs, req) } resp := ht.OpenMultiChannelsAsync(reqs) // Make sure the nodes know each other's channels if they are public. if !p.Private { for _, node := range nodes { for _, chanPoint := range resp { ht.AssertTopologyChannelOpen(node, chanPoint) } } } 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.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) // 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.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.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.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.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) } // 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`. // // NOTE: This test does not check for a specific fee rate because channel force // closures should be bumped taking a budget into account not a specific // fee rate. func testBumpForceCloseFee(ht *lntest.HarnessTest) { // Skip this test for neutrino, as it's not aware of mempool // transactions. if ht.IsNeutrinoBackend() { ht.Skipf("skipping BumpForceCloseFee test for neutrino backend") } // fundAmt is the funding amount. fundAmt := btcutil.Amount(1_000_000) // We add a push amount because otherwise no anchor for the counter // party will be created which influences the commitment fee // calculation. pushAmt := btcutil.Amount(50_000) openChannelParams := lntest.OpenChannelParams{ Amt: fundAmt, PushAmt: pushAmt, } // Bumping the close fee rate is only possible for anchor channels. cfg := []string{ "--protocol.anchors", } // Create a two hop network: Alice -> Bob. chanPoints, nodes := createSimpleNetwork(ht, cfg, 2, openChannelParams) // Unwrap the results. chanPoint := chanPoints[0] alice := nodes[0] // We need to fund alice with 2 wallet inputs so that we can test to // increase the fee rate of the anchor cpfp via two subsequent calls of // the`BumpForceCloseFee` rpc cmd. // // TODO (ziggie): Make sure we use enough wallet inputs so that both // anchor transactions (local, remote commitment tx) can be created and // broadcasted. Not sure if we really need this, because we can be sure // as soon as one anchor transactions makes it into the mempool that the // others will fail anyways? ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, alice) // Alice force closes the channel which has no HTLCs at stake. _, closingTxID := ht.CloseChannelAssertPending(alice, chanPoint, true) require.NotNil(ht, closingTxID) // Alice should see one waiting close channel. ht.AssertNumWaitingClose(alice, 1) // Alice should have 2 registered sweep inputs. The anchor of the local // commitment tx and the anchor of the remote commitment tx. ht.AssertNumPendingSweeps(alice, 2) // Calculate the commitment tx fee rate. closingTx := ht.AssertTxInMempool(closingTxID) require.NotNil(ht, closingTx) // The default commitment fee for anchor channels is capped at 2500 // sat/kw but there might be some inaccuracies because of the witness // signature length therefore we calculate the exact value here. closingFeeRate := ht.CalculateTxFeeRate(closingTx) // We increase the fee rate of the fee function by 100% to make sure // we trigger a cpfp-transaction. newFeeRate := closingFeeRate * 2 // We need to make sure that the budget can cover the fees for bumping. // However we also want to make sure that the budget is not too large // so that the delta of the fee function does not increase the feerate // by a single sat hence NOT rbfing the anchor sweep every time a new // block is found and a new sweep broadcast is triggered. // // NOTE: // We expect an anchor sweep with 2 inputs (anchor input + a wallet // input) and 1 p2tr output. This transaction has a weight of approx. // 725 wu. This info helps us to calculate the delta of the fee // function. // EndFeeRate: 100_000 sats/725 wu * 1000 = 137931 sat/kw // StartingFeeRate: 5000 sat/kw // delta = (137931-5000)/1008 = 132 sat/kw (which is lower than // 250 sat/kw) => hence we are violating BIP 125 Rule 4, which is // exactly what we want here to test the subsequent calling of the // bumpclosefee rpc. cpfpBudget := 100_000 bumpFeeReq := &walletrpc.BumpForceCloseFeeRequest{ ChanPoint: chanPoint, StartingFeerate: uint64(newFeeRate.FeePerVByte()), Budget: uint64(cpfpBudget), // We use a force param to create the sweeping tx immediately. Immediate: true, } alice.RPC.BumpForceCloseFee(bumpFeeReq) // We expect the initial closing transaction and the local anchor cpfp // transaction because alice force closed the channel. // // NOTE: We don't compare a feerate but only make sure that a cpfp // transaction was triggered. The sweeper increases the fee rate // periodically with every new incoming block and the selected fee // function. ht.AssertNumTxsInMempool(2) // Identify the cpfp anchor sweep. txns := ht.GetNumTxsFromMempool(2) cpfpSweep1 := ht.FindSweepingTxns(txns, 1, closingTx.TxHash())[0] // Mine an empty block and make sure the anchor cpfp is still in the // mempool hence the new block did not let the sweeper subsystem rbf // this anchor sweep transaction (because of the small fee delta). ht.MineEmptyBlocks(1) cpfpHash1 := cpfpSweep1.TxHash() ht.AssertTxInMempool(cpfpHash1) // Now Bump the fee rate again with a bigger starting fee rate of the // fee function. newFeeRate = closingFeeRate * 3 bumpFeeReq = &walletrpc.BumpForceCloseFeeRequest{ ChanPoint: chanPoint, StartingFeerate: uint64(newFeeRate.FeePerVByte()), // The budget needs to be high enough to pay for the fee because // the anchor does not have an output value high enough to pay // for itself. Budget: uint64(cpfpBudget), // We use a force param to create the sweeping tx immediately. Immediate: true, } alice.RPC.BumpForceCloseFee(bumpFeeReq) // Make sure the old sweep is not in the mempool anymore, which proofs // that a new cpfp transaction replaced the old one paying higher fees. ht.AssertTxNotInMempool(cpfpHash1) // Identify the new cpfp transaction. // Both anchor sweeps result from the same closing tx (the local // commitment) hence proofing that the remote commitment transaction // and its cpfp transaction is invalid and not accepted into the // mempool. txns = ht.GetNumTxsFromMempool(2) ht.FindSweepingTxns(txns, 1, closingTx.TxHash()) // Mine both transactions, the closing tx and the anchor cpfp tx. // This is needed to clean up the mempool. ht.MineBlocksAndAssertNumTxes(1, 2) }