diff --git a/consensus/block.go b/consensus/block.go index 6bbc9c0..93038f3 100644 --- a/consensus/block.go +++ b/consensus/block.go @@ -61,10 +61,34 @@ func medianTimestamp(timestamps []uint64) uint64 { return sorted[n/2] } +func expectedMinerTxVersion(forks []config.HardFork, height uint64) uint64 { + switch { + case config.IsHardForkActive(forks, config.HF5, height): + return types.VersionPostHF5 + case config.IsHardForkActive(forks, config.HF4Zarcanum, height): + return types.VersionPostHF4 + default: + return types.VersionPreHF4 + } +} + // ValidateMinerTx checks the structure of a coinbase (miner) transaction. // For PoW blocks: exactly 1 input (TxInputGenesis). For PoS blocks: exactly // 2 inputs (TxInputGenesis + stake input). func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFork) error { + expectedVersion := expectedMinerTxVersion(forks, height) + if tx.Version != expectedVersion { + return coreerr.E("ValidateMinerTx", fmt.Sprintf("version %d invalid at height %d (expected %d)", + tx.Version, height, expectedVersion), ErrMinerTxVersion) + } + if tx.Version >= types.VersionPostHF5 { + currentFork := config.VersionAtHeight(forks, height) + if tx.HardforkID != currentFork { + return coreerr.E("ValidateMinerTx", fmt.Sprintf("hardfork id %d does not match active fork %d at height %d", + tx.HardforkID, currentFork, height), ErrMinerTxVersion) + } + } + if len(tx.Vin) == 0 { return coreerr.E("ValidateMinerTx", "no inputs", ErrMinerTxInputs) } diff --git a/consensus/block_test.go b/consensus/block_test.go index 70dbea2..6a360c0 100644 --- a/consensus/block_test.go +++ b/consensus/block_test.go @@ -66,7 +66,7 @@ func TestCheckTimestamp_Ugly(t *testing.T) { func validMinerTx(height uint64) *types.Transaction { return &types.Transaction{ - Version: types.VersionInitial, + Version: types.VersionPreHF4, Vin: []types.TxInput{types.TxInputGenesis{Height: height}}, Vout: []types.TxOutput{ types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, @@ -74,6 +74,15 @@ func validMinerTx(height uint64) *types.Transaction { } } +func validMinerTxForForks(height uint64, forks []config.HardFork) *types.Transaction { + tx := validMinerTx(height) + tx.Version = expectedMinerTxVersion(forks, height) + if tx.Version >= types.VersionPostHF5 { + tx.HardforkID = config.VersionAtHeight(forks, height) + } + return tx +} + func TestValidateMinerTx_Good(t *testing.T) { tx := validMinerTx(100) err := ValidateMinerTx(tx, 100, config.MainnetForks) @@ -87,14 +96,14 @@ func TestValidateMinerTx_Bad_WrongHeight(t *testing.T) { } func TestValidateMinerTx_Bad_NoInputs(t *testing.T) { - tx := &types.Transaction{Version: types.VersionInitial} + tx := &types.Transaction{Version: types.VersionPreHF4} err := ValidateMinerTx(tx, 100, config.MainnetForks) assert.ErrorIs(t, err, ErrMinerTxInputs) } func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) { tx := &types.Transaction{ - Version: types.VersionInitial, + Version: types.VersionPreHF4, Vin: []types.TxInput{types.TxInputToKey{Amount: 1}}, } err := ValidateMinerTx(tx, 100, config.MainnetForks) @@ -103,7 +112,7 @@ func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) { func TestValidateMinerTx_Good_PoS(t *testing.T) { tx := &types.Transaction{ - Version: types.VersionInitial, + Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputGenesis{Height: 100}, types.TxInputToKey{Amount: 1}, // PoS stake input @@ -117,6 +126,98 @@ func TestValidateMinerTx_Good_PoS(t *testing.T) { require.NoError(t, err) } +func TestValidateMinerTx_Version_Good(t *testing.T) { + tests := []struct { + name string + forks []config.HardFork + tx *types.Transaction + height uint64 + }{ + { + name: "mainnet_pre_hf4_v1", + forks: config.MainnetForks, + height: 100, + tx: validMinerTx(100), + }, + { + name: "testnet_post_hf4_v2", + forks: config.TestnetForks, + height: 101, + tx: &types.Transaction{ + Version: types.VersionPostHF4, + Vin: []types.TxInput{types.TxInputGenesis{Height: 101}}, + Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}}, + }, + }, + { + name: "testnet_post_hf5_v3", + forks: config.TestnetForks, + height: 201, + tx: &types.Transaction{ + Version: types.VersionPostHF5, + HardforkID: config.HF5, + Vin: []types.TxInput{types.TxInputGenesis{Height: 201}}, + Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateMinerTx(tt.tx, tt.height, tt.forks) + require.NoError(t, err) + }) + } +} + +func TestValidateMinerTx_Version_Bad(t *testing.T) { + tests := []struct { + name string + forks []config.HardFork + height uint64 + tx *types.Transaction + }{ + { + name: "mainnet_pre_hf4_v0", + forks: config.MainnetForks, + height: 100, + tx: &types.Transaction{ + Version: types.VersionInitial, + Vin: []types.TxInput{types.TxInputGenesis{Height: 100}}, + Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}}, + }, + }, + { + name: "testnet_post_hf4_v1", + forks: config.TestnetForks, + height: 101, + tx: &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{types.TxInputGenesis{Height: 101}}, + Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}}, + }, + }, + { + name: "testnet_post_hf5_wrong_hardfork_id", + forks: config.TestnetForks, + height: 201, + tx: &types.Transaction{ + Version: types.VersionPostHF5, + HardforkID: config.HF4Zarcanum, + Vin: []types.TxInput{types.TxInputGenesis{Height: 201}}, + Vout: []types.TxOutput{types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateMinerTx(tt.tx, tt.height, tt.forks) + assert.ErrorIs(t, err, ErrMinerTxVersion) + }) + } +} + func TestValidateBlockReward_Good(t *testing.T) { height := uint64(100) tx := validMinerTx(height) @@ -127,7 +228,7 @@ func TestValidateBlockReward_Good(t *testing.T) { func TestValidateBlockReward_Bad_TooMuch(t *testing.T) { height := uint64(100) tx := &types.Transaction{ - Version: types.VersionInitial, + Version: types.VersionPreHF4, Vin: []types.TxInput{types.TxInputGenesis{Height: height}}, Vout: []types.TxOutput{ types.TxOutputBare{Amount: config.BlockReward + 1, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, @@ -141,7 +242,7 @@ func TestValidateBlockReward_Good_WithFees(t *testing.T) { height := uint64(100) fees := uint64(50_000_000_000) tx := &types.Transaction{ - Version: types.VersionInitial, + Version: types.VersionPreHF4, Vin: []types.TxInput{types.TxInputGenesis{Height: height}}, Vout: []types.TxOutput{ types.TxOutputBare{Amount: config.BlockReward + fees, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, @@ -423,7 +524,7 @@ func TestValidateBlock_MajorVersion_Good(t *testing.T) { Timestamp: now, Flags: 0, }, - MinerTx: *validMinerTx(tt.height), + MinerTx: *validMinerTxForForks(tt.height, tt.forks), } err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks) require.NoError(t, err) @@ -457,7 +558,7 @@ func TestValidateBlock_MajorVersion_Bad(t *testing.T) { Timestamp: now, Flags: 0, }, - MinerTx: *validMinerTx(tt.height), + MinerTx: *validMinerTxForForks(tt.height, tt.forks), } err := ValidateBlock(blk, tt.height, 1000, config.BlockGrantedFullRewardZone, 0, now, nil, tt.forks) assert.ErrorIs(t, err, ErrBlockMajorVersion) diff --git a/consensus/errors.go b/consensus/errors.go index 181feb4..3654522 100644 --- a/consensus/errors.go +++ b/consensus/errors.go @@ -29,15 +29,16 @@ var ( ErrNegativeFee = errors.New("consensus: outputs exceed inputs") // Block errors. - 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") + 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") + ErrMinerTxVersion = errors.New("consensus: invalid miner transaction version for current hardfork") + 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") // ErrBlockVersion is an alias for ErrBlockMajorVersion, used by // checkBlockVersion when the block major version does not match