fix(consensus): tighten fork-era type gating
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run

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 <charon@lethean.io>
This commit is contained in:
Virgil 2026-04-04 22:18:53 +00:00
parent 95edac1d15
commit bb941ebcc5
4 changed files with 86 additions and 5 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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,