itest: fix testSweepCommitOutputAndAnchor

This commit is contained in:
yyforyongyu
2024-10-24 19:52:55 +08:00
parent d260a87f3b
commit 1aeea8a90f

View File

@@ -2,7 +2,6 @@ package itest
import (
"fmt"
"math"
"time"
"github.com/btcsuite/btcd/btcutil"
@@ -1217,10 +1216,20 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// 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
// deadlineA is the deadline used for Alice, since her commit output is
// offered to the sweeper at CSV-1. With a deadline of 1000, her actual
// width of her fee func is CSV+1000-1. Given we are using a CSV of 2
// here, her fee func deadline then becomes 1001.
deadlineA := deadline + 1
// deadlineB is the deadline used for Bob, 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. In
// addition, when sweeping his to_local output from Alice's commit tx,
// because of CSV of 2, the starting height will be
// "force_close_height+2", which means when the sweep request is
// received by the sweeper, the actual deadline delta is "deadline+1".
deadlineB := 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
@@ -1231,7 +1240,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// Set up the fee estimator to return the testing fee rate when the
// conf target is the deadline.
ht.SetFeeEstimateWithConf(startFeeRate, deadlineA)
ht.SetFeeEstimateWithConf(startFeeRate, deadlineB)
// toLocalCSV is the CSV delay for Alice's to_local output. We use a
// small value to save us from mining blocks.
@@ -1239,25 +1248,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// 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
toLocalCSV := 2
// htlcAmt is the amount of the HTLC in sats, this should be Alice's
// to_remote amount that goes to Bob.
@@ -1356,155 +1347,41 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// - 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)
// Bob's sweeper should have broadcast the commit output sweeping tx.
// At the block which mined the force close tx, Bob's `chainWatcher`
// will process the blockbeat first, which sends a signal to his
// `ChainArbitrator` to launch the resolvers. Once launched, the sweep
// requests will be sent to the sweeper. Finally, when the sweeper
// receives this blockbeat, it will create the sweeping tx and publish
// it.
ht.AssertNumTxsInMempool(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]
// Mine one more empty block should trigger Bob's sweeping. Since we
// use a CSV of 2, this means Alice's to_local output is now mature.
ht.MineEmptyBlocks(1)
// We expect two pending sweeps for Bob - anchor and commit outputs.
pendingSweepBob := ht.AssertNumPendingSweeps(bob, 2)[0]
ht.AssertNumPendingSweeps(bob, 2)
// 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 expect two pending sweeps for Alice - anchor and commit outputs.
ht.AssertNumPendingSweeps(alice, 2)
// 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)
alicePosition, bobPosition := uint32(0), uint32(1)
// 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.
// - Bob's previous sweeping tx, which sweeps his 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]
aliceSweepTx, bobSweepTx := txns[0], txns[1]
// Swap them if bobSweepTx is smaller.
if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value {
@@ -1518,20 +1395,6 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
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
@@ -1546,7 +1409,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
aliceTxWeight := uint64(ht.CalculateTxWeight(aliceSweepTx))
aliceEndingFeeRate := sweep.DefaultMaxFeeRate.FeePerKWeight()
aliceFeeRateDelta := (aliceEndingFeeRate - aliceStartingFeeRate) /
chainfee.SatPerKWeight(deadlineA-1)
chainfee.SatPerKWeight(deadlineA)
aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx)
expectedFeeRateAlice := aliceStartingFeeRate +
@@ -1555,119 +1418,41 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
uint64(aliceFeeRate), 0.02, "want %v, got %v",
expectedFeeRateAlice, aliceFeeRate)
// We now check Bob' sweeping tx.
// We now check Bob's 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.
// Bob's sweeping tx should have one input, which is his commit output.
// His anchor output won't be swept due to it being uneconomical.
require.Len(ht, bobSweepTx.TxIn, 1, "tx=%v", bobSweepTx.TxHash())
// 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.
//
// Assert Bob's sweeping tx is not RBFed.
bobFeeRate := ht.CalculateTxFeeRate(bobSweepTx)
// Bob sweeps one input - the commit output.
bobValue := btcutil.Amount(bobToLocal)
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)
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
// We now mine 8 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
// block, but will only publish a tx every 3 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++ {
for i := 1; i < 9; i++ {
// Mine an empty block to trigger the possible RBF attempts.
ht.MineEmptyBlocks(1)
@@ -1690,9 +1475,9 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// 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 {
// RBFed every 3 blocks, his old sweeping tx only will be
// removed when there are 3 blocks increased.
if bobPosition%3 == 0 {
ht.AssertTxNotInMempool(bobSweepTx.TxHash())
}
@@ -1728,9 +1513,10 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
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)
"got feerate=%v, delta=%v in tx %v",
deadlineA-alicePosition, aliceTxWeight,
expectedFeeRateAlice, aliceFeeRate,
aliceFeeRateDelta, aliceSweepTx.TxHash())
require.InEpsilonf(ht, uint64(expectedFeeRateAlice),
uint64(aliceFeeRate), 0.02, "want %v, got %v in tx=%v",
@@ -1746,16 +1532,17 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
accumulatedDelta := bobFeeRateDelta *
chainfee.SatPerKWeight(bobPosition)
// Bob's sweeping tx will only be successfully RBFed every 4
// Bob's sweeping tx will only be successfully RBFed every 3
// blocks.
if bobPosition%4 == 0 {
if bobPosition%3 == 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)
"got feerate=%v, delta=%v in tx %v",
deadlineB-bobPosition, bobTxWeight,
expectedFeeRateBob, bobFeeRate,
bobFeeRateDelta, bobSweepTx.TxHash())
require.InEpsilonf(ht, uint64(expectedFeeRateBob),
uint64(bobFeeRate), 0.02, "want %d, got %d in tx=%v",