feat(consensus): enforce block version in chain sync
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run

This commit is contained in:
Virgil 2026-04-04 18:56:36 +00:00
parent 01f4e5cd0a
commit 0ba5bbe49c
5 changed files with 60 additions and 31 deletions

View file

@ -173,7 +173,7 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
}
// Validate header.
if err := c.ValidateHeader(&blk, height); err != nil {
if err := c.ValidateHeader(&blk, height, opts.Forks); err != nil {
return err
}

View file

@ -12,13 +12,14 @@ import (
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/consensus"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/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 {
func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64, forks []config.HardFork) error {
currentHeight, err := c.Height()
if err != nil {
return coreerr.E("Chain.ValidateHeader", "validate: get height", err)
@ -34,6 +35,9 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
if !b.PrevID.IsZero() {
return coreerr.E("Chain.ValidateHeader", "validate: genesis block has non-zero prev_id", nil)
}
if err := consensus.CheckBlockVersion(b.MajorVersion, forks, expectedHeight); err != nil {
return coreerr.E("Chain.ValidateHeader", "validate: block version", err)
}
return nil
}
@ -46,6 +50,11 @@ func (c *Chain) ValidateHeader(b *types.Block, expectedHeight uint64) error {
return coreerr.E("Chain.ValidateHeader", fmt.Sprintf("validate: prev_id %s does not match top block %s", b.PrevID, topMeta.Hash), nil)
}
// Block major version check.
if err := consensus.CheckBlockVersion(b.MajorVersion, forks, expectedHeight); err != nil {
return coreerr.E("Chain.ValidateHeader", "validate: block version", err)
}
// Block size check.
var buf bytes.Buffer
enc := wire.NewEncoder(&buf)

View file

@ -8,8 +8,9 @@ package chain
import (
"testing"
store "dappco.re/go/core/store"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
store "dappco.re/go/core/store"
)
func TestValidateHeader_Good_Genesis(t *testing.T) {
@ -19,13 +20,13 @@ func TestValidateHeader_Good_Genesis(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0,
Timestamp: 1770897600,
},
MinerTx: testCoinbaseTx(0),
}
err := c.ValidateHeader(blk, 0)
err := c.ValidateHeader(blk, 0, config.MainnetForks)
if err != nil {
t.Fatalf("ValidateHeader genesis: %v", err)
}
@ -38,7 +39,7 @@ func TestValidateHeader_Good_Sequential(t *testing.T) {
// Store block 0.
blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
hash0 := types.Hash{0x01}
@ -47,14 +48,14 @@ func TestValidateHeader_Good_Sequential(t *testing.T) {
// Validate block 1.
blk1 := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0,
Timestamp: 1770897720,
PrevID: hash0,
},
MinerTx: testCoinbaseTx(1),
}
err := c.ValidateHeader(blk1, 1)
err := c.ValidateHeader(blk1, 1, config.MainnetForks)
if err != nil {
t.Fatalf("ValidateHeader block 1: %v", err)
}
@ -66,21 +67,21 @@ func TestValidateHeader_Bad_WrongPrevID(t *testing.T) {
c := New(s)
blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
c.PutBlock(blk0, &BlockMeta{Hash: types.Hash{0x01}, Height: 0})
blk1 := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0,
Timestamp: 1770897720,
PrevID: types.Hash{0xFF}, // wrong
},
MinerTx: testCoinbaseTx(1),
}
err := c.ValidateHeader(blk1, 1)
err := c.ValidateHeader(blk1, 1, config.MainnetForks)
if err == nil {
t.Fatal("expected error for wrong prev_id")
}
@ -92,12 +93,12 @@ func TestValidateHeader_Bad_WrongHeight(t *testing.T) {
c := New(s)
blk := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600},
BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0),
}
// Chain is empty (height 0), but we pass expectedHeight=5.
err := c.ValidateHeader(blk, 5)
err := c.ValidateHeader(blk, 5, config.MainnetForks)
if err == nil {
t.Fatal("expected error for wrong height")
}
@ -110,14 +111,33 @@ func TestValidateHeader_Bad_GenesisNonZeroPrev(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0,
PrevID: types.Hash{0xFF}, // genesis must have zero prev_id
},
MinerTx: testCoinbaseTx(0),
}
err := c.ValidateHeader(blk, 0)
err := c.ValidateHeader(blk, 0, config.MainnetForks)
if err == nil {
t.Fatal("expected error for genesis with non-zero prev_id")
}
}
func TestValidateHeader_Bad_WrongVersion(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, config.MainnetForks)
if err == nil {
t.Fatal("expected error for wrong block version")
}
}

View file

@ -127,14 +127,14 @@ func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSi
return nil
}
// expectedBlockMajorVersion returns the expected block major version for a
// ExpectedBlockMajorVersion returns the expected block major version for a
// given height and fork schedule. This maps hardfork eras to block versions:
//
// HF0 (genesis) -> 0
// HF1 -> 1
// HF3 -> 2
// HF4+ -> 3
func expectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 {
func ExpectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 {
if config.IsHardForkActive(forks, config.HF4Zarcanum, height) {
return config.CurrentBlockMajorVersion // 3
}
@ -147,13 +147,13 @@ func expectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 {
return config.BlockMajorVersionInitial // 0
}
// checkBlockVersion validates that the block's major version matches
// CheckBlockVersion validates that the block's major version matches
// what is expected at the given height in the fork schedule.
func checkBlockVersion(blk *types.Block, forks []config.HardFork, height uint64) error {
expected := expectedBlockMajorVersion(forks, height)
if blk.MajorVersion != expected {
return coreerr.E("checkBlockVersion", fmt.Sprintf("got %d, want %d at height %d",
blk.MajorVersion, expected, height), ErrBlockMajorVersion)
func CheckBlockVersion(majorVersion uint8, forks []config.HardFork, height uint64) error {
expected := ExpectedBlockMajorVersion(forks, height)
if majorVersion != expected {
return coreerr.E("CheckBlockVersion", fmt.Sprintf("got %d, want %d at height %d",
majorVersion, expected, height), ErrBlockMajorVersion)
}
return nil
}
@ -166,7 +166,7 @@ func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, a
recentTimestamps []uint64, forks []config.HardFork) error {
// Block major version check.
if err := checkBlockVersion(blk, forks, height); err != nil {
if err := CheckBlockVersion(blk.MajorVersion, forks, height); err != nil {
return err
}

View file

@ -263,9 +263,9 @@ func TestExpectedBlockMajorVersion_Good(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := expectedBlockMajorVersion(tt.forks, tt.height)
got := ExpectedBlockMajorVersion(tt.forks, tt.height)
if got != tt.want {
t.Errorf("expectedBlockMajorVersion(%d) = %d, want %d", tt.height, got, tt.want)
t.Errorf("ExpectedBlockMajorVersion(%d) = %d, want %d", tt.height, got, tt.want)
}
})
}
@ -294,7 +294,7 @@ func TestCheckBlockVersion_Good(t *testing.T) {
Flags: 0,
},
}
err := checkBlockVersion(blk, tt.forks, tt.height)
err := CheckBlockVersion(blk.MajorVersion, tt.forks, tt.height)
require.NoError(t, err)
})
}
@ -323,7 +323,7 @@ func TestCheckBlockVersion_Bad(t *testing.T) {
Flags: 0,
},
}
err := checkBlockVersion(blk, tt.forks, tt.height)
err := CheckBlockVersion(blk.MajorVersion, tt.forks, tt.height)
assert.ErrorIs(t, err, ErrBlockVersion)
})
}
@ -336,17 +336,17 @@ func TestCheckBlockVersion_Ugly(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 255, Timestamp: now},
}
err := checkBlockVersion(blk, config.MainnetForks, 0)
err := CheckBlockVersion(blk.MajorVersion, config.MainnetForks, 0)
assert.ErrorIs(t, err, ErrBlockVersion)
err = checkBlockVersion(blk, config.MainnetForks, 10081)
err = CheckBlockVersion(blk.MajorVersion, config.MainnetForks, 10081)
assert.ErrorIs(t, err, ErrBlockVersion)
// Version 0 at the exact HF1 boundary (height 10080 -- fork not yet active).
blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: config.BlockMajorVersionInitial, Timestamp: now},
}
err = checkBlockVersion(blk0, config.MainnetForks, 10080)
err = CheckBlockVersion(blk0.MajorVersion, config.MainnetForks, 10080)
require.NoError(t, err)
}