integration: add reorg regression test for fetchHeaderBlocks fork point
What changed, and why it matters
This commit only adds a new regression test file. It does not change any production code, so it cannot by itself introduce a security vulnerability or fix one. The test documents a previously fixed bug in block reorganization handling and verifies the fix remains effective.
No security action required; treat as normal test coverage addition. If reviewing the referenced historical bug, ensure the actual fix in fetchHeaderBlocks is present in the branch being evaluated.
Security signals we found
No production code changes
New regression test only
Historical bug referenced in test comment is not patched in this commit
Evidence from the diff
The diff creates integration/reorg_test.go, an integration test named TestReorgFromForkPoint. It spins up two btcd nodes on SimNet, makes them mine a shared chain, disconnects them, mines divergent chains of different lengths, reconnects them, and asserts the shorter node reorgs onto the longer chain and records the orphaned fork as a valid side chain via getchaintips. The commit message describes the historical bug in fetchHeaderBlocks (starting from bestState.Height+1 instead of the fork point), but no source code for that function is modified here.
Changed components
integration/reorg_test.goInspect captured patch +138 / −0
diff --git a/integration/reorg_test.go b/integration/reorg_test.go
new file mode 100644
index 0000000..e9463d4
--- /dev/null
+++ b/integration/reorg_test.go
@@ -0,0 +1,138 @@
+package integration
+
+import (
+ "testing"
+ "time"
+
+ "github.com/btcsuite/btcd/btcjson"
+ "github.com/btcsuite/btcd/chaincfg"
+ "github.com/btcsuite/btcd/integration/rpctest"
+ "github.com/btcsuite/btcd/rpcclient"
+ "github.com/stretchr/testify/require"
+)
+
+// TestReorgFromForkPoint tests that when two nodes with a shared chain history
+// diverge and reconnect, the shorter node correctly downloads blocks starting
+// from the fork point. This is a regression test for a bug where
+// fetchHeaderBlocks started from bestState.Height + 1 instead of the fork
+// point between the best chain and the best header chain, causing blocks
+// between the fork point and the current height on the new chain to never be
+// downloaded.
+func TestReorgFromForkPoint(t *testing.T) {
+ t.Parallel()
+
+ const (
+ sharedBlocks = 10
+ shorterBlocks = 5
+ longerBlocks = 20
+ )
+ var (
+ sharedHeight = int32(sharedBlocks)
+ shorterHeight = sharedHeight + int32(shorterBlocks)
+ longerHeight = sharedHeight + int32(longerBlocks)
+ forkBranchLen = int32(shorterBlocks)
+ )
+
+ longer, err := rpctest.New(&chaincfg.SimNetParams, nil, []string{}, "")
+ require.NoError(t, err)
+ require.NoError(t, longer.SetUp(false, 0))
+ t.Cleanup(func() { require.NoError(t, longer.TearDown()) })
+
+ shorter, err := rpctest.New(&chaincfg.SimNetParams, nil, []string{}, "")
+ require.NoError(t, err)
+ require.NoError(t, shorter.SetUp(false, 0))
+ t.Cleanup(func() { require.NoError(t, shorter.TearDown()) })
+
+ // Mine the shared chain on the longer node before connecting so that
+ // it is "current" and can serve headers/blocks to the shorter node.
+ _, err = longer.Client.Generate(sharedBlocks)
+ require.NoError(t, err)
+
+ // Connect and let the shorter node sync the shared chain.
+ require.NoError(t, rpctest.ConnectNode(shorter, longer))
+ require.NoError(t, rpctest.JoinNodes(
+ []*rpctest.Harness{longer, shorter}, rpctest.Blocks,
+ ))
+
+ // Disconnect so they can mine independently.
+ require.NoError(t, shorter.Client.AddNode(
+ longer.P2PAddress(), rpcclient.ANRemove,
+ ))
+
+ // Wait for both sides to see the disconnect.
+ require.Eventually(t,
+ func() bool {
+ p1, err1 := longer.Client.GetPeerInfo()
+ p2, err2 := shorter.Client.GetPeerInfo()
+
+ return err1 == nil && err2 == nil &&
+ len(p1) == 0 && len(p2) == 0
+ },
+ 5*time.Second, 100*time.Millisecond,
+ )
+
+ // Mine divergent chains. Both chains diverge at the shared block at
+ // the shared height.
+ //
+ // Mine the shorter chain.
+ _, err = shorter.Client.Generate(shorterBlocks)
+ require.NoError(t, err)
+ _, gotShorterHeight, err := shorter.Client.GetBestBlock()
+ require.NoError(t, err)
+ require.Equal(t, shorterHeight, gotShorterHeight)
+
+ // Now the longer chain.
+ _, err = longer.Client.Generate(longerBlocks)
+ require.NoError(t, err)
+ _, gotLongerHeight, err := longer.Client.GetBestBlock()
+ require.NoError(t, err)
+ require.Equal(t, longerHeight, gotLongerHeight)
+
+ // Verify the chains actually diverged at the first block after the
+ // fork point.
+ forkDivergence := int64(sharedHeight + 1)
+ longerHashAtFork, err := longer.Client.GetBlockHash(forkDivergence)
+ require.NoError(t, err)
+ shorterHashAtFork, err := shorter.Client.GetBlockHash(forkDivergence)
+ require.NoError(t, err)
+ require.NotEqual(t, longerHashAtFork, shorterHashAtFork,
+ "chains should have diverged at height %d", forkDivergence)
+
+ // Reconnect. The shorter node should reorg to the longer chain.
+ // This requires fetchHeaderBlocks to download blocks from the fork
+ // point rather than from the shorter node's height, since blocks
+ // after the fork point on the longer chain differ from the shorter's.
+ require.NoError(t, rpctest.ConnectNode(shorter, longer))
+ require.NoError(t, rpctest.JoinNodes(
+ []*rpctest.Harness{longer, shorter}, rpctest.Blocks,
+ ))
+
+ // Both nodes should be on the same chain at the longer height.
+ longerBestHash, gotLongerHeight, err := longer.Client.GetBestBlock()
+ require.NoError(t, err)
+ shorterBestHash, gotShorterHeight, err := shorter.Client.GetBestBlock()
+ require.NoError(t, err)
+ require.Equal(t, longerHeight, gotLongerHeight)
+ require.Equal(t, longerHeight, gotShorterHeight)
+ require.Equal(t, longerBestHash, shorterBestHash)
+
+ // Verify the reorged node has the orphaned fork as a side chain.
+ tips, err := shorter.Client.GetChainTips()
+ require.NoError(t, err)
+ require.Equal(t, 2, len(tips))
+
+ var activeTip, forkTip *btcjson.GetChainTipsResult
+ for _, tip := range tips {
+ switch tip.Status {
+ case "active":
+ activeTip = tip
+ case "valid-fork":
+ forkTip = tip
+ }
+ }
+ require.NotNil(t, activeTip)
+ require.Equal(t, longerHeight, activeTip.Height)
+ require.NotNil(t, forkTip)
+ require.Equal(t, shorterHeight, forkTip.Height)
+ require.Equal(t, forkBranchLen, forkTip.BranchLen)
+}
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.