contractcourt: add unit tests for rbf re-org cases

This set of new tests ensures that if have created N RBF variants of the
coop close transaction, that any of then can confirm, and be re-org'd,
with us detecting the final spend once it confirms deeploy enough.
This commit is contained in:
Olaoluwa Osuntokun committed 2026-01-15 16:22:26 -08:00
1 parent e0f7b0a033
commit b6b4cefd08
1 file changed
+198
@@ -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)
}