From be99c5e93a485b06ec99d46e38de97b12bd44d73 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 19:56:50 +0000 Subject: [PATCH] fix(wire): reject unsupported transaction variants Add explicit errors for unknown input/output variants in the wire encoder and tighten transparent output validation in consensus. Cover the new failure paths with unit tests. Co-Authored-By: Charon --- consensus/tx.go | 9 ++++++- consensus/tx_test.go | 34 +++++++++++++++++++++++ wire/transaction.go | 9 +++++++ wire/transaction_test.go | 58 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) diff --git a/consensus/tx.go b/consensus/tx.go index 2f80ce5..1584666 100644 --- a/consensus/tx.go +++ b/consensus/tx.go @@ -144,15 +144,22 @@ func checkOutputs(tx *types.Transaction, hf1Active, hf4Active bool) error { if o.Amount == 0 { return coreerr.E("checkOutputs", fmt.Sprintf("output %d has zero amount", i), ErrInvalidOutput) } - // HTLC and Multisig output targets require at least HF1. + // Only known transparent output targets are accepted. switch o.Target.(type) { + case types.TxOutToKey: case types.TxOutHTLC, types.TxOutMultisig: if !hf1Active { return coreerr.E("checkOutputs", fmt.Sprintf("output %d: HTLC/multisig target pre-HF1", i), ErrInvalidOutput) } + case nil: + return coreerr.E("checkOutputs", fmt.Sprintf("output %d: missing target", i), ErrInvalidOutput) + default: + return coreerr.E("checkOutputs", fmt.Sprintf("output %d: unsupported target %T", i, o.Target), ErrInvalidOutput) } case types.TxOutputZarcanum: // Validated by proof verification. + 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 2ea2e40..fc6595e 100644 --- a/consensus/tx_test.go +++ b/consensus/tx_test.go @@ -32,6 +32,10 @@ func validV1Tx() *types.Transaction { } } +type unsupportedTxOutTarget struct{} + +func (unsupportedTxOutTarget) TargetType() uint8 { return 250 } + func TestValidateTransaction_Good(t *testing.T) { tx := validV1Tx() blob := make([]byte, 100) // small blob @@ -238,6 +242,36 @@ func TestCheckOutputs_MultisigTargetPostHF1_Good(t *testing.T) { require.NoError(t, err) } +func TestCheckOutputs_MissingTarget_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.TxOutputBare{Amount: 90, Target: nil}, + }, + } + blob := make([]byte, 100) + err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) + assert.ErrorIs(t, err, ErrInvalidOutput) +} + +func TestCheckOutputs_UnsupportedTarget_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.TxOutputBare{Amount: 90, Target: unsupportedTxOutTarget{}}, + }, + } + blob := make([]byte, 100) + err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) + assert.ErrorIs(t, err, ErrInvalidOutput) +} + // --- Key image tests for HTLC (Task 8) --- func TestCheckKeyImages_HTLCDuplicate_Bad(t *testing.T) { diff --git a/wire/transaction.go b/wire/transaction.go index 8a2720a..82d3a3f 100644 --- a/wire/transaction.go +++ b/wire/transaction.go @@ -176,6 +176,9 @@ func encodeInputs(enc *Encoder, vin []types.TxInput) { encodeKeyOffsets(enc, v.KeyOffsets) enc.WriteBlob32((*[32]byte)(&v.KeyImage)) enc.WriteBytes(v.EtcDetails) + default: + enc.err = coreerr.E("encodeInputs", fmt.Sprintf("wire: unsupported input type %T", in), nil) + return } } } @@ -298,6 +301,9 @@ func encodeOutputsV1(enc *Encoder, vout []types.TxOutput) { enc.WriteVarint(t.Expiration) enc.WriteBlob32((*[32]byte)(&t.PKRedeem)) enc.WriteBlob32((*[32]byte)(&t.PKRefund)) + default: + enc.err = coreerr.E("encodeOutputsV1", fmt.Sprintf("wire: unsupported output target type %T", v.Target), nil) + return } } } @@ -375,6 +381,9 @@ func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) { enc.WriteVarint(t.Expiration) enc.WriteBlob32((*[32]byte)(&t.PKRedeem)) enc.WriteBlob32((*[32]byte)(&t.PKRefund)) + default: + enc.err = coreerr.E("encodeOutputsV2", fmt.Sprintf("wire: unsupported output target type %T", v.Target), nil) + return } case types.TxOutputZarcanum: enc.WriteBlob32((*[32]byte)(&v.StealthAddress)) diff --git a/wire/transaction_test.go b/wire/transaction_test.go index 0ca4ae1..2981675 100644 --- a/wire/transaction_test.go +++ b/wire/transaction_test.go @@ -987,3 +987,61 @@ func TestHTLCTargetV2RoundTrip_Good(t *testing.T) { t.Errorf("round-trip mismatch:\n got: %x\n want: %x", rtBuf.Bytes(), buf.Bytes()) } } + +type unsupportedTxInput struct{} + +func (unsupportedTxInput) InputType() uint8 { return 250 } + +type unsupportedTxOutTarget struct{} + +func (unsupportedTxOutTarget) TargetType() uint8 { return 250 } + +func TestEncodeTransaction_UnsupportedInput_Bad(t *testing.T) { + tx := types.Transaction{ + Version: 1, + Vin: []types.TxInput{unsupportedTxInput{}}, + Vout: []types.TxOutput{types.TxOutputBare{ + Amount: 1, + Target: types.TxOutToKey{Key: types.PublicKey{1}}, + }}, + Extra: EncodeVarint(0), + } + + var buf bytes.Buffer + enc := NewEncoder(&buf) + EncodeTransactionPrefix(enc, &tx) + if enc.Err() == nil { + t.Fatal("expected encode error for unsupported input type") + } +} + +func TestEncodeTransaction_UnsupportedOutputTarget_Bad(t *testing.T) { + tests := []struct { + name string + version uint64 + }{ + {name: "v1", version: types.VersionPreHF4}, + {name: "v2", version: types.VersionPostHF4}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tx := types.Transaction{ + Version: tt.version, + Vin: []types.TxInput{types.TxInputGenesis{Height: 1}}, + Vout: []types.TxOutput{types.TxOutputBare{ + Amount: 1, + Target: unsupportedTxOutTarget{}, + }}, + Extra: EncodeVarint(0), + } + + var buf bytes.Buffer + enc := NewEncoder(&buf) + EncodeTransactionPrefix(enc, &tx) + if enc.Err() == nil { + t.Fatal("expected encode error for unsupported output target type") + } + }) + } +}