fix(wire): reject unsupported transaction variants
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run

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 <charon@lethean.io>
This commit is contained in:
Virgil 2026-04-04 19:56:50 +00:00
parent d2caf68d94
commit be99c5e93a
4 changed files with 109 additions and 1 deletions

View file

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

View file

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

View file

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

View file

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