mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-28 17:53:30 +02:00
itest+sweep: add itest testSweepCommitOutputAndAnchor
This commit adds a test case to check the no deadline sweeping behavior. The sweeper is updated to make sure it can handle the case for neutrino backend.
This commit is contained in:
parent
f5a321d0d3
commit
96883f307c
@ -590,4 +590,8 @@ var allTestCases = []*lntest.TestCase{
|
|||||||
Name: "sweep htlcs",
|
Name: "sweep htlcs",
|
||||||
TestFunc: testSweepHTLCs,
|
TestFunc: testSweepHTLCs,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "sweep commit output and anchor",
|
||||||
|
TestFunc: testSweepCommitOutputAndAnchor,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package itest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
@ -800,6 +801,580 @@ func testSweepHTLCs(ht *lntest.HarnessTest) {
|
|||||||
ht.MineBlocksAndAssertNumTxes(1, 2)
|
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)
|
||||||
|
|
||||||
|
// We should see neither Alice or Bob has any pending sweeps as there
|
||||||
|
// are no time-sensitive HTLCs.
|
||||||
|
ht.AssertNumPendingSweeps(alice, 0)
|
||||||
|
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.Miner.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 := ht.Miner.GetBestBlock()
|
||||||
|
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 := uint64(ht.CalculateTxWeight(bobSweepTx))
|
||||||
|
bobEndingFeeRate := chainfee.NewSatPerKWeight(bobBudget, bobTxWeight)
|
||||||
|
bobFeeRateDelta := (bobEndingFeeRate - bobStartFeeRate) /
|
||||||
|
chainfee.SatPerKWeight(deadlineB)
|
||||||
|
|
||||||
|
// 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 = ht.Miner.GetBestBlock()
|
||||||
|
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.Miner.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.Miner.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.Miner.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.Miner.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)
|
||||||
|
|
||||||
|
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.Miner.AssertNumTxsInMempool(2)
|
||||||
|
|
||||||
|
// Make sure Alice's old sweeping tx has been removed from the
|
||||||
|
// mempool.
|
||||||
|
ht.Miner.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.Miner.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.Miner.AssertNumTxsInMempool(2)
|
||||||
|
|
||||||
|
// Make sure Alice's old sweeping tx has been removed from the
|
||||||
|
// mempool.
|
||||||
|
ht.Miner.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.Miner.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.Miner.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
|
// createSimpleNetwork creates the specified number of nodes and makes a
|
||||||
// topology of `node1 -> node2 -> node3...`. Each node is created using the
|
// topology of `node1 -> node2 -> node3...`. Each node is created using the
|
||||||
// specified config, the neighbors are connected, and the channels are opened.
|
// specified config, the neighbors are connected, and the channels are opened.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user