From b1a0e9637b10dd70c107f08bc3b2a7090081e9c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 20:40:11 +0000 Subject: [PATCH] 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 --- consensus/block.go | 42 ++++++++++++++-- consensus/block_test.go | 105 ++++++++++++++++++++++++++++++++++++++-- consensus/errors.go | 17 ++++--- 3 files changed, 150 insertions(+), 14 deletions(-) diff --git a/consensus/block.go b/consensus/block.go index 0015a5c..eda9fa9 100644 --- a/consensus/block.go +++ b/consensus/block.go @@ -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 diff --git a/consensus/block_test.go b/consensus/block_test.go index efff152..a021878 100644 --- a/consensus/block_test.go +++ b/consensus/block_test.go @@ -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) +} diff --git a/consensus/errors.go b/consensus/errors.go index 0c0959d..06a40d8 100644 --- a/consensus/errors.go +++ b/consensus/errors.go @@ -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") )