feat(consensus): validate block major version for HF1

Adds expectedBlockMajorVersion and checkBlockVersion, called from
ValidateBlock before timestamp validation. Block version must match
the fork era: HF0->0, HF1->1, HF3->2, HF4+->3.
Tests cover both mainnet and testnet fork schedules including
boundary heights.

Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
Claude 2026-03-16 20:40:11 +00:00
parent f88d582c64
commit b1a0e9637b
No known key found for this signature in database
GPG key ID: AF404715446AEB41
3 changed files with 150 additions and 14 deletions

View file

@ -125,13 +125,49 @@ func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSi
return nil
}
// 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 {
if config.IsHardForkActive(forks, config.HF4Zarcanum, height) {
return config.CurrentBlockMajorVersion // 3
}
if config.IsHardForkActive(forks, config.HF3, height) {
return config.HF3BlockMajorVersion // 2
}
if config.IsHardForkActive(forks, config.HF1, height) {
return config.HF1BlockMajorVersion // 1
}
return config.BlockMajorVersionInitial // 0
}
// 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 fmt.Errorf("%w: got %d, want %d at height %d",
ErrBlockMajorVersion, blk.MajorVersion, expected, height)
}
return nil
}
// ValidateBlock performs full consensus validation on a block. It checks
// the timestamp, miner transaction structure, and reward. Transaction
// semantic validation for regular transactions should be done separately
// via ValidateTransaction for each tx in the block.
// the block version, timestamp, miner transaction structure, and reward.
// Transaction semantic validation for regular transactions should be done
// separately via ValidateTransaction for each tx in the block.
func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, adjustedTime uint64,
recentTimestamps []uint64, forks []config.HardFork) error {
// Block major version check.
if err := checkBlockVersion(blk, forks, height); err != nil {
return err
}
// Timestamp validation.
if err := CheckTimestamp(blk.Timestamp, blk.Flags, adjustedTime, recentTimestamps); err != nil {
return err

View file

@ -156,7 +156,7 @@ func TestValidateBlock_Good(t *testing.T) {
height := uint64(100)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0, // pre-HF1 on mainnet
Timestamp: now,
Flags: 0, // PoW
},
@ -172,7 +172,7 @@ func TestValidateBlock_Bad_Timestamp(t *testing.T) {
height := uint64(100)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0, // pre-HF1 on mainnet
Timestamp: now + config.BlockFutureTimeLimit + 100,
Flags: 0,
},
@ -188,7 +188,7 @@ func TestValidateBlock_Bad_MinerTx(t *testing.T) {
height := uint64(100)
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
MajorVersion: 0, // pre-HF1 on mainnet
Timestamp: now,
Flags: 0,
},
@ -198,3 +198,102 @@ func TestValidateBlock_Bad_MinerTx(t *testing.T) {
err := ValidateBlock(blk, height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
assert.ErrorIs(t, err, ErrMinerTxHeight)
}
// --- Block major version tests (Task 10) ---
func TestValidateBlock_MajorVersion_Good(t *testing.T) {
now := uint64(time.Now().Unix())
tests := []struct {
name string
forks []config.HardFork
height uint64
version uint8
}{
// Mainnet: pre-HF1 expects version 0.
{name: "mainnet_preHF1", forks: config.MainnetForks, height: 5000, version: 0},
// Mainnet: post-HF1 expects version 1.
{name: "mainnet_postHF1", forks: config.MainnetForks, height: 20000, version: 1},
// Testnet: HF1 active from genesis, HF3 active from genesis, expects version 2.
{name: "testnet_genesis", forks: config.TestnetForks, height: 5, version: 2},
// Testnet: post-HF4 (height > 100) expects version 3.
{name: "testnet_postHF4", forks: config.TestnetForks, height: 200, version: 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: tt.version,
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTx(tt.height),
}
err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
require.NoError(t, err)
})
}
}
func TestValidateBlock_MajorVersion_Bad(t *testing.T) {
now := uint64(time.Now().Unix())
tests := []struct {
name string
forks []config.HardFork
height uint64
version uint8
}{
// Mainnet: pre-HF1 with wrong version 1.
{name: "mainnet_preHF1_v1", forks: config.MainnetForks, height: 5000, version: 1},
// Mainnet: post-HF1 with wrong version 0.
{name: "mainnet_postHF1_v0", forks: config.MainnetForks, height: 20000, version: 0},
// Mainnet: post-HF1 with wrong version 2.
{name: "mainnet_postHF1_v2", forks: config.MainnetForks, height: 20000, version: 2},
// Testnet: post-HF4 with wrong version 2.
{name: "testnet_postHF4_v2", forks: config.TestnetForks, height: 200, version: 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: tt.version,
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTx(tt.height),
}
err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks)
assert.ErrorIs(t, err, ErrBlockMajorVersion)
})
}
}
func TestValidateBlock_MajorVersion_Ugly(t *testing.T) {
now := uint64(time.Now().Unix())
// Boundary test: exactly at HF1 activation height (10080) on mainnet.
// HF1 activates at heights strictly greater than 10080, so at height
// 10080 itself HF1 is NOT active; version must be 0.
blk := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 0,
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTx(10080),
}
err := ValidateBlock(blk, 10080, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
require.NoError(t, err)
// At height 10081, HF1 IS active; version must be 1.
blk2 := &types.Block{
BlockHeader: types.BlockHeader{
MajorVersion: 1,
Timestamp: now,
Flags: 0,
},
MinerTx: *validMinerTx(10081),
}
err = ValidateBlock(blk2, 10081, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, config.MainnetForks)
require.NoError(t, err)
}

View file

@ -27,12 +27,13 @@ var (
ErrNegativeFee = errors.New("consensus: outputs exceed inputs")
// Block errors.
ErrBlockTooLarge = errors.New("consensus: block exceeds max size")
ErrTimestampFuture = errors.New("consensus: block timestamp too far in future")
ErrTimestampOld = errors.New("consensus: block timestamp below median")
ErrMinerTxInputs = errors.New("consensus: invalid miner transaction inputs")
ErrMinerTxHeight = errors.New("consensus: miner transaction height mismatch")
ErrMinerTxUnlock = errors.New("consensus: miner transaction unlock time invalid")
ErrRewardMismatch = errors.New("consensus: block reward mismatch")
ErrMinerTxProofs = errors.New("consensus: miner transaction proof count invalid")
ErrBlockTooLarge = errors.New("consensus: block exceeds max size")
ErrBlockMajorVersion = errors.New("consensus: invalid block major version for height")
ErrTimestampFuture = errors.New("consensus: block timestamp too far in future")
ErrTimestampOld = errors.New("consensus: block timestamp below median")
ErrMinerTxInputs = errors.New("consensus: invalid miner transaction inputs")
ErrMinerTxHeight = errors.New("consensus: miner transaction height mismatch")
ErrMinerTxUnlock = errors.New("consensus: miner transaction unlock time invalid")
ErrRewardMismatch = errors.New("consensus: block reward mismatch")
ErrMinerTxProofs = errors.New("consensus: miner transaction proof count invalid")
)