// Copyright (c) 2017-2026 Lethean (https://lt.hn) // // Licensed under the European Union Public Licence (EUPL) version 1.2. // SPDX-License-Identifier: EUPL-1.2 //go:build !integration package consensus import ( "bytes" "testing" "dappco.re/go/core/blockchain/config" "dappco.re/go/core/blockchain/types" "dappco.re/go/core/blockchain/wire" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // validV1Tx returns a minimal valid v1 transaction for testing. func validV1Tx() *types.Transaction { return &types.Transaction{ Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputToKey{ Amount: 100, KeyImage: types.KeyImage{1}, }, }, Vout: []types.TxOutput{ types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, }, } } type unsupportedTxOutTarget struct{} func (unsupportedTxOutTarget) TargetType() uint8 { return 250 } func TestValidateTransaction_Good(t *testing.T) { tx := validV1Tx() blob := make([]byte, 100) // small blob err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) require.NoError(t, err) } func TestValidateTransaction_BlobTooLarge(t *testing.T) { tx := validV1Tx() blob := make([]byte, config.MaxTransactionBlobSize+1) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrTxTooLarge) } func TestValidateTransaction_NoInputs(t *testing.T) { tx := validV1Tx() tx.Vin = nil blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrNoInputs) } func TestValidateTransaction_TooManyInputs(t *testing.T) { tx := validV1Tx() tx.Vin = make([]types.TxInput, config.TxMaxAllowedInputs+1) for i := range tx.Vin { tx.Vin[i] = types.TxInputToKey{Amount: 1, KeyImage: types.KeyImage{byte(i)}} } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrTooManyInputs) } func TestValidateTransaction_InvalidInputType(t *testing.T) { tx := validV1Tx() tx.Vin = []types.TxInput{types.TxInputGenesis{Height: 1}} // genesis not allowed blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrInvalidInputType) } func TestValidateTransaction_NoOutputs(t *testing.T) { tx := validV1Tx() tx.Vout = nil blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrNoOutputs) } func TestValidateTransaction_TooManyOutputs(t *testing.T) { tx := validV1Tx() tx.Vout = make([]types.TxOutput, config.TxMaxAllowedOutputs+1) for i := range tx.Vout { tx.Vout[i] = types.TxOutputBare{Amount: 1, Target: types.TxOutToKey{Key: types.PublicKey{byte(i)}}} } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrTooManyOutputs) } func TestValidateTransaction_ZeroOutputAmount(t *testing.T) { tx := validV1Tx() tx.Vout = []types.TxOutput{ types.TxOutputBare{Amount: 0, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrInvalidOutput) } func TestValidateTransaction_DuplicateKeyImage(t *testing.T) { ki := types.KeyImage{42} tx := &types.Transaction{ Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputToKey{Amount: 100, KeyImage: ki}, types.TxInputToKey{Amount: 50, KeyImage: ki}, // duplicate }, Vout: []types.TxOutput{ types.TxOutputBare{Amount: 140, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrDuplicateKeyImage) } func TestValidateTransaction_NegativeFee(t *testing.T) { tx := &types.Transaction{ Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputToKey{Amount: 10, KeyImage: types.KeyImage{1}}, }, Vout: []types.TxOutput{ types.TxOutputBare{Amount: 100, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrNegativeFee) } // --- HF1 gating tests (Task 7) --- func TestCheckInputTypes_HTLCPreHF1_Bad(t *testing.T) { tx := &types.Transaction{ Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputHTLC{Amount: 100, KeyImage: types.KeyImage{1}}, }, Vout: []types.TxOutput{ types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) // pre-HF1 (10080) assert.ErrorIs(t, err, ErrInvalidInputType) } func TestCheckInputTypes_HTLCPostHF1_Good(t *testing.T) { tx := &types.Transaction{ Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputHTLC{ Amount: 100, KeyImage: types.KeyImage{1}, }, }, Vout: []types.TxOutput{ types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) // post-HF1 require.NoError(t, err) } func TestCheckInputTypes_MultisigPreHF1_Bad(t *testing.T) { tx := &types.Transaction{ Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputMultisig{Amount: 100}, }, Vout: []types.TxOutput{ types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrInvalidInputType) } func TestCheckOutputs_HTLCTargetPreHF1_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: types.TxOutHTLC{Expiration: 20000}, }, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrInvalidOutput) } func TestCheckOutputs_MultisigTargetPreHF1_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: types.TxOutMultisig{MinimumSigs: 2, Keys: []types.PublicKey{{1}, {2}}}, }, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrInvalidOutput) } func TestCheckOutputs_MultisigTargetPostHF1_Good(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: types.TxOutMultisig{MinimumSigs: 2, Keys: []types.PublicKey{{1}, {2}}}, }, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) // post-HF1 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, 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) } func assetDescriptorExtraBlob(ticker string, ownerZero bool) []byte { var buf bytes.Buffer enc := wire.NewEncoder(&buf) enc.WriteVarint(1) enc.WriteUint8(types.AssetDescriptorOperationTag) assetOp := bytes.Buffer{} opEnc := wire.NewEncoder(&assetOp) opEnc.WriteUint8(1) // version opEnc.WriteUint8(types.AssetOpRegister) opEnc.WriteUint8(0) // no asset id opEnc.WriteUint8(1) // descriptor present opEnc.WriteVarint(uint64(len(ticker))) opEnc.WriteBytes([]byte(ticker)) opEnc.WriteVarint(7) opEnc.WriteBytes([]byte("Lethean")) opEnc.WriteUint64LE(1000000) opEnc.WriteUint64LE(0) opEnc.WriteUint8(12) opEnc.WriteVarint(0) if ownerZero { opEnc.WriteBytes(make([]byte, 32)) } else { opEnc.WriteBytes(bytes.Repeat([]byte{0xAA}, 32)) } opEnc.WriteVarint(0) opEnc.WriteUint64LE(0) opEnc.WriteUint64LE(0) opEnc.WriteVarint(0) enc.WriteBytes(assetOp.Bytes()) return buf.Bytes() } func TestValidateTransaction_AssetDescriptorOperation_Good(t *testing.T) { tx := &types.Transaction{ Version: types.VersionPostHF5, 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}}, }, }, Extra: assetDescriptorExtraBlob("LTHN", false), } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.TestnetForks, 250) require.NoError(t, err) } func TestValidateTransaction_AssetDescriptorOperationPreHF5_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: types.TxOutToKey{Key: types.PublicKey{1}}}, }, Extra: assetDescriptorExtraBlob("LTHN", false), } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 5000) assert.ErrorIs(t, err, ErrInvalidExtra) } func TestValidateTransaction_AssetDescriptorOperationInvalid_Bad(t *testing.T) { tx := &types.Transaction{ Version: types.VersionPostHF5, 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}}}, }, Extra: assetDescriptorExtraBlob("TOO-LONG", true), } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.TestnetForks, 250) assert.ErrorIs(t, err, ErrInvalidExtra) } // --- Key image tests for HTLC (Task 8) --- func TestCheckKeyImages_HTLCDuplicate_Bad(t *testing.T) { ki := types.KeyImage{0x42} tx := &types.Transaction{ Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputHTLC{Amount: 100, KeyImage: ki}, types.TxInputHTLC{Amount: 50, KeyImage: ki}, // duplicate }, Vout: []types.TxOutput{ types.TxOutputBare{Amount: 140, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) // post-HF1 assert.ErrorIs(t, err, ErrDuplicateKeyImage) } func TestCheckKeyImages_HTLCAndToKeyDuplicate_Bad(t *testing.T) { ki := types.KeyImage{0x42} tx := &types.Transaction{ Version: types.VersionPreHF4, Vin: []types.TxInput{ types.TxInputToKey{Amount: 100, KeyImage: ki}, types.TxInputHTLC{Amount: 50, KeyImage: ki}, // duplicate across types }, Vout: []types.TxOutput{ types.TxOutputBare{Amount: 140, Target: types.TxOutToKey{Key: types.PublicKey{1}}}, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) assert.ErrorIs(t, err, ErrDuplicateKeyImage) } func TestCheckOutputs_HTLCTargetPostHF1_Good(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: types.TxOutHTLC{Expiration: 20000}, }, }, } blob := make([]byte, 100) err := ValidateTransaction(tx, blob, config.MainnetForks, 20000) // post-HF1 require.NoError(t, err) }