itest: assert sweep outpoint mined after RBF
What changed, and why it matters
This commit only changes integration test code for the LND Lightning node. It makes a test more robust by checking that a specific coin (an 'outpoint') was spent in a mined block, rather than checking the exact transaction ID seen earlier. This handles cases where a transaction gets replaced via RBF (Replace-By-Fee) between the mempool check and block mining. There is no change to production code, no security fix, and no vulnerability.
No security action needed. This is a test-only robustness improvement. Reviewers may optionally verify that the new helper correctly handles coinbase transactions and RBF-sensitive flows in integration tests.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit modifies itest/lnd_sweep_test.go and lntest/harness_miner.go. It adds a new test helper MineBlockAndAssertOutpointSpent and assertOutpointSpentInBlock, and updates testSweepCPFPAnchorIncomingTimeout to use them. The goal is to avoid test flakiness when an HTLC sweep transaction is RBF-replaced after the mempool assertion but before the block is mined. The previous assertion compared pre-mining txids to confirmed txids, which could fail under RBF. The new assertion checks that the expected outpoint is consumed by some transaction in the block. No production logic is altered.
Changed components
itest/lnd_sweep_test.golntest/harness_miner.goInspect captured patch +74 / −2
diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go
index 3874786..2b9c32e 100644
--- a/itest/lnd_sweep_test.go
+++ b/itest/lnd_sweep_test.go
@@ -689,8 +689,20 @@ func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) {
// 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)
- ht.MineBlocksAndAssertNumTxes(1, 1)
+ htlcSweep := ht.AssertNumPendingSweeps(bob, 1)[0]
+ htlcSweepOutpointHash, err := chainhash.NewHashFromStr(
+ htlcSweep.Outpoint.TxidStr,
+ )
+ require.NoError(ht, err)
+ htlcSweepOutpoint := wire.OutPoint{
+ Hash: *htlcSweepOutpointHash,
+ Index: htlcSweep.Outpoint.OutputIndex,
+ }
+
+ // The final sweep may be RBFed between the mempool check and block
+ // generation, so assert that the mined tx spends the pending sweep's
+ // outpoint instead of asserting the txid observed before mining.
+ ht.MineBlockAndAssertOutpointSpent(1, htlcSweepOutpoint)
// Finally, clean the mempool for the next test.
ht.CleanShutDown()
diff --git a/lntest/harness_miner.go b/lntest/harness_miner.go
index bc24b5e..0445f07 100644
--- a/lntest/harness_miner.go
+++ b/lntest/harness_miner.go
@@ -121,6 +121,66 @@ func (h *HarnessTest) MineBlocksAndAssertNumTxes(num uint32,
return blocks
}
+// MineBlockAndAssertOutpointSpent mines a block and asserts the given outpoint
+// was spent in it. Unlike MineBlocksAndAssertNumTxes, it does not require the
+// txids seen in the mempool before mining to be the txids that confirm. This is
+// useful for RBF-sensitive flows where a transaction may be replaced between
+// the mempool check and block generation.
+func (h *HarnessTest) MineBlockAndAssertOutpointSpent(numTxs int,
+ outpoint wire.OutPoint) *wire.MsgBlock {
+
+ // Update the harness's current height.
+ defer h.updateCurrentHeight()
+
+ // If we expect transactions to be included in the blocks we'll mine,
+ // wait until they are seen in the miner's mempool.
+ h.AssertNumTxsInMempool(numTxs)
+
+ // Mine a block.
+ block := h.miner.MineBlocks(1)[0]
+
+ // Assert that the expected number of non-coinbase transactions were
+ // included in the block.
+ require.Len(h, block.Transactions, numTxs+1)
+
+ // Assert that the expected outpoint was spent in the block.
+ h.assertOutpointSpentInBlock(block, outpoint)
+
+ // Finally, make sure all the active nodes are synced.
+ h.AssertActiveNodesSyncedTo(block.BlockHash())
+
+ return block
+}
+
+// assertOutpointSpentInBlock asserts that the given outpoint is spent by a
+// transaction in the passed block.
+func (h *HarnessTest) assertOutpointSpentInBlock(block *wire.MsgBlock,
+ outpoint wire.OutPoint) {
+
+ var txids []chainhash.Hash
+ var prevouts []wire.OutPoint
+
+ for _, tx := range block.Transactions {
+ if blockchain.IsCoinBaseTx(tx) {
+ continue
+ }
+
+ txids = append(txids, tx.TxHash())
+
+ for _, txIn := range tx.TxIn {
+ prevouts = append(prevouts, txIn.PreviousOutPoint)
+
+ if txIn.PreviousOutPoint == outpoint {
+ return
+ }
+ }
+ }
+
+ require.Failf(h, "outpoint was not spent in block",
+ "outpoint:%v, block:%v, txids:%v, prevouts:%v",
+ outpoint, block.BlockHash(), txids, prevouts)
+}
+
// ConnectMiner connects the miner with the chain backend in the network.
func (h *HarnessTest) ConnectMiner() {
err := h.manager.chainBackend.ConnectMiner()
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.