blockchain: reuse existing header node in maybeAcceptBlock

maybeAcceptBlock unconditionally created a new blockNode, overwriting the
index entry.  If maybeAcceptBlockHeader had already processed the header,
the pointer held by bestHeader's chainView became orphaned, breaking
bestHeader.Contains and downstream checks like IsValidHeader.

Check for an existing node first and upgrade its status to
statusDataStored rather than replacing it.
This commit is contained in:
Calvin Kim 2026-02-24 17:59:28 +09:00
parent c1a46122ca
commit f9645f07b5
2 changed files with 89 additions and 5 deletions

View file

@ -64,11 +64,20 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags)
// Create a new block node for the block and add it to the node index. Even
// if the block ultimately gets connected to the main chain, it starts out
// on a side chain.
blockHeader := &block.MsgBlock().Header
newNode := newBlockNode(blockHeader, prevNode)
newNode.status = statusDataStored
b.index.AddNode(newNode)
//
// If a header-only node already exists (from maybeAcceptBlockHeader),
// upgrade its status rather than creating a new node. Creating a new
// node would overwrite the index entry, orphaning the pointer held by
// bestHeader's chainView and breaking Contains checks.
newNode := b.index.LookupNode(block.Hash())
if newNode != nil {
b.index.SetStatusFlags(newNode, statusDataStored)
} else {
blockHeader := &block.MsgBlock().Header
newNode = newBlockNode(blockHeader, prevNode)
newNode.status = statusDataStored | statusHeaderStored
b.index.AddNode(newNode)
}
err = b.index.flushToDB()
if err != nil {
return false, err

75
blockchain/accept_test.go Normal file
View file

@ -0,0 +1,75 @@
// Copyright (c) 2013-2026 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package blockchain
import (
"testing"
"github.com/btcsuite/btcd/blockchain/internal/testhelper"
"github.com/btcsuite/btcd/btcutil"
)
// TestMaybeAcceptBlockReusesHeaderNode ensures that when a block header is
// processed first via ProcessBlockHeader and later the full block arrives via
// ProcessBlock, the existing blockNode pointer is reused rather than replaced.
// Replacing the pointer would orphan the entry held by bestHeader's chainView,
// causing bestHeader.Contains(index.LookupNode(hash)) to return false and
// breaking IsValidHeader and downstream netsync checks.
func TestMaybeAcceptBlockReusesHeaderNode(t *testing.T) {
chain, params, tearDown := utxoCacheTestChain(
"TestMaybeAcceptBlockReusesHeaderNode")
defer tearDown()
// Build a base chain of 3 blocks.
//
// genesis -> 1 -> 2 -> 3
tip := btcutil.NewBlock(params.GenesisBlock)
_, _, err := addBlocks(3, chain, tip, []*testhelper.SpendableOut{})
if err != nil {
t.Fatalf("failed to build base chain: %v", err)
}
// Create block 4 without processing it.
prevBlock, err := chain.BlockByHeight(3)
if err != nil {
t.Fatalf("failed to get block at height 3: %v", err)
}
block4, _, err := newBlock(chain, prevBlock, nil)
if err != nil {
t.Fatalf("failed to create block 4: %v", err)
}
// Process block 4's header first.
block4Hash := block4.Hash()
_, err = chain.ProcessBlockHeader(
&block4.MsgBlock().Header, BFNone, false)
if err != nil {
t.Fatalf("ProcessBlockHeader fail: %v", err)
}
// Capture the header-only node pointer from the index.
headerNode := chain.index.LookupNode(block4Hash)
if headerNode == nil {
t.Fatal("header node not found in block index")
}
// Now process the full block.
_, _, err = chain.ProcessBlock(block4, BFNone)
if err != nil {
t.Fatalf("ProcessBlock fail: %v", err)
}
// The index must still hold the same pointer that bestHeader has.
// Before the fix, maybeAcceptBlock would create a fresh node and
// overwrite the index entry, orphaning the pointer in bestHeader.
fullBlockNode := chain.index.LookupNode(block4Hash)
if fullBlockNode != headerNode {
t.Fatal("ProcessBlock replaced the header node pointer " +
"instead of reusing it")
}
if !chain.bestHeader.Contains(fullBlockNode) {
t.Fatal("node no longer in bestHeader after ProcessBlock")
}
}