What changed, and why it matters
This commit adds a new public function, ProcessBlockHeader, to btcd's blockchain package. It is a feature addition that exposes existing internal header-validation logic so callers can download and validate block headers before receiving full blocks. There is no indication in the commit or supplied references that this fixes a security bug or introduces a vulnerability.
No security action required. Review as a normal feature addition; ensure callers of ProcessBlockHeader handle returned errors and do not bypass full block validation later.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces BlockChain.ProcessBlockHeader, which wraps the existing maybeAcceptBlockHeader with the chain lock. It performs chain selection and context-free/contextual validation for block headers, supports headers-first sync, and allows optionally skipping checkpoint checks. A comprehensive unit test is added covering main-chain growth, checkpoint enforcement, invalid forks, side-chain acceptance, and chain reorganization. No existing behavior is modified and no security flaw is described.
Changed components
blockchain/process.goblockchain/process_test.goInspect captured patch +214 / −0
diff --git a/blockchain/process.go b/blockchain/process.go
index 64d5c1e..0c114f4 100644
--- a/blockchain/process.go
+++ b/blockchain/process.go
@@ -11,6 +11,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/database"
+ "github.com/btcsuite/btcd/wire"
)
// BehaviorFlags is a bitmask defining tweaks to the normal behavior when
@@ -242,3 +243,34 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bo
return isMainChain, false, nil
}
+
+// ProcessBlockHeader is the main workhorse for handling insertion of new block
+// headers into the block chain using headers-first semantics. It includes
+// functionality such as rejecting headers that do not connect to an existing
+// known header, ensuring headers follow all rules and insertion into the block
+// index.
+//
+// Block headers that have already been inserted are ignored, unless they have
+// subsequently been marked invalid, in which case an appropriate error is
+// returned.
+//
+// It should be noted that this function intentionally does not accept block
+// headers that do not connect to an existing known header or to headers which
+// are already known to be a part of an invalid branch. This means headers must
+// be processed in order.
+//
+// The skipCheckpoint boolean allows skipping of the check for if the header is
+// part of the existing checkpoints.
+//
+// The returned boolean indicates whether or not the header was in the main chain
+// or not.
+//
+// This function is safe for concurrent access.
+func (b *BlockChain) ProcessBlockHeader(header *wire.BlockHeader,
+ flags BehaviorFlags, skipCheckpoint bool) (bool, error) {
+
+ b.chainLock.Lock()
+ defer b.chainLock.Unlock()
+
+ return b.maybeAcceptBlockHeader(header, flags, skipCheckpoint)
+}
diff --git a/blockchain/process_test.go b/blockchain/process_test.go
new file mode 100644
index 0000000..bc5de72
--- /dev/null
+++ b/blockchain/process_test.go
@@ -0,0 +1,182 @@
+package blockchain
+
+import (
+ "crypto/rand"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/btcsuite/btcd/blockchain/internal/testhelper"
+ "github.com/btcsuite/btcd/chaincfg"
+ "github.com/btcsuite/btcd/chaincfg/chainhash"
+ "github.com/btcsuite/btcd/wire"
+ "github.com/stretchr/testify/require"
+)
+
+// chainedHeaders returns desired amount of connected headers from the parentHeight.
+func chainedHeaders(parent *wire.BlockHeader, chainParams *chaincfg.Params,
+ parentHeight int32, numHeaders int) []*wire.BlockHeader {
+
+ headers := make([]*wire.BlockHeader, 0, numHeaders)
+ tip := parent
+
+ blockHeight := parentHeight
+ for range numHeaders {
+ // Use a timestamp that is one second after the previous block unless
+ // this is the first block in which case the current time is used.
+ var ts time.Time
+ if blockHeight == 1 {
+ ts = time.Unix(time.Now().Unix(), 0)
+ } else {
+ ts = tip.Timestamp.Add(time.Second)
+ }
+
+ var randBytes [4]byte
+ rand.Read(randBytes[:])
+ merkle := chainhash.HashH(randBytes[:])
+
+ header := wire.BlockHeader{
+ Version: 1,
+ PrevBlock: tip.BlockHash(),
+ MerkleRoot: merkle,
+ Bits: chainParams.PowLimitBits,
+ Timestamp: ts,
+ Nonce: 0,
+ }
+ if !testhelper.SolveBlock(&header) {
+ panic(fmt.Sprintf("Unable to solve block at height %d",
+ blockHeight))
+ }
+ headers = append(headers, &header)
+ tip = &header
+ }
+
+ return headers
+}
+
+func TestProcessBlockHeader(t *testing.T) {
+ chain, params, tearDown := utxoCacheTestChain("TestProcessBlockHeader")
+ defer tearDown()
+
+ // Generate and process the intial 10 block headers.
+ //
+ // genesis -> 1 -> 2 -> ... -> 10 (active)
+ headers := chainedHeaders(¶ms.GenesisBlock.Header, params, 0, 10)
+
+ // Set checkpoint at block 4.
+ fourthHeader := headers[3]
+ fourthHeaderHash := fourthHeader.BlockHash()
+ checkpoint := chaincfg.Checkpoint{
+ Height: 4,
+ Hash: &fourthHeaderHash,
+ }
+ chain.checkpoints = append(chain.checkpoints, checkpoint)
+ chain.checkpointsByHeight = make(map[int32]*chaincfg.Checkpoint)
+ chain.checkpointsByHeight[checkpoint.Height] = &checkpoint
+
+ for _, header := range headers {
+ isMainChain, err := chain.ProcessBlockHeader(header, BFNone, false)
+ require.NoError(t, err)
+ require.True(t, isMainChain)
+ }
+
+ // Check that the tip is correct.
+ lastHeader := headers[len(headers)-1]
+ lastHeaderHash := lastHeader.BlockHash()
+ tipNode := chain.bestHeader.Tip()
+ require.Equal(t, lastHeaderHash, tipNode.hash)
+ require.Equal(t, statusHeaderStored, tipNode.status)
+ require.Equal(t, int32(len(headers)), tipNode.height)
+
+ // Create invalid header at the checkpoint.
+ thirdHeaderHash := headers[2].BlockHash()
+ thirdNode := chain.index.LookupNode(&thirdHeaderHash)
+ invalidForkHeight := thirdNode.height
+ invalidHeaders := chainedHeaders(headers[2], params, invalidForkHeight, 1)
+
+ // Check that the header fails validation.
+ _, err := chain.ProcessBlockHeader(invalidHeaders[0], BFNone, false)
+ require.Errorf(t, err,
+ "invalidHeader %v passed verification but "+
+ "should've failed verification "+
+ "as the header doesn't match the checkpoint",
+ invalidHeaders[0].BlockHash().String(),
+ )
+
+ // Create sidechain block headers.
+ //
+ // genesis -> 1 -> 2 -> 3 -> 4 -> 5 -> ... -> 10 (active)
+ // \-> 6 -> ... -> 8 (valid-fork)
+ blockHash := headers[4].BlockHash()
+ node := chain.index.LookupNode(&blockHash)
+ forkHeight := node.height
+ sideChainHeaders := chainedHeaders(headers[4], params, node.height, 3)
+ sidechainTip := sideChainHeaders[len(sideChainHeaders)-1]
+
+ // Test that the last block header fails as it's missing the previous block
+ // header.
+ _, err = chain.ProcessBlockHeader(sidechainTip, BFNone, false)
+ require.Errorf(t, err,
+ "sideChainHeader %v passed verification but "+
+ "should've failed verification"+
+ "as the previous header is not known",
+ sideChainHeaders[len(sideChainHeaders)-1].BlockHash().String(),
+ )
+
+ // Verify that the side-chain headers verify.
+ for _, header := range sideChainHeaders {
+ isMainChain, err := chain.ProcessBlockHeader(header, BFNone, false)
+ require.NoError(t, err)
+ require.False(t, isMainChain)
+ }
+
+ // Check that the tip is still the same as before.
+ tipNode = chain.bestHeader.Tip()
+ require.Equal(t, lastHeaderHash, tipNode.hash)
+ require.Equal(t, statusHeaderStored, tipNode.status)
+ require.Equal(t, int32(len(headers)), tipNode.height)
+
+ // Verify that the side-chain extending headers verify.
+ sidechainExtendingHeaders := chainedHeaders(
+ sidechainTip, params, forkHeight+int32(len(sideChainHeaders)), 10)
+ for _, header := range sidechainExtendingHeaders {
+ isMainChain, err := chain.ProcessBlockHeader(header, BFNone, false)
+ require.NoError(t, err)
+
+ blockHash := header.BlockHash()
+ node := chain.index.LookupNode(&blockHash)
+ if node.height <= 10 {
+ require.False(t, isMainChain)
+ } else {
+ require.True(t, isMainChain)
+ }
+ }
+
+ // Create more sidechain block headers so that it becomes the active chain.
+ //
+ // genesis -> 1 -> 2 -> 3 -> 4 -> 5 -> ... -> 10 (valid-fork)
+ // \-> 6 -> ... -> 18 (active)
+ lastSideChainHeaderIdx := len(sidechainExtendingHeaders) - 1
+ lastSidechainHeader := sidechainExtendingHeaders[lastSideChainHeaderIdx]
+ lastSidechainHeaderHash := lastSidechainHeader.BlockHash()
+
+ // Check that the tip is now different.
+ tipNode = chain.bestHeader.Tip()
+ require.Equal(t, lastSidechainHeaderHash, tipNode.hash)
+ require.Equal(t, statusHeaderStored, tipNode.status)
+ require.Equal(t,
+ int32(len(sideChainHeaders)+len(sidechainExtendingHeaders))+forkHeight,
+ tipNode.height)
+
+ // Extend the original headers and check it still verifies.
+ extendedOrigHdrs := chainedHeaders(lastHeader, params, int32(len(headers)), 2)
+ for _, header := range extendedOrigHdrs {
+ isMainChain, err := chain.ProcessBlockHeader(header, BFNone, false)
+ require.NoError(t, err)
+ require.False(t, isMainChain)
+ }
+
+ // Check that the tip didn't change.
+ tipNode = chain.bestHeader.Tip()
+ require.Equal(t, lastSidechainHeaderHash, tipNode.hash)
+}
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.