feat(chain): block header validation (linkage, height, size)
Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
89f5f0ebdf
commit
f5822e7222
2 changed files with 182 additions and 0 deletions
59
chain/validate.go
Normal file
59
chain/validate.go
Normal file
|
|
@ -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
|
||||
}
|
||||
123
chain/validate_test.go
Normal file
123
chain/validate_test.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue