refactor(consensus): centralise validation fork state
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 22:38:20 +00:00
parent b7428496bd
commit 99720fff5e
2 changed files with 40 additions and 31 deletions

View file

@ -15,14 +15,29 @@ import (
"dappco.re/go/core/blockchain/wire" "dappco.re/go/core/blockchain/wire"
) )
type txForkState struct {
currentFork uint8
hf1Active bool
hf4Active bool
hf5Active bool
}
func newTxForkState(forks []config.HardFork, height uint64) txForkState {
return txForkState{
currentFork: config.VersionAtHeight(forks, height),
hf1Active: config.IsHardForkActive(forks, config.HF1, height),
hf4Active: config.IsHardForkActive(forks, config.HF4Zarcanum, height),
hf5Active: config.IsHardForkActive(forks, config.HF5, height),
}
}
// ValidateTransaction performs semantic validation on a regular (non-coinbase) // ValidateTransaction performs semantic validation on a regular (non-coinbase)
// transaction. Checks are ordered to match the C++ validate_tx_semantic(). // transaction. Checks are ordered to match the C++ validate_tx_semantic().
func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error { func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error {
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height) state := newTxForkState(forks, height)
hf5Active := config.IsHardForkActive(forks, config.HF5, height)
// 0. Transaction version. // 0. Transaction version.
if err := checkTxVersion(tx, forks, height); err != nil { if err := checkTxVersion(tx, state, height); err != nil {
return err return err
} }
@ -39,20 +54,18 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
return coreerr.E("ValidateTransaction", fmt.Sprintf("%d", len(tx.Vin)), ErrTooManyInputs) return coreerr.E("ValidateTransaction", fmt.Sprintf("%d", len(tx.Vin)), ErrTooManyInputs)
} }
hf1Active := config.IsHardForkActive(forks, config.HF1, height)
// 3. Input types — TxInputGenesis not allowed in regular transactions. // 3. Input types — TxInputGenesis not allowed in regular transactions.
if err := checkInputTypes(tx, hf1Active, hf4Active); err != nil { if err := checkInputTypes(tx, state); err != nil {
return err return err
} }
// 4. Output validation. // 4. Output validation.
if err := checkOutputs(tx, hf1Active, hf4Active); err != nil { if err := checkOutputs(tx, state); err != nil {
return err return err
} }
// 4a. HF5 asset operation validation inside extra. // 4a. HF5 asset operation validation inside extra.
if err := checkAssetOperations(tx.Extra, hf5Active); err != nil { if err := checkAssetOperations(tx.Extra, state.hf5Active); err != nil {
return err return err
} }
@ -70,7 +83,7 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
} }
// 7. Balance check (pre-HF4 only — post-HF4 uses commitment proofs). // 7. Balance check (pre-HF4 only — post-HF4 uses commitment proofs).
if !hf4Active { if !state.hf4Active {
if _, err := TxFee(tx); err != nil { if _, err := TxFee(tx); err != nil {
return err return err
} }
@ -86,16 +99,12 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
// HF4 era: regular transactions must use version 2. // HF4 era: regular transactions must use version 2.
// HF5+: transaction version must be exactly version 3 and the embedded // HF5+: transaction version must be exactly version 3 and the embedded
// hardfork_id must match the active hardfork version. // hardfork_id must match the active hardfork version.
func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint64) error { func checkTxVersion(tx *types.Transaction, state txForkState, height uint64) error {
hf5Active := config.IsHardForkActive(forks, config.HF5, height)
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
currentFork := config.VersionAtHeight(forks, height)
var expectedVersion uint64 var expectedVersion uint64
switch { switch {
case hf5Active: case state.hf5Active:
expectedVersion = types.VersionPostHF5 expectedVersion = types.VersionPostHF5
case hf4Active: case state.hf4Active:
expectedVersion = types.VersionPostHF4 expectedVersion = types.VersionPostHF4
default: default:
expectedVersion = types.VersionPreHF4 expectedVersion = types.VersionPreHF4
@ -107,16 +116,16 @@ func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint6
ErrTxVersionInvalid) ErrTxVersionInvalid)
} }
if tx.Version >= types.VersionPostHF5 && tx.HardforkID != currentFork { if tx.Version >= types.VersionPostHF5 && tx.HardforkID != state.currentFork {
return coreerr.E("checkTxVersion", return coreerr.E("checkTxVersion",
fmt.Sprintf("hardfork id %d does not match active fork %d at height %d", tx.HardforkID, currentFork, height), fmt.Sprintf("hardfork id %d does not match active fork %d at height %d", tx.HardforkID, state.currentFork, height),
ErrTxVersionInvalid) ErrTxVersionInvalid)
} }
return nil return nil
} }
func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error { func checkInputTypes(tx *types.Transaction, state txForkState) error {
for _, vin := range tx.Vin { for _, vin := range tx.Vin {
switch vin.(type) { switch vin.(type) {
case types.TxInputToKey: case types.TxInputToKey:
@ -125,11 +134,11 @@ func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error {
return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType) return coreerr.E("checkInputTypes", "txin_gen in regular transaction", ErrInvalidInputType)
case types.TxInputHTLC, types.TxInputMultisig: case types.TxInputHTLC, types.TxInputMultisig:
// HTLC and multisig inputs require at least HF1. // HTLC and multisig inputs require at least HF1.
if !hf1Active { if !state.hf1Active {
return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF1", vin.InputType()), ErrInvalidInputType) return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF1", vin.InputType()), ErrInvalidInputType)
} }
case types.TxInputZC: case types.TxInputZC:
if !hf4Active { if !state.hf4Active {
return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF4", vin.InputType()), ErrInvalidInputType) return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF4", vin.InputType()), ErrInvalidInputType)
} }
default: default:
@ -139,12 +148,12 @@ func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error {
return nil return nil
} }
func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error { func checkOutputs(tx *types.Transaction, state txForkState) error {
if len(tx.Vout) == 0 { if len(tx.Vout) == 0 {
return ErrNoOutputs return ErrNoOutputs
} }
if hf4Active && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs { if state.hf4Active && uint64(len(tx.Vout)) < config.TxMinAllowedOutputs {
return coreerr.E("checkOutputs", fmt.Sprintf("%d (min %d)", len(tx.Vout), config.TxMinAllowedOutputs), ErrTooFewOutputs) return coreerr.E("checkOutputs", fmt.Sprintf("%d (min %d)", len(tx.Vout), config.TxMinAllowedOutputs), ErrTooFewOutputs)
} }
@ -162,7 +171,7 @@ func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error {
switch o.Target.(type) { switch o.Target.(type) {
case types.TxOutToKey: case types.TxOutToKey:
case types.TxOutHTLC, types.TxOutMultisig: case types.TxOutHTLC, types.TxOutMultisig:
if !hf1Active { if !state.hf1Active {
return coreerr.E("checkOutputs", fmt.Sprintf("output %d: HTLC/multisig target pre-HF1", i), ErrInvalidOutput) return coreerr.E("checkOutputs", fmt.Sprintf("output %d: HTLC/multisig target pre-HF1", i), ErrInvalidOutput)
} }
case nil: case nil:
@ -171,7 +180,7 @@ func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error {
return coreerr.E("checkOutputs", fmt.Sprintf("output %d: unsupported target %T", i, o.Target), ErrInvalidOutput) return coreerr.E("checkOutputs", fmt.Sprintf("output %d: unsupported target %T", i, o.Target), ErrInvalidOutput)
} }
case types.TxOutputZarcanum: case types.TxOutputZarcanum:
if !hf4Active { if !state.hf4Active {
return coreerr.E("checkOutputs", fmt.Sprintf("output %d: Zarcanum output pre-HF4", i), ErrInvalidOutput) return coreerr.E("checkOutputs", fmt.Sprintf("output %d: Zarcanum output pre-HF4", i), ErrInvalidOutput)
} }
default: default:

View file

@ -64,7 +64,7 @@ func TestCheckTxVersion_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) {
err := checkTxVersion(tt.tx, tt.forks, tt.height) err := checkTxVersion(tt.tx, newTxForkState(tt.forks, tt.height), tt.height)
if err != nil { if err != nil {
t.Errorf("checkTxVersion returned unexpected error: %v", err) t.Errorf("checkTxVersion returned unexpected error: %v", err)
} }
@ -109,7 +109,7 @@ func TestCheckTxVersion_Bad(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) {
err := checkTxVersion(tt.tx, tt.forks, tt.height) err := checkTxVersion(tt.tx, newTxForkState(tt.forks, tt.height), tt.height)
if err == nil { if err == nil {
t.Error("expected ErrTxVersionInvalid, got nil") t.Error("expected ErrTxVersionInvalid, got nil")
} }
@ -120,28 +120,28 @@ func TestCheckTxVersion_Bad(t *testing.T) {
func TestCheckTxVersion_Ugly(t *testing.T) { func TestCheckTxVersion_Ugly(t *testing.T) {
// v2 at exact HF4 activation boundary (height 101 on testnet, HF4.Height=100). // v2 at exact HF4 activation boundary (height 101 on testnet, HF4.Height=100).
txHF4 := validV2Tx() txHF4 := validV2Tx()
err := checkTxVersion(txHF4, config.TestnetForks, 101) err := checkTxVersion(txHF4, newTxForkState(config.TestnetForks, 101), 101)
if err != nil { if err != nil {
t.Errorf("v2 at HF4 activation boundary should be valid: %v", err) t.Errorf("v2 at HF4 activation boundary should be valid: %v", err)
} }
// v1 at exact HF4 activation boundary should be rejected. // v1 at exact HF4 activation boundary should be rejected.
txPreHF4 := validV1Tx() txPreHF4 := validV1Tx()
err = checkTxVersion(txPreHF4, config.TestnetForks, 101) err = checkTxVersion(txPreHF4, newTxForkState(config.TestnetForks, 101), 101)
if err == nil { if err == nil {
t.Error("v1 at HF4 activation boundary should be rejected") t.Error("v1 at HF4 activation boundary should be rejected")
} }
// v3 at exact HF5 activation boundary (height 201 on testnet, HF5.Height=200). // v3 at exact HF5 activation boundary (height 201 on testnet, HF5.Height=200).
tx := validV3Tx() tx := validV3Tx()
err = checkTxVersion(tx, config.TestnetForks, 201) err = checkTxVersion(tx, newTxForkState(config.TestnetForks, 201), 201)
if err != nil { if err != nil {
t.Errorf("v3 at HF5 activation boundary should be valid: %v", err) t.Errorf("v3 at HF5 activation boundary should be valid: %v", err)
} }
// v2 at exact HF5 activation boundary — should be rejected. // v2 at exact HF5 activation boundary — should be rejected.
tx2 := validV2Tx() tx2 := validV2Tx()
err = checkTxVersion(tx2, config.TestnetForks, 201) err = checkTxVersion(tx2, newTxForkState(config.TestnetForks, 201), 201)
if err == nil { if err == nil {
t.Error("v2 at HF5 activation boundary should be rejected") t.Error("v2 at HF5 activation boundary should be rejected")
} }