From f44bfd3d7c26ce63669ff9fcc565c99bf2933766 Mon Sep 17 00:00:00 2001 From: Kilombino Date: Thu, 15 Oct 2026 18:09:43 +0000 Subject: [PATCH] blockchain: refuse a v2 header that commits to a height below the BLAKE2b activation, as Knots does in CheckBlockHeader CheckHeaderV2Sanity is the context-free part of Knots' CheckBlockHeader for BLAKE2b headers: a v2 header must commit to a height where BLAKE2b proof of work is required (bad-version-sha256d), and a network without BLAKE2b configured takes no v2 header. It needs only the header, so it runs where the real height is not known yet (an orphan block), next to checkBlockSanity in ProcessBlock and CheckConnectBlockTemplate, and in verifychain; checkHeaderV2Context still checks the committed height against the real one. Exported for light clients (neutrino's context-free header check). Tests: a header committing to the activation height and one to the block below it, and a network without BLAKE2b. --- blockchain/blake2b_test.go | 32 ++++++++++++++++++++++++++++++++ blockchain/blake2bpow.go | 24 ++++++++++++++++++++++++ blockchain/process.go | 4 ++++ blockchain/validate.go | 4 ++++ rpcserver.go | 5 +++++ 5 files changed, 69 insertions(+) diff --git a/blockchain/blake2b_test.go b/blockchain/blake2b_test.go index 80535d0d80..b9c4f3d160 100644 --- a/blockchain/blake2b_test.go +++ b/blockchain/blake2b_test.go @@ -249,3 +249,35 @@ func TestBlake2bHeaderContext(t *testing.T) { t.Fatalf("classic header without BLAKE2b configured: %v", err) } } + +// TestCheckHeaderV2Sanity pins the context-free rule Knots applies in +// CheckBlockHeader: a v2 header committing to the activation height is +// accepted, one committing to the block below it is refused (bad-version- +// sha256d), and a network without BLAKE2b configured takes no v2 header. +func TestCheckHeaderV2Sanity(t *testing.T) { + params := &chaincfg.MainNetParams + v2 := mustHeader(t, activationHeaderRaw) + if v2.Height != 961640 { + t.Fatalf("the activation header commits to %d", v2.Height) + } + + if err := CheckHeaderV2Sanity(v2, params); err != nil { + t.Fatalf("v2 header committing to the activation height: %v", err) + } + below := *v2 + below.Height = 961639 + if code, _ := ruleCode(CheckHeaderV2Sanity(&below, params)); code != ErrBadHeaderV2 { + t.Fatalf("v2 header committing to the block below the activation: got %v, want ErrBadHeaderV2", code) + } + + classic := chaincfg.MainNetParams.GenesisBlock.Header + if err := CheckHeaderV2Sanity(&classic, params); err != nil { + t.Fatalf("classic header: %v", err) + } + + none := chaincfg.MainNetParams + none.Blake2bHeight = 0 + if code, _ := ruleCode(CheckHeaderV2Sanity(v2, &none)); code != ErrUnsupportedProofOfWork { + t.Fatalf("v2 header without BLAKE2b configured: got %v, want ErrUnsupportedProofOfWork", code) + } +} diff --git a/blockchain/blake2bpow.go b/blockchain/blake2bpow.go index 65f9518fd3..d7cff5c3c2 100644 --- a/blockchain/blake2bpow.go +++ b/blockchain/blake2bpow.go @@ -73,6 +73,30 @@ func checkHeaderV2Context(header *wire.BlockHeader, height int32, return nil } +// CheckHeaderV2Sanity is the context-free part of Knots' CheckBlockHeader for +// BLAKE2b headers: a v2 header must commit to a height at which BLAKE2b proof +// of work is required (Knots' bad-version-sha256d), and a network without +// BLAKE2b proof of work configured takes no v2 header. It needs only the +// header, so it runs where the block's real height is not known yet (an +// orphan, a header out of order); checkHeaderV2Context then checks the +// committed height against the real one. +func CheckHeaderV2Sanity(header *wire.BlockHeader, params *chaincfg.Params) error { + if !header.IsV2() { + return nil + } + if !params.Blake2bPowDeployed() { + return ruleError(ErrUnsupportedProofOfWork, "BLAKE2b (header v2) "+ + "proof of work is not configured for network "+params.Name) + } + if !params.IsBlake2bHeight(header.Height) { + str := fmt.Sprintf("header v2 commits to height %d: blocks require "+ + "SHA256d proof of work until height %d", header.Height, + params.Blake2bHeight) + return ruleError(ErrBadHeaderV2, str) + } + return nil +} + // applyBlake2bTargetShift is Knots' ApplyBlake2bTargetShift: the target of the // first block mined under BLAKE2b, from the one the usual rules give. func applyBlake2bTargetShift(bits uint32, params *chaincfg.Params) uint32 { diff --git a/blockchain/process.go b/blockchain/process.go index 64d5c1e14f..8e76e84c70 100644 --- a/blockchain/process.go +++ b/blockchain/process.go @@ -169,6 +169,10 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bo if err != nil { return false, false, err } + err = CheckHeaderV2Sanity(&block.MsgBlock().Header, b.chainParams) + if err != nil { + return false, false, err + } // Find the previous checkpoint and perform some additional checks based // on the checkpoint. This provides a few nice properties such as diff --git a/blockchain/validate.go b/blockchain/validate.go index 2febf62f1c..85d3859902 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -1394,6 +1394,10 @@ func (b *BlockChain) CheckConnectBlockTemplate(block *btcutil.Block) error { if err != nil { return err } + err = CheckHeaderV2Sanity(&header, b.chainParams) + if err != nil { + return err + } err = b.checkBlockContext(block, tip, flags) if err != nil { diff --git a/rpcserver.go b/rpcserver.go index bec0fad82c..ec53b69eec 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3720,6 +3720,11 @@ func verifyChain(s *rpcServer, level, depth int32) error { if level > 0 { err := blockchain.CheckBlockSanity(block, s.cfg.ChainParams.PowLimit, s.cfg.TimeSource) + if err == nil { + err = blockchain.CheckHeaderV2Sanity( + &block.MsgBlock().Header, s.cfg.ChainParams, + ) + } if err != nil { rpcsLog.Errorf("Verify is unable to validate "+ "block at hash %v height %d: %v",