fix(consensus): enforce tx versions across fork eras
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run

Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
Virgil 2026-04-04 20:04:24 +00:00
parent be99c5e93a
commit c1b68523c6
2 changed files with 43 additions and 13 deletions

View file

@ -75,26 +75,32 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
// checkTxVersion validates that the transaction version is appropriate for the
// current hardfork era.
//
// After HF5: transaction version must be exactly VersionPostHF5 (3) and the
// embedded hardfork_id must match the active hardfork version.
// Before HF5: transaction version 3 is rejected (too early).
// Pre-HF4: regular transactions must use version 1.
// HF4 era: regular transactions must use version 2.
// HF5+: transaction version must be exactly version 3 and the embedded
// hardfork_id must match the active hardfork version.
func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint64) error {
hf5Active := config.IsHardForkActive(forks, config.HF5, height)
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
currentFork := config.VersionAtHeight(forks, height)
if hf5Active && tx.Version != types.VersionPostHF5 {
var expectedVersion uint64
switch {
case hf5Active:
expectedVersion = types.VersionPostHF5
case hf4Active:
expectedVersion = types.VersionPostHF4
default:
expectedVersion = types.VersionPreHF4
}
if tx.Version != expectedVersion {
return coreerr.E("checkTxVersion",
fmt.Sprintf("version %d invalid after HF5 at height %d", tx.Version, height),
fmt.Sprintf("version %d invalid at height %d (expected %d)", tx.Version, height, expectedVersion),
ErrTxVersionInvalid)
}
if !hf5Active && tx.Version >= types.VersionPostHF5 {
return coreerr.E("checkTxVersion",
fmt.Sprintf("version %d not allowed before HF5 at height %d", tx.Version, height),
ErrTxVersionInvalid)
}
if hf5Active && tx.HardforkID != currentFork {
if tx.Version >= types.VersionPostHF5 && tx.HardforkID != currentFork {
return coreerr.E("checkTxVersion",
fmt.Sprintf("hardfork id %d does not match active fork %d at height %d", tx.HardforkID, currentFork, height),
ErrTxVersionInvalid)

View file

@ -79,8 +79,18 @@ func TestCheckTxVersion_Bad(t *testing.T) {
forks []config.HardFork
height uint64
}{
// v0 regular transaction before HF4 — must still be v1.
{"v0_before_hf4", func() *types.Transaction {
tx := validV1Tx()
tx.Version = types.VersionInitial
return tx
}(), config.MainnetForks, 5000},
// v1 transaction after HF4 — must be v2.
{"v1_after_hf4", validV1Tx(), config.TestnetForks, 150},
// v2 transaction after HF5 — must be v3.
{"v2_after_hf5", validV2Tx(), config.TestnetForks, 250},
// v3 transaction after HF4 but before HF5 — too early.
{"v3_after_hf4_before_hf5", validV3Tx(), config.TestnetForks, 150},
// v3 transaction after HF5 with wrong hardfork id.
{"v3_after_hf5_wrong_hardfork", func() *types.Transaction {
tx := validV3Tx()
@ -108,9 +118,23 @@ func TestCheckTxVersion_Bad(t *testing.T) {
}
func TestCheckTxVersion_Ugly(t *testing.T) {
// v2 at exact HF4 activation boundary (height 101 on testnet, HF4.Height=100).
txHF4 := validV2Tx()
err := checkTxVersion(txHF4, config.TestnetForks, 101)
if err != nil {
t.Errorf("v2 at HF4 activation boundary should be valid: %v", err)
}
// v1 at exact HF4 activation boundary should be rejected.
txPreHF4 := validV1Tx()
err = checkTxVersion(txPreHF4, config.TestnetForks, 101)
if err == nil {
t.Error("v1 at HF4 activation boundary should be rejected")
}
// v3 at exact HF5 activation boundary (height 201 on testnet, HF5.Height=200).
tx := validV3Tx()
err := checkTxVersion(tx, config.TestnetForks, 201)
err = checkTxVersion(tx, config.TestnetForks, 201)
if err != nil {
t.Errorf("v3 at HF5 activation boundary should be valid: %v", err)
}