From f5822e7222fd853983b7eeef0ea13ea5b8df2746 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 21:50:12 +0000 Subject: [PATCH] feat(chain): block header validation (linkage, height, size) Co-Authored-By: Charon --- chain/validate.go | 59 ++++++++++++++++++++ chain/validate_test.go | 123 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 chain/validate.go create mode 100644 chain/validate_test.go diff --git a/chain/validate.go b/chain/validate.go new file mode 100644 index 0000000..642705d --- /dev/null +++ b/chain/validate.go @@ -0,0 +1,59 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package chain + +import ( + "bytes" + "fmt" + + "forge.lthn.ai/core/go-blockchain/config" + "forge.lthn.ai/core/go-blockchain/types" + "forge.lthn.ai/core/go-blockchain/wire" +) + +// ValidateHeader checks a block header before storage. +// expectedHeight is the height at which this block would be stored. +func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error { + currentHeight, err := c.Height() + if err != nil { + return fmt.Errorf("validate: get height: %w", err) + } + + // Height sequence check. + if expectedHeight != currentHeight { + return fmt.Errorf("validate: expected height %d but chain is at %d", + expectedHeight, currentHeight) + } + + // Genesis block: prev_id must be zero. + if expectedHeight == 0 { + if !b.PrevID.IsZero() { + return fmt.Errorf("validate: genesis block has non-zero prev_id") + } + return nil + } + + // Non-genesis: prev_id must match top block hash. + _, topMeta, err := c.TopBlock() + if err != nil { + return fmt.Errorf("validate: get top block: %w", err) + } + if b.PrevID != topMeta.Hash { + return fmt.Errorf("validate: prev_id %s does not match top block %s", + b.PrevID, topMeta.Hash) + } + + // Block size check. + var buf bytes.Buffer + enc := wire.NewEncoder(&buf) + wire.EncodeBlock(enc, b) + if enc.Err() == nil && uint64(buf.Len()) > config.MaxBlockSize { + return fmt.Errorf("validate: block size %d exceeds max %d", + buf.Len(), config.MaxBlockSize) + } + + return nil +} diff --git a/chain/validate_test.go b/chain/validate_test.go new file mode 100644 index 0000000..6c96ab5 --- /dev/null +++ b/chain/validate_test.go @@ -0,0 +1,123 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package chain + +import ( + "testing" + + store "forge.lthn.ai/core/go-store" + "forge.lthn.ai/core/go-blockchain/types" +) + +func TestValidateHeader_Good_Genesis(t *testing.T) { + s, _ := store.New(":memory:") + defer s.Close() + c := New(s) + + blk := &types.Block{ + BlockHeader: types.BlockHeader{ + MajorVersion: 1, + Timestamp: 1770897600, + }, + MinerTx: testCoinbaseTx(0), + } + + err := c.ValidateHeader(blk, 0) + if err != nil { + t.Fatalf("ValidateHeader genesis: %v", err) + } +} + +func TestValidateHeader_Good_Sequential(t *testing.T) { + s, _ := store.New(":memory:") + defer s.Close() + c := New(s) + + // Store block 0. + blk0 := &types.Block{ + BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600}, + MinerTx: testCoinbaseTx(0), + } + hash0 := types.Hash{0x01} + c.PutBlock(blk0, &BlockMeta{Hash: hash0, Height: 0, Timestamp: 1770897600}) + + // Validate block 1. + blk1 := &types.Block{ + BlockHeader: types.BlockHeader{ + MajorVersion: 1, + Timestamp: 1770897720, + PrevID: hash0, + }, + MinerTx: testCoinbaseTx(1), + } + + err := c.ValidateHeader(blk1, 1) + if err != nil { + t.Fatalf("ValidateHeader block 1: %v", err) + } +} + +func TestValidateHeader_Bad_WrongPrevID(t *testing.T) { + s, _ := store.New(":memory:") + defer s.Close() + c := New(s) + + blk0 := &types.Block{ + BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600}, + MinerTx: testCoinbaseTx(0), + } + c.PutBlock(blk0, &BlockMeta{Hash: types.Hash{0x01}, Height: 0}) + + blk1 := &types.Block{ + BlockHeader: types.BlockHeader{ + MajorVersion: 1, + Timestamp: 1770897720, + PrevID: types.Hash{0xFF}, // wrong + }, + MinerTx: testCoinbaseTx(1), + } + + err := c.ValidateHeader(blk1, 1) + if err == nil { + t.Fatal("expected error for wrong prev_id") + } +} + +func TestValidateHeader_Bad_WrongHeight(t *testing.T) { + s, _ := store.New(":memory:") + defer s.Close() + c := New(s) + + blk := &types.Block{ + BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600}, + MinerTx: testCoinbaseTx(0), + } + + // Chain is empty (height 0), but we pass expectedHeight=5. + err := c.ValidateHeader(blk, 5) + if err == nil { + t.Fatal("expected error for wrong height") + } +} + +func TestValidateHeader_Bad_GenesisNonZeroPrev(t *testing.T) { + s, _ := store.New(":memory:") + defer s.Close() + c := New(s) + + blk := &types.Block{ + BlockHeader: types.BlockHeader{ + MajorVersion: 1, + PrevID: types.Hash{0xFF}, // genesis must have zero prev_id + }, + MinerTx: testCoinbaseTx(0), + } + + err := c.ValidateHeader(blk, 0) + if err == nil { + t.Fatal("expected error for genesis with non-zero prev_id") + } +}