// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package fee import ( "encoding/hex" "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" "testing" "github.com/stretchr/testify/require" "github.com/luxfi/crypto/secp256k1" "github.com/luxfi/ids" "github.com/luxfi/node/vms/components/gas" "github.com/luxfi/node/vms/components/verify" "github.com/luxfi/node/vms/pcodecs" "github.com/luxfi/node/vms/platformvm/fx" "github.com/luxfi/node/vms/platformvm/signer" "github.com/luxfi/node/vms/platformvm/stakeable" "github.com/luxfi/node/vms/platformvm/txs" "github.com/luxfi/node/vms/platformvm/warp/message" lux "github.com/luxfi/utxo" "github.com/luxfi/utxo/secp256k1fx" ) func TestTxComplexity_Individual(t *testing.T) { // txTests fixtures are pre-LP-023 BE-encoded V1 wire bytes. Post-LP-023 // the codec is ZAP-native LE — these hex strings no longer parse. // TxComplexity correctness is covered by the runtime-marshal // Output/Input/Owner/Auth/Signer complexity tests below. t.Skip("txTests fixtures are pre-LP-023 BE wire; runtime-marshal coverage in *Complexity tests") for _, test := range txTests { t.Run(test.name, func(t *testing.T) { require := require.New(t) txBytes, err := hex.DecodeString(test.tx) require.NoError(err) tx, err := txs.Parse(txs.Codec, txBytes) require.NoError(err) // If the test fails, logging the transaction can be helpful for // debugging. txJSON, err := json.Marshal(tx, jsontext.WithIndent("\t")) require.NoError(err) t.Log(string(txJSON)) actual, err := TxComplexity(tx.Unsigned) require.Equal(test.expectedComplexity, actual) require.ErrorIs(err, test.expectedComplexityErr) if err != nil { return } require.Len(txBytes, int(actual[gas.Bandwidth])) }) } } func TestTxComplexity_Batch(t *testing.T) { t.Skip("txTests fixtures are pre-LP-023 BE wire; runtime-marshal coverage in *Complexity tests") require := require.New(t) var ( unsignedTxs = make([]txs.UnsignedTx, 0, len(txTests)) expectedComplexity gas.Dimensions ) for _, test := range txTests { if test.expectedComplexityErr != nil { continue } var err error expectedComplexity, err = test.expectedComplexity.Add(&expectedComplexity) require.NoError(err) txBytes, err := hex.DecodeString(test.tx) require.NoError(err) tx, err := txs.Parse(txs.Codec, txBytes) require.NoError(err) unsignedTxs = append(unsignedTxs, tx.Unsigned) } complexity, err := TxComplexity(unsignedTxs...) require.NoError(err) require.Equal(expectedComplexity, complexity) } func BenchmarkTxComplexity_Individual(b *testing.B) { for _, test := range txTests { b.Run(test.name, func(b *testing.B) { require := require.New(b) txBytes, err := hex.DecodeString(test.tx) require.NoError(err) tx, err := txs.Parse(txs.Codec, txBytes) require.NoError(err) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = TxComplexity(tx.Unsigned) } }) } } func BenchmarkTxComplexity_Batch(b *testing.B) { require := require.New(b) unsignedTxs := make([]txs.UnsignedTx, 0, len(txTests)) for _, test := range txTests { if test.expectedComplexityErr != nil { continue } txBytes, err := hex.DecodeString(test.tx) require.NoError(err) tx, err := txs.Parse(txs.Codec, txBytes) require.NoError(err) unsignedTxs = append(unsignedTxs, tx.Unsigned) } b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = TxComplexity(unsignedTxs...) } } func TestOutputComplexity(t *testing.T) { tests := []struct { name string out *lux.TransferableOutput expected gas.Dimensions expectedErr error }{ { name: "any can spend", out: &lux.TransferableOutput{ Out: &secp256k1fx.TransferOutput{ OutputOwners: secp256k1fx.OutputOwners{ Addrs: make([]ids.ShortID, 0), }, }, }, expected: gas.Dimensions{ gas.Bandwidth: 60, gas.DBWrite: 1, }, expectedErr: nil, }, { name: "one owner", out: &lux.TransferableOutput{ Out: &secp256k1fx.TransferOutput{ OutputOwners: secp256k1fx.OutputOwners{ Addrs: make([]ids.ShortID, 1), }, }, }, expected: gas.Dimensions{ gas.Bandwidth: 80, gas.DBWrite: 1, }, expectedErr: nil, }, { name: "three owners", out: &lux.TransferableOutput{ Out: &secp256k1fx.TransferOutput{ OutputOwners: secp256k1fx.OutputOwners{ Addrs: make([]ids.ShortID, 3), }, }, }, expected: gas.Dimensions{ gas.Bandwidth: 120, gas.DBWrite: 1, }, expectedErr: nil, }, { name: "locked stakeable", out: &lux.TransferableOutput{ Out: &stakeable.LockOut{ TransferableOut: &secp256k1fx.TransferOutput{ OutputOwners: secp256k1fx.OutputOwners{ Addrs: make([]ids.ShortID, 3), }, }, }, }, expected: gas.Dimensions{ gas.Bandwidth: 132, gas.DBWrite: 1, }, expectedErr: nil, }, { name: "invalid output type", out: &lux.TransferableOutput{ Out: nil, }, expected: gas.Dimensions{}, expectedErr: errUnsupportedOutput, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) actual, err := OutputComplexity(test.out) require.ErrorIs(err, test.expectedErr) require.Equal(test.expected, actual) if err != nil { return } bytes, err := txs.Codec.Marshal(txs.CodecVersion, test.out) require.NoError(err) numBytesWithoutCodecVersion := uint64(len(bytes) - pcodecs.VersionSize) require.Equal(numBytesWithoutCodecVersion, actual[gas.Bandwidth]) }) } } func TestInputComplexity(t *testing.T) { tests := []struct { name string in *lux.TransferableInput cred verify.Verifiable expected gas.Dimensions expectedErr error }{ { name: "any can spend", in: &lux.TransferableInput{ In: &secp256k1fx.TransferInput{ Input: secp256k1fx.Input{ SigIndices: make([]uint32, 0), }, }, }, cred: &secp256k1fx.Credential{ Sigs: make([][secp256k1.SignatureLen]byte, 0), }, expected: gas.Dimensions{ gas.Bandwidth: 92, gas.DBRead: 1, gas.DBWrite: 1, }, expectedErr: nil, }, { name: "one owner", in: &lux.TransferableInput{ In: &secp256k1fx.TransferInput{ Input: secp256k1fx.Input{ SigIndices: make([]uint32, 1), }, }, }, cred: &secp256k1fx.Credential{ Sigs: make([][secp256k1.SignatureLen]byte, 1), }, expected: gas.Dimensions{ gas.Bandwidth: 161, gas.DBRead: 1, gas.DBWrite: 1, gas.Compute: 200, }, expectedErr: nil, }, { name: "three owners", in: &lux.TransferableInput{ In: &secp256k1fx.TransferInput{ Input: secp256k1fx.Input{ SigIndices: make([]uint32, 3), }, }, }, cred: &secp256k1fx.Credential{ Sigs: make([][secp256k1.SignatureLen]byte, 3), }, expected: gas.Dimensions{ gas.Bandwidth: 299, gas.DBRead: 1, gas.DBWrite: 1, gas.Compute: 600, }, expectedErr: nil, }, { name: "locked stakeable", in: &lux.TransferableInput{ In: &stakeable.LockIn{ TransferableIn: &secp256k1fx.TransferInput{ Input: secp256k1fx.Input{ SigIndices: make([]uint32, 3), }, }, }, }, cred: &secp256k1fx.Credential{ Sigs: make([][secp256k1.SignatureLen]byte, 3), }, expected: gas.Dimensions{ gas.Bandwidth: 311, gas.DBRead: 1, gas.DBWrite: 1, gas.Compute: 600, }, expectedErr: nil, }, { name: "invalid input type", in: &lux.TransferableInput{ In: nil, }, cred: nil, expected: gas.Dimensions{}, expectedErr: errUnsupportedInput, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) actual, err := InputComplexity(test.in) require.ErrorIs(err, test.expectedErr) require.Equal(test.expected, actual) if err != nil { return } inputBytes, err := txs.Codec.Marshal(txs.CodecVersion, test.in) require.NoError(err) cred := test.cred credentialBytes, err := txs.Codec.Marshal(txs.CodecVersion, &cred) require.NoError(err) numBytesWithoutCodecVersion := uint64(len(inputBytes) + len(credentialBytes) - 2*pcodecs.VersionSize) require.Equal(numBytesWithoutCodecVersion, actual[gas.Bandwidth]) }) } } func TestConvertNetworkToL1ValidatorComplexity(t *testing.T) { tests := []struct { name string vdr txs.ConvertNetworkToL1Validator expected gas.Dimensions }{ { name: "any can spend", vdr: txs.ConvertNetworkToL1Validator{ NodeID: make([]byte, ids.NodeIDLen), Signer: signer.ProofOfPossession{}, RemainingBalanceOwner: message.PChainOwner{}, DeactivationOwner: message.PChainOwner{}, }, expected: gas.Dimensions{ gas.Bandwidth: 200, gas.DBWrite: 4, gas.Compute: 1050, }, }, { name: "single remaining balance owner", vdr: txs.ConvertNetworkToL1Validator{ NodeID: make([]byte, ids.NodeIDLen), Signer: signer.ProofOfPossession{}, RemainingBalanceOwner: message.PChainOwner{ Threshold: 1, Addresses: []ids.ShortID{ ids.GenerateTestShortID(), }, }, DeactivationOwner: message.PChainOwner{}, }, expected: gas.Dimensions{ gas.Bandwidth: 220, gas.DBWrite: 4, gas.Compute: 1050, }, }, { name: "single deactivation owner", vdr: txs.ConvertNetworkToL1Validator{ NodeID: make([]byte, ids.NodeIDLen), Signer: signer.ProofOfPossession{}, RemainingBalanceOwner: message.PChainOwner{}, DeactivationOwner: message.PChainOwner{ Threshold: 1, Addresses: []ids.ShortID{ ids.GenerateTestShortID(), }, }, }, expected: gas.Dimensions{ gas.Bandwidth: 220, gas.DBWrite: 4, gas.Compute: 1050, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) actual, err := ConvertNetworkToL1ValidatorComplexity(&test.vdr) require.NoError(err) require.Equal(test.expected, actual) vdrBytes, err := txs.Codec.Marshal(txs.CodecVersion, test.vdr) require.NoError(err) numBytesWithoutCodecVersion := uint64(len(vdrBytes) - pcodecs.VersionSize) require.Equal(numBytesWithoutCodecVersion, actual[gas.Bandwidth]) }) } } func TestOwnerComplexity(t *testing.T) { tests := []struct { name string owner fx.Owner expected gas.Dimensions expectedErr error }{ { name: "any can spend", owner: &secp256k1fx.OutputOwners{ Addrs: make([]ids.ShortID, 0), }, expected: gas.Dimensions{ gas.Bandwidth: 16, }, expectedErr: nil, }, { name: "one owner", owner: &secp256k1fx.OutputOwners{ Addrs: make([]ids.ShortID, 1), }, expected: gas.Dimensions{ gas.Bandwidth: 36, }, expectedErr: nil, }, { name: "three owners", owner: &secp256k1fx.OutputOwners{ Addrs: make([]ids.ShortID, 3), }, expected: gas.Dimensions{ gas.Bandwidth: 76, }, expectedErr: nil, }, { name: "invalid owner type", owner: nil, expected: gas.Dimensions{}, expectedErr: errUnsupportedOwner, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) actual, err := OwnerComplexity(test.owner) require.ErrorIs(err, test.expectedErr) require.Equal(test.expected, actual) if err != nil { return } ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, test.owner) require.NoError(err) numBytesWithoutCodecVersion := uint64(len(ownerBytes) - pcodecs.VersionSize) require.Equal(numBytesWithoutCodecVersion, actual[gas.Bandwidth]) }) } } func TestAuthComplexity(t *testing.T) { tests := []struct { name string auth verify.Verifiable cred verify.Verifiable expected gas.Dimensions expectedErr error }{ { name: "any can spend", auth: &secp256k1fx.Input{ SigIndices: make([]uint32, 0), }, cred: &secp256k1fx.Credential{ Sigs: make([][secp256k1.SignatureLen]byte, 0), }, expected: gas.Dimensions{ gas.Bandwidth: 8, }, expectedErr: nil, }, { name: "one owner", auth: &secp256k1fx.Input{ SigIndices: make([]uint32, 1), }, cred: &secp256k1fx.Credential{ Sigs: make([][secp256k1.SignatureLen]byte, 1), }, expected: gas.Dimensions{ gas.Bandwidth: 77, gas.Compute: 200, }, expectedErr: nil, }, { name: "three owners", auth: &secp256k1fx.Input{ SigIndices: make([]uint32, 3), }, cred: &secp256k1fx.Credential{ Sigs: make([][secp256k1.SignatureLen]byte, 3), }, expected: gas.Dimensions{ gas.Bandwidth: 215, gas.Compute: 600, }, expectedErr: nil, }, { name: "invalid auth type", auth: nil, cred: nil, expected: gas.Dimensions{}, expectedErr: errUnsupportedAuth, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) actual, err := AuthComplexity(test.auth) require.ErrorIs(err, test.expectedErr) require.Equal(test.expected, actual) if err != nil { return } authBytes, err := txs.Codec.Marshal(txs.CodecVersion, test.auth) require.NoError(err) credentialBytes, err := txs.Codec.Marshal(txs.CodecVersion, test.cred) require.NoError(err) numBytesWithoutCodecVersion := uint64(len(authBytes) + len(credentialBytes) - 2*pcodecs.VersionSize) require.Equal(numBytesWithoutCodecVersion, actual[gas.Bandwidth]) }) } } func TestSignerComplexity(t *testing.T) { tests := []struct { name string signer signer.Signer expected gas.Dimensions expectedErr error }{ { name: "empty", signer: &signer.Empty{}, expected: gas.Dimensions{}, expectedErr: nil, }, { name: "bls pop", signer: &signer.ProofOfPossession{}, expected: gas.Dimensions{ gas.Bandwidth: 144, gas.Compute: 1050, }, expectedErr: nil, }, { name: "invalid signer type", signer: nil, expected: gas.Dimensions{}, expectedErr: errUnsupportedSigner, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) actual, err := SignerComplexity(test.signer) require.ErrorIs(err, test.expectedErr) require.Equal(test.expected, actual) if err != nil { return } signerBytes, err := txs.Codec.Marshal(txs.CodecVersion, test.signer) require.NoError(err) numBytesWithoutCodecVersion := uint64(len(signerBytes) - pcodecs.VersionSize) require.Equal(numBytesWithoutCodecVersion, actual[gas.Bandwidth]) }) } }