feat(consensus): enforce transaction version 3 after HF5
After HF5 activation, only version 3 transactions are accepted. Before HF5, version 3 is rejected. Matches C++ check_tx_semantic hardfork gating logic. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
939ad198fe
commit
efbf050c1b
3 changed files with 142 additions and 0 deletions
|
|
@ -20,6 +20,8 @@ var (
|
|||
ErrInvalidOutput = errors.New("consensus: invalid output")
|
||||
ErrDuplicateKeyImage = errors.New("consensus: duplicate key image in transaction")
|
||||
ErrInvalidExtra = errors.New("consensus: invalid extra field")
|
||||
ErrTxVersionInvalid = errors.New("consensus: invalid transaction version for current hardfork")
|
||||
ErrPreHardforkFreeze = errors.New("consensus: non-coinbase transaction rejected during pre-hardfork freeze")
|
||||
|
||||
// Transaction economic errors.
|
||||
ErrInputOverflow = errors.New("consensus: input amount overflow")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
|
|||
hf1Active := config.IsHardForkActive(forks, config.HF1, height)
|
||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
|
||||
// 0. Transaction version for current hardfork.
|
||||
if err := checkTxVersion(tx, forks, height); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. Blob size.
|
||||
if uint64(len(txBlob)) >= config.MaxTransactionBlobSize {
|
||||
return fmt.Errorf("%w: %d bytes", ErrTxTooLarge, len(txBlob))
|
||||
|
|
@ -122,6 +127,29 @@ func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// checkTxVersion validates that the transaction version is correct for the
|
||||
// current hardfork era. After HF5, version must be 3. Before HF5, version 3
|
||||
// is rejected.
|
||||
func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint64) error {
|
||||
hf5Active := config.IsHardForkActive(forks, config.HF5, height)
|
||||
|
||||
if hf5Active {
|
||||
// After HF5: must be version 3.
|
||||
if tx.Version != types.VersionPostHF5 {
|
||||
return fmt.Errorf("%w: got version %d, require %d after HF5",
|
||||
ErrTxVersionInvalid, tx.Version, types.VersionPostHF5)
|
||||
}
|
||||
} else {
|
||||
// Before HF5: version 3 is not allowed.
|
||||
if tx.Version >= types.VersionPostHF5 {
|
||||
return fmt.Errorf("%w: version %d not allowed before HF5",
|
||||
ErrTxVersionInvalid, tx.Version)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkKeyImages(tx *types.Transaction) error {
|
||||
seen := make(map[types.KeyImage]struct{})
|
||||
for _, vin := range tx.Vin {
|
||||
|
|
|
|||
112
consensus/tx_version_test.go
Normal file
112
consensus/tx_version_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||
//
|
||||
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
//go:build !integration
|
||||
|
||||
package consensus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
)
|
||||
|
||||
// validV2Tx returns a minimal valid v2 (Zarcanum) transaction for testing.
|
||||
func validV2Tx() *types.Transaction {
|
||||
return &types.Transaction{
|
||||
Version: types.VersionPostHF4,
|
||||
Vin: []types.TxInput{
|
||||
types.TxInputZC{
|
||||
KeyImage: types.KeyImage{1},
|
||||
},
|
||||
},
|
||||
Vout: []types.TxOutput{
|
||||
types.TxOutputZarcanum{StealthAddress: types.PublicKey{1}},
|
||||
types.TxOutputZarcanum{StealthAddress: types.PublicKey{2}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// validV3Tx returns a minimal valid v3 (HF5) transaction for testing.
|
||||
func validV3Tx() *types.Transaction {
|
||||
return &types.Transaction{
|
||||
Version: types.VersionPostHF5,
|
||||
HardforkID: 5,
|
||||
Vin: []types.TxInput{
|
||||
types.TxInputZC{
|
||||
KeyImage: types.KeyImage{1},
|
||||
},
|
||||
},
|
||||
Vout: []types.TxOutput{
|
||||
types.TxOutputZarcanum{StealthAddress: types.PublicKey{1}},
|
||||
types.TxOutputZarcanum{StealthAddress: types.PublicKey{2}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckTxVersion_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tx *types.Transaction
|
||||
forks []config.HardFork
|
||||
height uint64
|
||||
}{
|
||||
// v1 transaction before HF4 — valid.
|
||||
{"v1_before_hf4", validV1Tx(), config.MainnetForks, 5000},
|
||||
// v2 transaction after HF4, before HF5 — valid.
|
||||
{"v2_after_hf4_before_hf5", validV2Tx(), config.TestnetForks, 150},
|
||||
// v3 transaction after HF5 — valid.
|
||||
{"v3_after_hf5", validV3Tx(), config.TestnetForks, 250},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := checkTxVersion(tt.tx, tt.forks, tt.height)
|
||||
if err != nil {
|
||||
t.Errorf("checkTxVersion returned unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckTxVersion_Bad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tx *types.Transaction
|
||||
forks []config.HardFork
|
||||
height uint64
|
||||
}{
|
||||
// v2 transaction after HF5 — must be v3.
|
||||
{"v2_after_hf5", validV2Tx(), config.TestnetForks, 250},
|
||||
// v3 transaction before HF5 — too early.
|
||||
{"v3_before_hf5", validV3Tx(), config.TestnetForks, 150},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := checkTxVersion(tt.tx, tt.forks, tt.height)
|
||||
if err == nil {
|
||||
t.Error("expected ErrTxVersionInvalid, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckTxVersion_Ugly(t *testing.T) {
|
||||
// v3 at exact HF5 activation boundary (height 201 on testnet, HF5.Height=200).
|
||||
tx := validV3Tx()
|
||||
err := checkTxVersion(tx, config.TestnetForks, 201)
|
||||
if err != nil {
|
||||
t.Errorf("v3 at HF5 activation boundary should be valid: %v", err)
|
||||
}
|
||||
|
||||
// v2 at exact HF5 activation boundary — should be rejected.
|
||||
tx2 := validV2Tx()
|
||||
err = checkTxVersion(tx2, config.TestnetForks, 201)
|
||||
if err == nil {
|
||||
t.Error("v2 at HF5 activation boundary should be rejected")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue