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. // Validate header.
if err := c.ValidateHeader(&blk, height); err != nil { if err := c.ValidateHeader(&blk, height, opts.Forks); err != nil {
return err return err
} }

View file

@ -12,13 +12,14 @@ import (
coreerr "dappco.re/go/core/log" coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/config" "dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/consensus"
"dappco.re/go/core/blockchain/types" "dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire" "dappco.re/go/core/blockchain/wire"
) )
// ValidateHeader checks a block header before storage. // ValidateHeader checks a block header before storage.
// expectedHeight is the height at which this block would be stored. // 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() currentHeight, err := c.Height()
if err != nil { if err != nil {
return coreerr.E("Chain.ValidateHeader", "validate: get height", err) 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() { if !b.PrevID.IsZero() {
return coreerr.E("Chain.ValidateHeader", "validate: genesis block has non-zero prev_id", nil) 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 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) 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. // Block size check.
var buf bytes.Buffer var buf bytes.Buffer
enc := wire.NewEncoder(&buf) enc := wire.NewEncoder(&buf)

View file

@ -8,8 +8,9 @@ package chain
import ( import (
"testing" "testing"
store "dappco.re/go/core/store" "dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types" "dappco.re/go/core/blockchain/types"
store "dappco.re/go/core/store"
) )
func TestValidateHeader_Good_Genesis(t *testing.T) { func TestValidateHeader_Good_Genesis(t *testing.T) {
@ -19,13 +20,13 @@ func TestValidateHeader_Good_Genesis(t *testing.T) {
blk := &types.Block{ blk := &types.Block{
BlockHeader: types.BlockHeader{ BlockHeader: types.BlockHeader{
MajorVersion: 1, MajorVersion: 0,
Timestamp: 1770897600, Timestamp: 1770897600,
}, },
MinerTx: testCoinbaseTx(0), MinerTx: testCoinbaseTx(0),
} }
err := c.ValidateHeader(blk, 0) err := c.ValidateHeader(blk, 0, config.MainnetForks)
if err != nil { if err != nil {
t.Fatalf("ValidateHeader genesis: %v", err) t.Fatalf("ValidateHeader genesis: %v", err)
} }
@ -38,7 +39,7 @@ func TestValidateHeader_Good_Sequential(t *testing.T) {
// Store block 0. // Store block 0.
blk0 := &types.Block{ blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600}, BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0), MinerTx: testCoinbaseTx(0),
} }
hash0 := types.Hash{0x01} hash0 := types.Hash{0x01}
@ -47,14 +48,14 @@ func TestValidateHeader_Good_Sequential(t *testing.T) {
// Validate block 1. // Validate block 1.
blk1 := &types.Block{ blk1 := &types.Block{
BlockHeader: types.BlockHeader{ BlockHeader: types.BlockHeader{
MajorVersion: 1, MajorVersion: 0,
Timestamp: 1770897720, Timestamp: 1770897720,
PrevID: hash0, PrevID: hash0,
}, },
MinerTx: testCoinbaseTx(1), MinerTx: testCoinbaseTx(1),
} }
err := c.ValidateHeader(blk1, 1) err := c.ValidateHeader(blk1, 1, config.MainnetForks)
if err != nil { if err != nil {
t.Fatalf("ValidateHeader block 1: %v", err) t.Fatalf("ValidateHeader block 1: %v", err)
} }
@ -66,21 +67,21 @@ func TestValidateHeader_Bad_WrongPrevID(t *testing.T) {
c := New(s) c := New(s)
blk0 := &types.Block{ blk0 := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600}, BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0), MinerTx: testCoinbaseTx(0),
} }
c.PutBlock(blk0, &BlockMeta{Hash: types.Hash{0x01}, Height: 0}) c.PutBlock(blk0, &BlockMeta{Hash: types.Hash{0x01}, Height: 0})
blk1 := &types.Block{ blk1 := &types.Block{
BlockHeader: types.BlockHeader{ BlockHeader: types.BlockHeader{
MajorVersion: 1, MajorVersion: 0,
Timestamp: 1770897720, Timestamp: 1770897720,
PrevID: types.Hash{0xFF}, // wrong PrevID: types.Hash{0xFF}, // wrong
}, },
MinerTx: testCoinbaseTx(1), MinerTx: testCoinbaseTx(1),
} }
err := c.ValidateHeader(blk1, 1) err := c.ValidateHeader(blk1, 1, config.MainnetForks)
if err == nil { if err == nil {
t.Fatal("expected error for wrong prev_id") t.Fatal("expected error for wrong prev_id")
} }
@ -92,12 +93,12 @@ func TestValidateHeader_Bad_WrongHeight(t *testing.T) {
c := New(s) c := New(s)
blk := &types.Block{ blk := &types.Block{
BlockHeader: types.BlockHeader{MajorVersion: 1, Timestamp: 1770897600}, BlockHeader: types.BlockHeader{MajorVersion: 0, Timestamp: 1770897600},
MinerTx: testCoinbaseTx(0), MinerTx: testCoinbaseTx(0),
} }
// Chain is empty (height 0), but we pass expectedHeight=5. // 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 { if err == nil {
t.Fatal("expected error for wrong height") t.Fatal("expected error for wrong height")
} }
@ -110,14 +111,33 @@ func TestValidateHeader_Bad_GenesisNonZeroPrev(t *testing.T) {
blk := &types.Block{ blk := &types.Block{
BlockHeader: types.BlockHeader{ BlockHeader: types.BlockHeader{
MajorVersion: 1, MajorVersion: 0,
PrevID: types.Hash{0xFF}, // genesis must have zero prev_id PrevID: types.Hash{0xFF}, // genesis must have zero prev_id
}, },
MinerTx: testCoinbaseTx(0), MinerTx: testCoinbaseTx(0),
} }
err := c.ValidateHeader(blk, 0) err := c.ValidateHeader(blk, 0, config.MainnetForks)
if err == nil { if err == nil {
t.Fatal("expected error for genesis with non-zero prev_id") 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 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: // given height and fork schedule. This maps hardfork eras to block versions:
// //
// HF0 (genesis) -> 0 // HF0 (genesis) -> 0
// HF1 -> 1 // HF1 -> 1
// HF3 -> 2 // HF3 -> 2
// HF4+ -> 3 // 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) { if config.IsHardForkActive(forks, config.HF4Zarcanum, height) {
return config.CurrentBlockMajorVersion // 3 return config.CurrentBlockMajorVersion // 3
} }
@ -147,13 +147,13 @@ func expectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 {
return config.BlockMajorVersionInitial // 0 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. // what is expected at the given height in the fork schedule.
func checkBlockVersion(blk *types.Block, forks []config.HardFork, height uint64) error { func CheckBlockVersion(majorVersion uint8, forks []config.HardFork, height uint64) error {
expected := expectedBlockMajorVersion(forks, height) expected := ExpectedBlockMajorVersion(forks, height)
if blk.MajorVersion != expected { if majorVersion != expected {
return coreerr.E("checkBlockVersion", fmt.Sprintf("got %d, want %d at height %d", return coreerr.E("CheckBlockVersion", fmt.Sprintf("got %d, want %d at height %d",
blk.MajorVersion, expected, height), ErrBlockMajorVersion) majorVersion, expected, height), ErrBlockMajorVersion)
} }
return nil return nil
} }
@ -166,7 +166,7 @@ func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, a
recentTimestamps []uint64, forks []config.HardFork) error { recentTimestamps []uint64, forks []config.HardFork) error {
// Block major version check. // Block major version check.
if err := checkBlockVersion(blk, forks, height); err != nil { if err := CheckBlockVersion(blk.MajorVersion, forks, height); err != nil {
return err return err
} }

View file

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