From c1a46122cae1e8138fbde5e793c5d9ff928048ba Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 16 Sep 2025 20:52:24 +0900 Subject: [PATCH] blockchain: add ProcessBlockHeader ProcessBlockHeader performs chain selection and context-free & contextual validation for the given block header. The function allows a header-first downloading of blocks even without checkpoints. --- blockchain/process.go | 32 +++++++ blockchain/process_test.go | 182 +++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 blockchain/process_test.go diff --git a/blockchain/process.go b/blockchain/process.go index 64d5c1e1..0c114f47 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 00000000..bc5de726 --- /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) +}