From bb941ebcc510e6d6768346dbfbced8a5f75a0ccd Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 22:18:53 +0000 Subject: [PATCH] fix(consensus): tighten fork-era type gating Reject Zarcanum inputs and outputs before HF4 instead of letting unsupported combinations pass semantic validation. Also restrict PoS miner stake inputs to txin_to_key pre-HF4 and txin_zc post-HF4, with regression coverage for the affected paths. Co-Authored-By: Charon --- consensus/block.go | 5 +++-- consensus/block_test.go | 30 ++++++++++++++++++++++++++ consensus/tx.go | 9 +++++--- consensus/tx_test.go | 47 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/consensus/block.go b/consensus/block.go index a1787c9..50602a2 100644 --- a/consensus/block.go +++ b/consensus/block.go @@ -112,12 +112,13 @@ func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFo switch tx.Vin[1].(type) { case types.TxInputToKey: // Pre-HF4 PoS. - default: + case types.TxInputZC: hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height) if !hf4Active { return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs) } - // Post-HF4: accept ZC inputs. + default: + return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs) } } else { return coreerr.E("ValidateMinerTx", fmt.Sprintf("%d inputs (expected 1 or 2)", len(tx.Vin)), ErrMinerTxInputs) diff --git a/consensus/block_test.go b/consensus/block_test.go index aba41a0..3d7a4a6 100644 --- a/consensus/block_test.go +++ b/consensus/block_test.go @@ -126,6 +126,36 @@ func TestValidateMinerTx_Good_PoS(t *testing.T) { require.NoError(t, err) } +func TestValidateMinerTx_Good_PoS_ZCAfterHF4(t *testing.T) { + tx := &types.Transaction{ + Version: types.VersionPostHF4, + Vin: []types.TxInput{ + types.TxInputGenesis{Height: 101}, + types.TxInputZC{KeyImage: types.KeyImage{1}}, + }, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, + }, + } + err := ValidateMinerTx(tx, 101, config.TestnetForks) + require.NoError(t, err) +} + +func TestValidateMinerTx_Bad_PoS_UnsupportedStakeInput(t *testing.T) { + tx := &types.Transaction{ + Version: types.VersionPostHF4, + Vin: []types.TxInput{ + types.TxInputGenesis{Height: 101}, + types.TxInputHTLC{Amount: 1, KeyImage: types.KeyImage{1}}, + }, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, + }, + } + err := ValidateMinerTx(tx, 101, config.TestnetForks) + assert.ErrorIs(t, err, ErrMinerTxInputs) +} + func TestValidateMinerTx_Version_Good(t *testing.T) { tests := []struct { name string diff --git a/consensus/tx.go b/consensus/tx.go index f12d4ff..cfac3a1 100644 --- a/consensus/tx.go +++ b/consensus/tx.go @@ -128,11 +128,12 @@ func checkInputTypes(tx *types.Transaction, hf1Active, hf4Active bool) error { if !hf1Active { return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF1", vin.InputType()), ErrInvalidInputType) } - default: - // Future types (ZC) — accept if HF4+. + case types.TxInputZC: if !hf4Active { return coreerr.E("checkInputTypes", fmt.Sprintf("tag %d pre-HF4", vin.InputType()), ErrInvalidInputType) } + default: + return coreerr.E("checkInputTypes", fmt.Sprintf("unsupported input type %T", vin), ErrInvalidInputType) } } return nil @@ -170,7 +171,9 @@ func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error { return coreerr.E("checkOutputs", fmt.Sprintf("output %d: unsupported target %T", i, o.Target), ErrInvalidOutput) } case types.TxOutputZarcanum: - // Validated by proof verification. + if !hf4Active { + return coreerr.E("checkOutputs", fmt.Sprintf("output %d: Zarcanum output pre-HF4", i), ErrInvalidOutput) + } default: return coreerr.E("checkOutputs", fmt.Sprintf("output %d: unsupported output type %T", i, vout), ErrInvalidOutput) } diff --git a/consensus/tx_test.go b/consensus/tx_test.go index d336a55..c89520b 100644 --- a/consensus/tx_test.go +++ b/consensus/tx_test.go @@ -244,6 +244,53 @@ func TestCheckOutputs_MultisigTargetPostHF1_Good(t *testing.T) { require.NoError(t, err) } +func TestCheckInputTypes_ZCPreHF4_Bad(t *testing.T) { + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputZC{KeyImage: types.KeyImage{1}}, + }, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, + types.TxOutputBare{Amount: 1, Target: types.TxOutToKey{Key: types.PublicKey{2}}}, + }, + } + blob := make([]byte, 100) + err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) + assert.ErrorIs(t, err, ErrInvalidInputType) +} + +func TestCheckOutputs_ZarcanumPreHF4_Bad(t *testing.T) { + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}}, + }, + Vout: []types.TxOutput{ + types.TxOutputZarcanum{StealthAddress: types.PublicKey{1}}, + }, + } + blob := make([]byte, 100) + err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) + assert.ErrorIs(t, err, ErrInvalidOutput) +} + +func TestCheckOutputs_ZarcanumPostHF4_Good(t *testing.T) { + tx := &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}}, + }, + } + blob := make([]byte, 100) + err := ValidateTransaction(tx, blob, config.TestnetForks, 150) + require.NoError(t, err) +} + func TestCheckOutputs_MissingTarget_Bad(t *testing.T) { tx := &types.Transaction{ Version: types.VersionPreHF4,