From 3ee066e233bd8abab86f125f0781b49090b2c293 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 00:51:56 +0000 Subject: [PATCH] feat(consensus): miner transaction validation ValidateMinerTx checks genesis input height, input count (1 for PoW, 2 for PoS), and stake input type per hardfork version. Co-Authored-By: Charon --- consensus/block.go | 40 ++++++++++++++++++++++++++++++ consensus/block_test.go | 54 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/consensus/block.go b/consensus/block.go index bbf131f..933898c 100644 --- a/consensus/block.go +++ b/consensus/block.go @@ -10,6 +10,7 @@ import ( "sort" "forge.lthn.ai/core/go-blockchain/config" + "forge.lthn.ai/core/go-blockchain/types" ) // IsPoS returns true if the block flags indicate a Proof-of-Stake block. @@ -57,3 +58,42 @@ func medianTimestamp(timestamps []uint64) uint64 { } return sorted[n/2] } + +// ValidateMinerTx checks the structure of a coinbase (miner) transaction. +// For PoW blocks: exactly 1 input (TxInputGenesis). For PoS blocks: exactly +// 2 inputs (TxInputGenesis + stake input). +func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFork) error { + if len(tx.Vin) == 0 { + return fmt.Errorf("%w: no inputs", ErrMinerTxInputs) + } + + // First input must be TxInputGenesis. + gen, ok := tx.Vin[0].(types.TxInputGenesis) + if !ok { + return fmt.Errorf("%w: first input is not txin_gen", ErrMinerTxInputs) + } + if gen.Height != height { + return fmt.Errorf("%w: got %d, expected %d", ErrMinerTxHeight, gen.Height, height) + } + + // PoW blocks: exactly 1 input. PoS: exactly 2. + if len(tx.Vin) == 1 { + // PoW — valid. + } else if len(tx.Vin) == 2 { + // PoS — second input must be a spend input. + switch tx.Vin[1].(type) { + case types.TxInputToKey: + // Pre-HF4 PoS. + default: + hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height) + if !hf4Active { + return fmt.Errorf("%w: invalid PoS stake input type", ErrMinerTxInputs) + } + // Post-HF4: accept ZC inputs. + } + } else { + return fmt.Errorf("%w: %d inputs (expected 1 or 2)", ErrMinerTxInputs, len(tx.Vin)) + } + + return nil +} diff --git a/consensus/block_test.go b/consensus/block_test.go index 2eb136a..282b23b 100644 --- a/consensus/block_test.go +++ b/consensus/block_test.go @@ -7,6 +7,7 @@ import ( "time" "forge.lthn.ai/core/go-blockchain/config" + "forge.lthn.ai/core/go-blockchain/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -62,3 +63,56 @@ func TestCheckTimestamp_Ugly(t *testing.T) { err := CheckTimestamp(now-200, 0, now, timestamps) // old but under 60 entries require.NoError(t, err) } + +func validMinerTx(height uint64) *types.Transaction { + return &types.Transaction{ + Version: types.VersionInitial, + Vin: []types.TxInput{types.TxInputGenesis{Height: height}}, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, + }, + } +} + +func TestValidateMinerTx_Good(t *testing.T) { + tx := validMinerTx(100) + err := ValidateMinerTx(tx, 100, config.MainnetForks) + require.NoError(t, err) +} + +func TestValidateMinerTx_Bad_WrongHeight(t *testing.T) { + tx := validMinerTx(100) + err := ValidateMinerTx(tx, 200, config.MainnetForks) // height mismatch + assert.ErrorIs(t, err, ErrMinerTxHeight) +} + +func TestValidateMinerTx_Bad_NoInputs(t *testing.T) { + tx := &types.Transaction{Version: types.VersionInitial} + err := ValidateMinerTx(tx, 100, config.MainnetForks) + assert.ErrorIs(t, err, ErrMinerTxInputs) +} + +func TestValidateMinerTx_Bad_WrongFirstInput(t *testing.T) { + tx := &types.Transaction{ + Version: types.VersionInitial, + Vin: []types.TxInput{types.TxInputToKey{Amount: 1}}, + } + err := ValidateMinerTx(tx, 100, config.MainnetForks) + assert.ErrorIs(t, err, ErrMinerTxInputs) +} + +func TestValidateMinerTx_Good_PoS(t *testing.T) { + tx := &types.Transaction{ + Version: types.VersionInitial, + Vin: []types.TxInput{ + types.TxInputGenesis{Height: 100}, + types.TxInputToKey{Amount: 1}, // PoS stake input + }, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: config.BlockReward, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, + }, + } + err := ValidateMinerTx(tx, 100, config.MainnetForks) + // 2 inputs with genesis + TxInputToKey is valid PoS structure. + require.NoError(t, err) +}