lnd/itest/lnd_sweep_test.go
ziggie 2d04813dc3
itest: Add itest for bumpclosefeerate rpc.
Add an itest which will bump the close fee rate of an anchor
channel which is force closed without having any HTLCs at stake.
2024-08-09 19:41:27 +02:00

2311 lines
83 KiB
Go

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)
}