contractcourt: add unit tests for rbf re-org cases
What changed, and why it matters
This commit only adds new automated unit tests for how LND's chain watcher behaves when a cooperative channel-close transaction is affected by a Bitcoin blockchain reorganization (re-org). It does not change any production code, so by itself it cannot introduce or fix a live security vulnerability. The tests verify that if one version of a cooperative close transaction is reorganized out, the watcher can detect an alternative version and confirm the correct final spend.
No immediate security action is required. Treat this as a test-coverage change. If reviewing a related patch series, ensure the production code that these tests exercise (chain watcher cooperative-close handling) is also reviewed for correctness, because tests alone do not prove the implementation is safe.
Security signals we found
Tests exercise reorg handling for RBF variants of cooperative close transactions
Tests check that the chain watcher re-registers for spend notifications after a reorg
Tests verify scaled confirmation requirements survive reorgs
Tests stress rapid reorg sequences to detect state-machine breakage
Evidence from the diff
The diff creates contractcourt/chain_watcher_coop_reorg_test.go with 198 lines of Go test code. It adds seven test cases covering: reorg to an alternative RBF coop-close tx, reorg where the same tx re-confirms, multiple consecutive reorgs, reorg with no alternative tx, scaled confirmation requirements with reorg, and rapid successive reorgs. The tests rely on harness helpers (newChainWatcherTestHarness, createCoopCloseTx, runCoopCloseFlow, triggerReorg, etc.) that are not shown in this diff. No production logic is modified.
Changed components
contractcourt/chain_watcher_coop_reorg_test.goInspect captured patch +198 / −0
diff --git a/contractcourt/chain_watcher_coop_reorg_test.go b/contractcourt/chain_watcher_coop_reorg_test.go
new file mode 100644
index 0000000..0e0a552
--- /dev/null
+++ b/contractcourt/chain_watcher_coop_reorg_test.go
@@ -0,0 +1,198 @@
+package contractcourt
+
+import (
+ "testing"
+ "time"
+
+ "github.com/btcsuite/btcd/wire"
+)
+
+// TestChainWatcherCoopCloseReorg tests that the chain watcher properly handles
+// a reorganization during cooperative close confirmation waiting. When a
+// cooperative close transaction is reorganized out, the chain watcher should
+// re-register for spend notifications and detect an alternative transaction.
+func TestChainWatcherCoopCloseReorg(t *testing.T) {
+ t.Parallel()
+
+ // Create test harness.
+ harness := newChainWatcherTestHarness(t)
+
+ // Create two cooperative close transactions with different fees.
+ tx1 := harness.createCoopCloseTx(5000)
+ tx2 := harness.createCoopCloseTx(4900)
+
+ // Run cooperative close flow with reorg.
+ closeInfo := harness.runCoopCloseFlow(tx1, true, 2, tx2)
+
+ // Assert that the second transaction was confirmed.
+ harness.assertCoopCloseTx(closeInfo, tx2)
+}
+
+// TestChainWatcherCoopCloseSameTransactionAfterReorg tests that if the same
+// transaction re-confirms after a reorganization, it is properly handled.
+func TestChainWatcherCoopCloseSameTransactionAfterReorg(t *testing.T) {
+ t.Parallel()
+
+ harness := newChainWatcherTestHarness(t)
+
+ // Create a single cooperative close transaction.
+ tx := harness.createCoopCloseTx(5000)
+
+ // Run flow with the same tx confirming after the reorg.
+ closeInfo := harness.runCoopCloseFlow(tx, true, 2, tx)
+
+ harness.assertCoopCloseTx(closeInfo, tx)
+}
+
+// TestChainWatcherCoopCloseMultipleReorgs tests handling of multiple
+// consecutive reorganizations during cooperative close confirmation.
+func TestChainWatcherCoopCloseMultipleReorgs(t *testing.T) {
+ t.Parallel()
+
+ // Create test harness.
+ harness := newChainWatcherTestHarness(t)
+
+ // Create multiple cooperative close transactions with different fees.
+ txs := []*wire.MsgTx{
+ harness.createCoopCloseTx(5000),
+ harness.createCoopCloseTx(4950),
+ harness.createCoopCloseTx(4900),
+ harness.createCoopCloseTx(4850),
+ }
+
+ // Define reorg depths for each transition.
+ reorgDepths := []int32{1, 2, 3}
+
+ // Run multiple reorg flow.
+ closeInfo := harness.runMultipleReorgFlow(txs, reorgDepths)
+
+ // Assert that the final transaction was confirmed.
+ harness.assertCoopCloseTx(closeInfo, txs[3])
+}
+
+// TestChainWatcherCoopCloseReorgNoAlternative tests that if a cooperative
+// close is reorganized out and no alternative transaction appears, the
+// chain watcher continues waiting.
+func TestChainWatcherCoopCloseReorgNoAlternative(t *testing.T) {
+ t.Parallel()
+
+ // Create test harness.
+ harness := newChainWatcherTestHarness(t)
+
+ // Create a cooperative close transaction.
+ tx := harness.createCoopCloseTx(5000)
+
+ // Send spend and wait for confirmation registration.
+ harness.sendSpend(tx)
+ harness.waitForConfRegistration()
+
+ // Trigger reorg after some confirmations.
+ harness.mineBlocks(2)
+ harness.triggerReorg(tx, 2)
+
+ // Assert no cooperative close event is received.
+ harness.assertNoCoopClose(2 * time.Second)
+
+ // Now send a new transaction after the timeout.
+ harness.waitForSpendRegistration()
+ newTx := harness.createCoopCloseTx(4900)
+ harness.sendSpend(newTx)
+ harness.waitForConfRegistration()
+ harness.mineBlocks(1)
+ harness.confirmTx(newTx, harness.currentHeight)
+
+ // Should receive cooperative close for the new transaction.
+ closeInfo := harness.waitForCoopClose(5 * time.Second)
+ harness.assertCoopCloseTx(closeInfo, newTx)
+}
+
+// TestChainWatcherCoopCloseScaledConfirmationsWithReorg tests that scaled
+// confirmations (based on channel capacity) work correctly with reorgs.
+func TestChainWatcherCoopCloseScaledConfirmationsWithReorg(t *testing.T) {
+ t.Parallel()
+
+ // Test with different confirmation requirements and reorg depths.
+ // Note: We start at 3 confirmations because 1-conf uses the fast path
+ // which bypasses reorg protection (it dispatches immediately).
+ testCases := []struct {
+ name string
+ requiredConfs uint32
+ reorgDepth int32
+ }{
+ {
+ name: "triple_conf",
+ requiredConfs: 3,
+ reorgDepth: 2,
+ },
+ {
+ name: "six_conf",
+ requiredConfs: 6,
+ reorgDepth: 4,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // Create harness with specific confirmation
+ // requirements.
+ harness := newChainWatcherTestHarness(
+ t, withRequiredConfs(tc.requiredConfs),
+ )
+
+ // Create transactions.
+ tx1 := harness.createCoopCloseTx(5000)
+ tx2 := harness.createCoopCloseTx(4900)
+
+ // Run with reorg at different depths based on capacity.
+ closeInfo := harness.runCoopCloseFlow(
+ tx1, true, tc.reorgDepth, tx2,
+ )
+
+ // Verify correct transaction confirmed.
+ harness.assertCoopCloseTx(closeInfo, tx2)
+ })
+ }
+}
+
+// TestChainWatcherCoopCloseRapidReorgs tests that the chain watcher handles
+// multiple rapid reorgs in succession without getting into a broken state.
+func TestChainWatcherCoopCloseRapidReorgs(t *testing.T) {
+ t.Parallel()
+
+ // Create test harness.
+ harness := newChainWatcherTestHarness(t)
+
+ // Create a cooperative close transaction.
+ tx := harness.createCoopCloseTx(5000)
+
+ // Send spend notification.
+ harness.sendSpend(tx)
+
+ // Trigger multiple rapid reorgs to stress the state machine.
+ for i := 0; i < 5; i++ {
+ harness.waitForConfRegistration()
+ harness.mineBlocks(1)
+ harness.triggerReorg(tx, int32(i+1))
+ if i < 4 {
+ // Re-register for spend after each reorg except the
+ // last.
+ harness.waitForSpendRegistration()
+ harness.sendSpend(tx)
+ }
+ }
+
+ // After stress, send a clean transaction.
+ harness.waitForSpendRegistration()
+ cleanTx := harness.createCoopCloseTx(4800)
+ harness.sendSpend(cleanTx)
+ harness.waitForConfRegistration()
+ harness.mineBlocks(1)
+ harness.confirmTx(cleanTx, harness.currentHeight)
+
+ // Should still receive the cooperative close.
+ closeInfo := harness.waitForCoopClose(10 * time.Second)
+ harness.assertCoopCloseTx(closeInfo, cleanTx)
+}
Why this scored 12/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.