From eb8c0a0279e89c7faefd5a5f257035df00c349b6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 18:42:36 -0400 Subject: [PATCH 001/120] sae: Add C-chain custom tx serialization --- ids/short.go | 10 + vms/saevm/cchain/tx/BUILD.bazel | 44 ++ vms/saevm/cchain/tx/codec.go | 47 ++ vms/saevm/cchain/tx/export.go | 38 ++ vms/saevm/cchain/tx/import.go | 37 ++ .../FuzzParseCompatibility/0ff1164eb2ab2c0b | 2 + .../FuzzParseCompatibility/befe02d645ceffe6 | 2 + vms/saevm/cchain/tx/tx.go | 100 ++++ vms/saevm/cchain/tx/tx_test.go | 514 ++++++++++++++++++ vms/secp256k1fx/credential.go | 4 + 10 files changed, 798 insertions(+) create mode 100644 vms/saevm/cchain/tx/BUILD.bazel create mode 100644 vms/saevm/cchain/tx/codec.go create mode 100644 vms/saevm/cchain/tx/export.go create mode 100644 vms/saevm/cchain/tx/import.go create mode 100644 vms/saevm/cchain/tx/testdata/fuzz/FuzzParseCompatibility/0ff1164eb2ab2c0b create mode 100644 vms/saevm/cchain/tx/testdata/fuzz/FuzzParseCompatibility/befe02d645ceffe6 create mode 100644 vms/saevm/cchain/tx/tx.go create mode 100644 vms/saevm/cchain/tx/tx_test.go diff --git a/ids/short.go b/ids/short.go index 5d17a3a492c6..1a582e385038 100644 --- a/ids/short.go +++ b/ids/short.go @@ -40,6 +40,16 @@ func ShortFromString(idStr string) (ShortID, error) { return ToShortID(bytes) } +// ShortFromStringOrPanic is the same as ShortFromString, but will panic on +// error. +func ShortFromStringOrPanic(idStr string) ShortID { + id, err := ShortFromString(idStr) + if err != nil { + panic(err) + } + return id +} + // ShortFromPrefixedString returns a ShortID assuming the cb58 format is // prefixed func ShortFromPrefixedString(idStr, prefix string) (ShortID, error) { diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel new file mode 100644 index 000000000000..91e98c137633 --- /dev/null +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//.bazel:defs.bzl", "go_test") + +go_library( + name = "tx", + srcs = [ + "codec.go", + "export.go", + "import.go", + "tx.go", + ], + importpath = "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx", + visibility = ["//visibility:public"], + deps = [ + "//codec", + "//codec/linearcodec", + "//ids", + "//utils/hashing", + "//utils/wrappers", + "//vms/components/avax", + "//vms/secp256k1fx", + "@com_github_ava_labs_libevm//common", + ], +) + +go_test( + name = "tx_test", + srcs = ["tx_test.go"], + data = glob(["testdata/**"]), + embed = [":tx"], + deps = [ + "//graft/coreth/plugin/evm/atomic", + "//graft/coreth/plugin/evm/atomic/vm", + "//ids", + "//vms/components/avax", + "//vms/components/verify", + "//vms/secp256k1fx", + "@com_github_ava_labs_libevm//common", + "@com_github_google_go_cmp//cmp", + "@com_github_google_go_cmp//cmp/cmpopts", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/vms/saevm/cchain/tx/codec.go b/vms/saevm/cchain/tx/codec.go new file mode 100644 index 000000000000..3e527228b5a8 --- /dev/null +++ b/vms/saevm/cchain/tx/codec.go @@ -0,0 +1,47 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import ( + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/utils/wrappers" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +const codecVersion uint16 = 0 + +var c codec.Manager + +func init() { + c = codec.NewDefaultManager() + + // Registration order impacts the typeID included in the canonical format. + // We skip registrations in specific locations so that UTXOs in shared + // memory share the same serialized format as on the P-Chain and X-Chain. + var ( + lc = linearcodec.NewDefault() + errs = wrappers.Errs{} + ) + errs.Add( + lc.RegisterType(&Import{}), + lc.RegisterType(&Export{}), + ) + lc.SkipRegistrations(3) + errs.Add( + lc.RegisterType(&secp256k1fx.TransferInput{}), + ) + lc.SkipRegistrations(1) + errs.Add( + lc.RegisterType(&secp256k1fx.TransferOutput{}), + ) + lc.SkipRegistrations(1) + errs.Add( + lc.RegisterType(&secp256k1fx.Credential{}), + c.RegisterCodec(codecVersion, lc), + ) + if errs.Errored() { + panic(errs.Err) + } +} diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go new file mode 100644 index 000000000000..6e780b9edccf --- /dev/null +++ b/vms/saevm/cchain/tx/export.go @@ -0,0 +1,38 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import ( + "github.com/ava-labs/libevm/common" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" +) + +// Export is the unsigned component of a transaction that transfers assets from +// the C-Chain to either the P-Chain or the X-Chain. It modifies the C-Chain +// state and produces UTXOs in the shared memory between the C-Chain and the +// destination chain. +type Export struct { + NetworkID uint32 `serialize:"true" json:"networkID"` + BlockchainID ids.ID `serialize:"true" json:"blockchainID"` + DestinationChain ids.ID `serialize:"true" json:"destinationChain"` + Ins []Input `serialize:"true" json:"inputs"` + ExportedOutputs []*avax.TransferableOutput `serialize:"true" json:"exportedOutputs"` +} + +// TODO(StephenButtolph): Remove this with its removal from the interface. +func (*Export) isUnsigned() {} + +// Input identifies an account + nonce pair on the C-Chain that authorizes the +// asset and quantity to deduct. +// +// If the AssetID is AVAX, the amount will be scaled up to account for the EVM's +// higher denomination. +type Input struct { + Address common.Address `serialize:"true" json:"address"` + Amount uint64 `serialize:"true" json:"amount"` + AssetID ids.ID `serialize:"true" json:"assetID"` + Nonce uint64 `serialize:"true" json:"nonce"` +} diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go new file mode 100644 index 000000000000..428678df753a --- /dev/null +++ b/vms/saevm/cchain/tx/import.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import ( + "github.com/ava-labs/libevm/common" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" +) + +// Import is the unsigned component of a transaction that transfers assets from +// either the P-Chain or the X-Chain to the C-Chain. It consumes UTXOs in the +// shared memory between the C-Chain and the source chain and increases balances +// in the C-Chain state. +type Import struct { + NetworkID uint32 `serialize:"true" json:"networkID"` + BlockchainID ids.ID `serialize:"true" json:"blockchainID"` + SourceChain ids.ID `serialize:"true" json:"sourceChain"` + ImportedInputs []*avax.TransferableInput `serialize:"true" json:"importedInputs"` + Outs []Output `serialize:"true" json:"outputs"` +} + +// TODO(StephenButtolph): Remove this with its removal from the interface. +func (*Import) isUnsigned() {} + +// Output specifies an account on the C-Chain whose balance of the specified +// asset should be increased. +// +// If the AssetID is AVAX, the amount will be scaled up to account for the EVM's +// higher denomination. +type Output struct { + Address common.Address `serialize:"true" json:"address"` + Amount uint64 `serialize:"true" json:"amount"` + AssetID ids.ID `serialize:"true" json:"assetID"` +} diff --git a/vms/saevm/cchain/tx/testdata/fuzz/FuzzParseCompatibility/0ff1164eb2ab2c0b b/vms/saevm/cchain/tx/testdata/fuzz/FuzzParseCompatibility/0ff1164eb2ab2c0b new file mode 100644 index 000000000000..220a199d580d --- /dev/null +++ b/vms/saevm/cchain/tx/testdata/fuzz/FuzzParseCompatibility/0ff1164eb2ab2c0b @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x00\x00\x00\x00\x0000000000000000000000000000000000000000000000000000000000000000000000\x00\x00\x00\x0100000000000000000000000000000000000000000000000000000000000000000000\x00\x00\x00\x0500000000\x00\x00\x00\x010000\x00\x00\x00\x01000000000000000000000000000000000000000000000000000000000000\x00\x00\x00\x01\x00\x00\x00\x0500000000\x00\x00\x00\x010000") diff --git a/vms/saevm/cchain/tx/testdata/fuzz/FuzzParseCompatibility/befe02d645ceffe6 b/vms/saevm/cchain/tx/testdata/fuzz/FuzzParseCompatibility/befe02d645ceffe6 new file mode 100644 index 000000000000..6de5c237f489 --- /dev/null +++ b/vms/saevm/cchain/tx/testdata/fuzz/FuzzParseCompatibility/befe02d645ceffe6 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x00\x00\x00\x00\x0000000000000000000000000000000000000000000000000000000000000000000000\x00\x00\x00\x0100000000000000000000000000000000000000000000000000000000000000000000\x00\x00\x00\x0500000000\x00\x00\x00\x010000\x00\x00\x00\x01000000000000000000000000000000000000000000000000000000000000\x00\x00\x00\x01\x00\x00\x00\n\x00\x00\x00\x010000") diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go new file mode 100644 index 000000000000..8cfa462b89d5 --- /dev/null +++ b/vms/saevm/cchain/tx/tx.go @@ -0,0 +1,100 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package tx defines the Avalanche-specific transaction types used on the +// C-Chain to interact with the shared memory between the C-Chain and other +// chains on the Primary Network. +package tx + +import ( + "errors" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +// Tx is a signed transaction that interacts with shared memory. +// The [Unsigned] body can be implemented by either [Export] or [Import]. +// The [Credential] values are implemented by [secp256k1fx.Credential]. +type Tx struct { + Unsigned `serialize:"true" json:"unsignedTx"` + Creds []Credential `serialize:"true" json:"credentials"` +} + +// Unsigned is a common interface implemented by [Import] and [Export]. +// +// TODO(StephenButtolph): Expand this interface to include UTXO handling, +// verification, and state execution. +type Unsigned interface { + // This function ensures that [Tx.Unsigned] can only be parsed as [Export] + // or [Import]. + // + // TODO(StephenButtolph): Once [Unsigned] includes other unexported + // functions, remove this function. + isUnsigned() +} + +// Credential is used in [Tx] to authorize an input of a transaction. +// +// It is only implemented by [secp256k1fx.Credential]. An interface must be used +// to correctly produce the canonical binary format during serialization. +type Credential interface { + Self() *secp256k1fx.Credential +} + +// ID returns the unique hash of the transaction. +func (t *Tx) ID() ids.ID { + // TODO(StephenButtolph): Optimize ID by caching previously calculated + // values. + bytes, err := t.Bytes() + // This error can happen, but only with invalid transactions. To avoid + // polluting the interface, we represent all invalid transactions with + // the zero ID. + if err != nil { + return ids.ID{} + } + return hashing.ComputeHash256Array(bytes) +} + +// Bytes returns the canonical binary format of the transaction. +func (t *Tx) Bytes() ([]byte, error) { + // TODO(StephenButtolph): Optimize Bytes by caching previously calculated + // values. + return c.Marshal(codecVersion, t) +} + +// Parse deserializes a [Tx] from its canonical binary format. +func Parse(b []byte) (*Tx, error) { + var tx Tx + if _, err := c.Unmarshal(b, &tx); err != nil { + return nil, err + } + return &tx, nil +} + +// MarshalSlice returns the canonical binary format of a slice of transactions. +func MarshalSlice(txs []*Tx) ([]byte, error) { + if len(txs) == 0 { + return nil, nil + } + return c.Marshal(codecVersion, txs) +} + +var errInefficientSlicePacking = errors.New("inefficient slice packing: empty slices should be packed as nil") + +// ParseSlice deserializes a slice of [Tx] from its canonical binary format. +func ParseSlice(b []byte) ([]*Tx, error) { + if len(b) == 0 { + return nil, nil + } + + var txs []*Tx + if _, err := c.Unmarshal(b, &txs); err != nil { + return nil, err + } + if len(txs) == 0 { + return nil, errInefficientSlicePacking + } + return txs, nil +} diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go new file mode 100644 index 000000000000..e44c1eda3753 --- /dev/null +++ b/vms/saevm/cchain/tx/tx_test.go @@ -0,0 +1,514 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + // Imported for [parseOldTx] comment resolution. + _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/vm" + + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +// tests is defined at the package level to allow sharing between fuzz tests and +// unit tests. +var ( + tests = [...]struct { + name string + old *atomic.Tx + new *Tx + json string + id ids.ID + bytes []byte + }{ + { + name: "import", // Included in https://subnets.avax.network/c-chain/block/4 + old: &atomic.Tx{ + UnsignedAtomicTx: &atomic.UnsignedImportTx{ + NetworkID: 1, + BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), + SourceChain: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + ImportedInputs: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{ + TxID: ids.FromStringOrPanic("2VqSFA5hxukiv1FSAB8ShjwHwmPev9ZS8VD9aUTCDRoff7T5Bi"), + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + }, + In: &secp256k1fx.TransferInput{ + Amt: 50000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }}, + Outs: []atomic.EVMOutput{{ + Address: common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"), + Amount: 50000000, + AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + }}, + }, + Creds: []verify.Verifiable{ + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x3e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01")), + }, + }, + }, + }, + new: &Tx{ + Unsigned: &Import{ + NetworkID: 1, + BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), + SourceChain: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + ImportedInputs: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{ + TxID: ids.FromStringOrPanic("2VqSFA5hxukiv1FSAB8ShjwHwmPev9ZS8VD9aUTCDRoff7T5Bi"), + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + }, + In: &secp256k1fx.TransferInput{ + Amt: 50000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }}, + Outs: []Output{{ + Address: common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"), + Amount: 50000000, + AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + }}, + }, + Creds: []Credential{ + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x3e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01")), + }, + }, + }, + }, + json: `{ + "unsignedTx":{ + "networkID":1, + "blockchainID":"2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", + "sourceChain":"2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", + "importedInputs":[{ + "txID":"2VqSFA5hxukiv1FSAB8ShjwHwmPev9ZS8VD9aUTCDRoff7T5Bi", + "outputIndex":1, + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID":"11111111111111111111111111111111LpoYY", + "input":{ + "amount":50000000, + "signatureIndices":[0] + } + }], + "outputs":[{ + "address":"0xb8b5a87d1c05676f1f966da49151fa54dbe68c33", + "amount":50000000, + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z" + }] + }, + "credentials":[{ + "signatures":[ + "0x3e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01" + ] + }] + }`, + id: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), + bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001c52b712aa7dce27a650bf509f799673e245edd4fa9e4e1700eb6105202fe579a0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000002faf080000000010000000000000001b8b5a87d1c05676f1f966da49151fa54dbe68c330000000002faf08021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000100000009000000013e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01"), + }, + { + name: "export", // Included in https://subnets.avax.network/c-chain/block/48 + old: &atomic.Tx{ + UnsignedAtomicTx: &atomic.UnsignedExportTx{ + NetworkID: 1, + BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), + DestinationChain: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + Ins: []atomic.EVMInput{{ + Address: common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), + Amount: 1000001, + AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + Nonce: 0, + }}, + ExportedOutputs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ + ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + }, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.ShortFromStringOrPanic("LanVZgBDVvtarbTXD1uU7r1nXVJyLmPUz"), + }, + }, + }, + }}, + }, + Creds: []verify.Verifiable{ + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01")), + }, + }, + }, + }, + new: &Tx{ + Unsigned: &Export{ + NetworkID: 1, + BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), + DestinationChain: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + Ins: []Input{{ + Address: common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), + Amount: 1000001, + AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + Nonce: 0, + }}, + ExportedOutputs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ + ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + }, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.ShortFromStringOrPanic("LanVZgBDVvtarbTXD1uU7r1nXVJyLmPUz"), + }, + }, + }, + }}, + }, + Creds: []Credential{ + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01")), + }, + }, + }, + }, + json: `{ + "unsignedTx":{ + "networkID":1, + "blockchainID":"2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", + "destinationChain":"2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", + "inputs":[{ + "address":"0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6", + "amount":1000001, + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "nonce":0 + }], + "exportedOutputs":[{ + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID":"11111111111111111111111111111111LpoYY", + "output":{ + "addresses":["LanVZgBDVvtarbTXD1uU7r1nXVJyLmPUz"], + "amount":1, + "locktime":0, + "threshold":1 + } + }] + }, + "credentials":[{ + "signatures":[ + "0x254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01" + ] + }] + }`, + id: ids.FromStringOrPanic("ng7Dox1r8nctrF6zurhRPYWxkmE2juUhT7Qhpauyo8qSEu6jB"), + bytes: common.FromHex("0x000000000001000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001eb019ccd325ad53543a7e7e3b04828bdecf3cff600000000000f424121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000000000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500000000010000000900000001254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01"), + }, + } + oldSlice []*atomic.Tx + newSlice []*Tx +) + +func init() { + oldSlice = make([]*atomic.Tx, len(tests)) + newSlice = make([]*Tx, len(tests)) + for i, test := range tests { + oldSlice[i] = test.old + newSlice[i] = test.new + } +} + +func TestID(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Run("old", func(t *testing.T) { + // We must parse the old tx to properly initialize the ID. + old, err := parseOldTx(test.bytes) + require.NoError(t, err, "parseOldTx()") + assert.Equalf(t, test.id, old.ID(), "%T.ID()", old) + }) + t.Run("new", func(t *testing.T) { + assert.Equalf(t, test.id, test.new.ID(), "%T.ID()", test.new) + }) + }) + } +} + +func TestBytes(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Run("old", func(t *testing.T) { + got, err := atomic.Codec.Marshal(atomic.CodecVersion, test.old) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, test.old) + assert.Equalf(t, test.bytes, got, "%T.Marshal(, %T)", atomic.Codec, test.old) + }) + t.Run("new", func(t *testing.T) { + got, err := test.new.Bytes() + require.NoErrorf(t, err, "%T.Bytes()", test.new) + assert.Equalf(t, test.bytes, got, "%T.Bytes()", test.new) + }) + }) + } +} + +func TestMarshalSlice(t *testing.T) { + want, err := atomic.Codec.Marshal(atomic.CodecVersion, oldSlice) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldSlice) + + tests := []struct { + name string + txs []*Tx + want []byte + }{ + { + name: "mainnet", + txs: newSlice, + want: want, + }, + { + name: "empty", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := MarshalSlice(test.txs) + require.NoErrorf(t, err, "MarshalSlice(%T)", test.txs) + assert.Equalf(t, test.want, got, "MarshalSlice(%T)", test.txs) + }) + } +} + +func TestParse(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Run("old", func(t *testing.T) { + got := new(atomic.Tx) + _, err := atomic.Codec.Unmarshal(test.bytes, got) + require.NoErrorf(t, err, "%T.Unmarshal(, %T)", atomic.Codec, got) + assert.Equalf(t, test.old, got, "%T.Unmarshal(, %T)", atomic.Codec, got) + }) + t.Run("new", func(t *testing.T) { + got, err := Parse(test.bytes) + require.NoError(t, err, "Parse()") + assert.Equal(t, test.new, got, "Parse()") + }) + }) + } +} + +func TestParseSlice(t *testing.T) { + bytes, err := atomic.Codec.Marshal(atomic.CodecVersion, oldSlice) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldSlice) + + tests := []struct { + name string + bytes []byte + want []*Tx + wantErr error + }{ + { + name: "mainnet", + bytes: bytes, + want: newSlice, + }, + { + name: "empty", + }, + { + name: "inefficient", + bytes: []byte{ + // codecVersion: + 0x00, 0x00, + // len(txs): + 0x00, 0x00, 0x00, 0x00, + }, + wantErr: errInefficientSlicePacking, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := ParseSlice(test.bytes) + require.ErrorIs(t, err, test.wantErr, "ParseSlice()") + assert.Equal(t, test.want, got, "ParseSlice()") + }) + } +} + +var errUnexpectedCredentialType = errors.New("unexpected credential type") + +// parseOldTx parses a transaction using coreth's old parsing logic but enforces +// additional restrictions. Coreth's parsing logic is overly permissive and +// depends on later verification in [vm.VerifierBackend]. +func parseOldTx(b []byte) (*atomic.Tx, error) { + tx, err := atomic.ExtractAtomicTx(b, atomic.Codec) + if err != nil { + return nil, err + } + for _, cred := range tx.Creds { + if _, ok := cred.(*secp256k1fx.Credential); !ok { + return nil, errUnexpectedCredentialType + } + } + return tx, nil +} + +// parseOldTxs parses a slice of transactions using coreth's old parsing logic +// but enforces additional restrictions. Coreth's parsing logic is overly +// permissive and depends on later verification in [vm.VerifierBackend]. +func parseOldTxs(b []byte) ([]*atomic.Tx, error) { + txs, err := atomic.ExtractAtomicTxs(b, true, atomic.Codec) + if err != nil { + return nil, err + } + for _, tx := range txs { + for _, cred := range tx.Creds { + if _, ok := cred.(*secp256k1fx.Credential); !ok { + return nil, errUnexpectedCredentialType + } + } + } + return txs, nil +} + +func FuzzParseCompatibility(f *testing.F) { + for _, test := range tests { + f.Add(test.bytes) + } + f.Fuzz(func(t *testing.T, data []byte) { + _, oldErr := parseOldTx(data) + oldOk := oldErr == nil + + _, newErr := Parse(data) + newOk := newErr == nil + + assert.Equal(t, oldOk, newOk, "Parse(b) == parseOldTx(b)") + }) +} + +func FuzzParseSliceCompatibility(f *testing.F) { + { + b, err := MarshalSlice(newSlice) + require.NoError(f, err, "MarshalSlice()") + f.Add(b) + } + + f.Fuzz(func(t *testing.T, data []byte) { + _, oldErr := parseOldTxs(data) + oldOk := oldErr == nil + + _, newErr := ParseSlice(data) + newOk := newErr == nil + + assert.Equal(t, oldOk, newOk, "ParseSlice(b) == parseOldTxs(b)") + }) +} + +func FuzzParseRoundTrip(f *testing.F) { + for _, test := range tests { + f.Add(test.bytes) + } + f.Fuzz(func(t *testing.T, data []byte) { + tx, err := Parse(data) + if err != nil { + return + } + + got, err := tx.Bytes() + require.NoErrorf(t, err, "%T.Bytes()", tx) + assert.Equal(t, data, got, "Parse(b).Bytes() == b") + }) +} + +func FuzzParseSliceRoundTrip(f *testing.F) { + { + b, err := MarshalSlice(newSlice) + require.NoError(f, err, "MarshalSlice()") + f.Add(b) + } + + f.Fuzz(func(t *testing.T, data []byte) { + txs, err := ParseSlice(data) + if err != nil { + return + } + + got, err := MarshalSlice(txs) + require.NoError(t, err, "MarshalSlice()") + if diff := cmp.Diff(data, got, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("MarshalSlice(ParseSlice()) diff (-want +got):\n%s", diff) + } + }) +} + +func TestJSONMarshal(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Run("old", func(t *testing.T) { + got, err := json.Marshal(test.old) + require.NoErrorf(t, err, "json.Marshal(%T)", test.old) + assert.JSONEqf(t, test.json, string(got), "json.Marshal(%T)", test.old) + }) + t.Run("new", func(t *testing.T) { + got, err := json.Marshal(test.new) + require.NoErrorf(t, err, "json.Marshal(%T)", test.new) + assert.JSONEqf(t, test.json, string(got), "json.Marshal(%T)", test.new) + }) + }) + } +} + +func FuzzJSONCompatibility(f *testing.F) { + for _, test := range tests { + f.Add(test.bytes) + } + f.Fuzz(func(t *testing.T, data []byte) { + oldTx, err := parseOldTx(data) + if err != nil { + t.Skip("invalid tx") + } + + newTx, err := Parse(data) + require.NoError(t, err, "Parse()") + + oldJSON, err := json.Marshal(oldTx) + require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) + + newJSON, err := json.Marshal(newTx) + require.NoErrorf(t, err, "json.Marshal(%T)", newTx) + assert.JSONEq(t, string(oldJSON), string(newJSON)) + }) +} diff --git a/vms/secp256k1fx/credential.go b/vms/secp256k1fx/credential.go index 2baa75175934..2e805ed1d1b7 100644 --- a/vms/secp256k1fx/credential.go +++ b/vms/secp256k1fx/credential.go @@ -42,3 +42,7 @@ func (cr *Credential) Verify() error { return nil } + +func (cr *Credential) Self() *Credential { + return cr +} From e248997e57b8ae7c76ae9d0b02eebcfa2282aec1 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 18:43:59 -0400 Subject: [PATCH 002/120] sae: Implement AsOp --- vms/saevm/cchain/tx/BUILD.bazel | 7 +++ vms/saevm/cchain/tx/export.go | 67 +++++++++++++++++++- vms/saevm/cchain/tx/import.go | 69 ++++++++++++++++++++- vms/saevm/cchain/tx/tx.go | 106 ++++++++++++++++++++++++++++++-- vms/saevm/cchain/tx/tx_test.go | 59 +++++++++++++----- 5 files changed, 282 insertions(+), 26 deletions(-) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index 91e98c137633..7160fb3adea7 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -14,12 +14,17 @@ go_library( deps = [ "//codec", "//codec/linearcodec", + "//graft/coreth/plugin/evm/upgrade/ap5", "//ids", "//utils/hashing", + "//utils/math", "//utils/wrappers", "//vms/components/avax", + "//vms/components/gas", + "//vms/saevm/hook", "//vms/secp256k1fx", "@com_github_ava_labs_libevm//common", + "@com_github_holiman_uint256//:uint256", ], ) @@ -34,10 +39,12 @@ go_test( "//ids", "//vms/components/avax", "//vms/components/verify", + "//vms/saevm/hook", "//vms/secp256k1fx", "@com_github_ava_labs_libevm//common", "@com_github_google_go_cmp//cmp", "@com_github_google_go_cmp//cmp/cmpopts", + "@com_github_holiman_uint256//:uint256", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", ], diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 6e780b9edccf..45292f96126b 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -4,10 +4,16 @@ package tx import ( + "errors" + "fmt" + "github.com/ava-labs/libevm/common" + "github.com/holiman/uint256" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/saevm/hook" ) // Export is the unsigned component of a transaction that transfers assets from @@ -22,9 +28,6 @@ type Export struct { ExportedOutputs []*avax.TransferableOutput `serialize:"true" json:"exportedOutputs"` } -// TODO(StephenButtolph): Remove this with its removal from the interface. -func (*Export) isUnsigned() {} - // Input identifies an account + nonce pair on the C-Chain that authorizes the // asset and quantity to deduct. // @@ -36,3 +39,61 @@ type Input struct { AssetID ids.ID `serialize:"true" json:"assetID"` Nonce uint64 `serialize:"true" json:"nonce"` } + +func (e *Export) burned(assetID ids.ID) (uint64, error) { + var ( + burned uint64 + err error + ) + for _, in := range e.Ins { + if in.AssetID == assetID { + burned, err = math.Add(burned, in.Amount) + if err != nil { + return 0, err + } + } + } + for _, out := range e.ExportedOutputs { + if out.Asset.ID == assetID { + burned, err = math.Sub(burned, out.Out.Amount()) + if err != nil { + return 0, err + } + } + } + return burned, nil +} + +func (e *Export) numSigs() (uint64, error) { + return uint64(len(e.Ins)), nil +} + +var errMultipleNonces = errors.New("multiple inputs for address with different nonces") + +func (e *Export) asOp(avaxAssetID ids.ID) (op, error) { + burn := make(map[common.Address]hook.AccountDebit, len(e.Ins)) + for _, in := range e.Ins { + debit, ok := burn[in.Address] + if ok && debit.Nonce != in.Nonce { + return op{}, fmt.Errorf("%w: address %s has nonces %d and %d", errMultipleNonces, in.Address, debit.Nonce, in.Nonce) + } + + // Only AVAX assets are transferred through the SAE ops, but SAE owns + // all nonce modifications. + if in.AssetID == avaxAssetID { + var amount uint256.Int + amount.SetUint64(in.Amount) + amount.Mul(&amount, x2cRate) + if _, overflow := debit.Amount.AddOverflow(&debit.Amount, &amount); overflow { + return op{}, fmt.Errorf("%w: for address %s", errOverflow, in.Address) + } + } + + debit.Nonce = in.Nonce + debit.MinBalance = debit.Amount + burn[in.Address] = debit + } + return op{ + burn: burn, + }, nil +} diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 428678df753a..2497fe624c86 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -4,10 +4,16 @@ package tx import ( + "errors" + "fmt" + "github.com/ava-labs/libevm/common" + "github.com/holiman/uint256" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) // Import is the unsigned component of a transaction that transfers assets from @@ -22,9 +28,6 @@ type Import struct { Outs []Output `serialize:"true" json:"outputs"` } -// TODO(StephenButtolph): Remove this with its removal from the interface. -func (*Import) isUnsigned() {} - // Output specifies an account on the C-Chain whose balance of the specified // asset should be increased. // @@ -35,3 +38,63 @@ type Output struct { Amount uint64 `serialize:"true" json:"amount"` AssetID ids.ID `serialize:"true" json:"assetID"` } + +func (i *Import) burned(assetID ids.ID) (uint64, error) { + var ( + burned uint64 + err error + ) + for _, in := range i.ImportedInputs { + if in.Asset.ID == assetID { + burned, err = math.Add(burned, in.In.Amount()) + if err != nil { + return 0, err + } + } + } + for _, out := range i.Outs { + if out.AssetID == assetID { + burned, err = math.Sub(burned, out.Amount) + if err != nil { + return 0, err + } + } + } + return burned, nil +} + +func (i *Import) numSigs() (uint64, error) { + var n uint64 + for _, in := range i.ImportedInputs { + input, ok := in.In.(*secp256k1fx.TransferInput) + if !ok { + return 0, nil + } + n += uint64(len(input.SigIndices)) + } + return n, nil +} + +var errOverflow = errors.New("amount overflow") + +func (i *Import) asOp(avaxAssetID ids.ID) (op, error) { + mint := make(map[common.Address]uint256.Int, len(i.Outs)) + for _, out := range i.Outs { + if out.AssetID != avaxAssetID { + continue + } + + var amount uint256.Int + amount.SetUint64(out.Amount) + amount.Mul(&amount, x2cRate) + + total := mint[out.Address] + if _, overflow := total.AddOverflow(&total, &amount); overflow { + return op{}, fmt.Errorf("%w: for address %s", errOverflow, out.Address) + } + mint[out.Address] = total + } + return op{ + mint: mint, + }, nil +} diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 8cfa462b89d5..3faf762b9d66 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -8,9 +8,17 @@ package tx import ( "errors" + "fmt" + "github.com/ava-labs/libevm/common" + "github.com/holiman/uint256" + + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap5" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) @@ -27,12 +35,22 @@ type Tx struct { // TODO(StephenButtolph): Expand this interface to include UTXO handling, // verification, and state execution. type Unsigned interface { - // This function ensures that [Tx.Unsigned] can only be parsed as [Export] - // or [Import]. - // - // TODO(StephenButtolph): Once [Unsigned] includes other unexported - // functions, remove this function. - isUnsigned() + // burned returns the amount of assetID that is consumed but not produced by + // this transaction. + burned(assetID ids.ID) (uint64, error) + + // numSigs returns the expected number of signatures required to sign this + // transaction. + numSigs() (uint64, error) + + // asOp returns the operation that this transaction performs on the EVM + // state. + asOp(avaxAssetID ids.ID) (op, error) +} + +type op struct { + burn map[common.Address]hook.AccountDebit + mint map[common.Address]uint256.Int } // Credential is used in [Tx] to authorize an input of a transaction. @@ -64,6 +82,82 @@ func (t *Tx) Bytes() ([]byte, error) { return c.Marshal(codecVersion, t) } +func (t *Tx) AsOp(avaxAssetID ids.ID) (hook.Op, error) { + gasUsed, err := gasUsed(t.Unsigned) + if err != nil { + return hook.Op{}, fmt.Errorf("calculating gas used: %w", err) + } + + burned, err := t.burned(avaxAssetID) + if err != nil { + return hook.Op{}, fmt.Errorf("calculating amount burned: %w", err) + } + + op, err := t.Unsigned.asOp(avaxAssetID) + if err != nil { + return hook.Op{}, fmt.Errorf("converting unsigned transaction to operation: %w", err) + } + + return hook.Op{ + ID: t.ID(), + Gas: gas.Gas(gasUsed), + GasFeeCap: gasPrice(burned, gasUsed), + Burn: op.burn, + Mint: op.mint, + }, nil +} + +const ( + IntrinsicGas = ap5.AtomicTxIntrinsicGas + GasPerByte = 1 // atomic.TxBytesGas + GasPerSig = secp256k1fx.CostPerSignature +) + +func gasUsed(t Unsigned) (uint64, error) { + numBytes, err := c.Size(codecVersion, &t) + if err != nil { + return 0, err + } + bytesGas, err := math.Mul(uint64(numBytes), GasPerByte) + if err != nil { + return 0, err + } + numSigs, err := t.numSigs() + if err != nil { + return 0, err + } + sigsGas, err := math.Mul(numSigs, GasPerSig) + if err != nil { + return 0, err + } + dynamicGas, err := math.Add(bytesGas, sigsGas) + if err != nil { + return 0, err + } + return math.Add(IntrinsicGas, dynamicGas) +} + +const x2cRateC = 1_000_000_000 + +// x2cRate is the conversion rate between the smallest denomination on the +// X-Chain, 1 nAVAX, and the smallest denomination on the C-Chain 1 aAVAX. +var x2cRate = uint256.NewInt(x2cRateC) + +// gasPrice takes in the burned amount of AVAX in nAVAX and the gas used and +// returns the price per gas in aAVAX/gas. +// +// The result is rounded down to the nearest aAVAX/gas. +func gasPrice(burned, gasUsed uint64) uint256.Int { + var u uint256.Int + u.SetUint64(gasUsed) + + var p uint256.Int + p.SetUint64(burned) + p.Mul(&p, x2cRate) + p.Div(&p, &u) + return p +} + // Parse deserializes a [Tx] from its canonical binary format. func Parse(b []byte) (*Tx, error) { var tx Tx diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index e44c1eda3753..fedcca88591f 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -11,6 +11,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/holiman/uint256" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,19 +22,21 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) // tests is defined at the package level to allow sharing between fuzz tests and // unit tests. var ( - tests = [...]struct { + avaxAssetID = ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z") + tests = [...]struct { name string old *atomic.Tx new *Tx json string - id ids.ID bytes []byte + op hook.Op }{ { name: "import", // Included in https://subnets.avax.network/c-chain/block/4 @@ -48,7 +51,7 @@ var ( OutputIndex: 1, }, Asset: avax.Asset{ - ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + ID: avaxAssetID, }, In: &secp256k1fx.TransferInput{ Amt: 50000000, @@ -60,7 +63,7 @@ var ( Outs: []atomic.EVMOutput{{ Address: common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"), Amount: 50000000, - AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + AssetID: avaxAssetID, }}, }, Creds: []verify.Verifiable{ @@ -82,7 +85,7 @@ var ( OutputIndex: 1, }, Asset: avax.Asset{ - ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + ID: avaxAssetID, }, In: &secp256k1fx.TransferInput{ Amt: 50000000, @@ -94,7 +97,7 @@ var ( Outs: []Output{{ Address: common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"), Amount: 50000000, - AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + AssetID: avaxAssetID, }}, }, Creds: []Credential{ @@ -132,8 +135,15 @@ var ( ] }] }`, - id: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001c52b712aa7dce27a650bf509f799673e245edd4fa9e4e1700eb6105202fe579a0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000002faf080000000010000000000000001b8b5a87d1c05676f1f966da49151fa54dbe68c330000000002faf08021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000100000009000000013e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01"), + op: hook.Op{ + ID: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), + Gas: 11230, + GasFeeCap: *uint256.NewInt(0), + Mint: map[common.Address]uint256.Int{ + common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"): *uint256.NewInt(50_000_000 * x2cRateC), + }, + }, }, { name: "export", // Included in https://subnets.avax.network/c-chain/block/48 @@ -145,12 +155,12 @@ var ( Ins: []atomic.EVMInput{{ Address: common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), Amount: 1000001, - AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + AssetID: avaxAssetID, Nonce: 0, }}, ExportedOutputs: []*avax.TransferableOutput{{ Asset: avax.Asset{ - ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + ID: avaxAssetID, }, Out: &secp256k1fx.TransferOutput{ Amt: 1, @@ -180,12 +190,12 @@ var ( Ins: []Input{{ Address: common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), Amount: 1000001, - AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + AssetID: avaxAssetID, Nonce: 0, }}, ExportedOutputs: []*avax.TransferableOutput{{ Asset: avax.Asset{ - ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + ID: avaxAssetID, }, Out: &secp256k1fx.TransferOutput{ Amt: 1, @@ -235,8 +245,19 @@ var ( ] }] }`, - id: ids.FromStringOrPanic("ng7Dox1r8nctrF6zurhRPYWxkmE2juUhT7Qhpauyo8qSEu6jB"), bytes: common.FromHex("0x000000000001000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001eb019ccd325ad53543a7e7e3b04828bdecf3cff600000000000f424121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000000000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500000000010000000900000001254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01"), + op: hook.Op{ + ID: ids.FromStringOrPanic("ng7Dox1r8nctrF6zurhRPYWxkmE2juUhT7Qhpauyo8qSEu6jB"), + Gas: 11230, + GasFeeCap: *uint256.NewInt(1_000_000 * x2cRateC / 11230), + Burn: map[common.Address]hook.AccountDebit{ + common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"): { + Nonce: 0, + Amount: *uint256.NewInt(1_000_001 * x2cRateC), + MinBalance: *uint256.NewInt(1_000_001 * x2cRateC), + }, + }, + }, }, } oldSlice []*atomic.Tx @@ -259,10 +280,10 @@ func TestID(t *testing.T) { // We must parse the old tx to properly initialize the ID. old, err := parseOldTx(test.bytes) require.NoError(t, err, "parseOldTx()") - assert.Equalf(t, test.id, old.ID(), "%T.ID()", old) + assert.Equalf(t, test.op.ID, old.ID(), "%T.ID()", old) }) t.Run("new", func(t *testing.T) { - assert.Equalf(t, test.id, test.new.ID(), "%T.ID()", test.new) + assert.Equalf(t, test.op.ID, test.new.ID(), "%T.ID()", test.new) }) }) } @@ -512,3 +533,13 @@ func FuzzJSONCompatibility(f *testing.F) { assert.JSONEq(t, string(oldJSON), string(newJSON)) }) } + +func TestAsOp(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.new.AsOp(avaxAssetID) + require.NoErrorf(t, err, "%T.AsOp(avaxAssetID)", test.new) + require.Equalf(t, test.op, got, "%T.AsOp(avaxAssetID)", test.new) + }) + } +} From 253c2551f05999c012ae3ce7b3c03dcded832044 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 19:27:47 -0400 Subject: [PATCH 003/120] wip --- vms/saevm/cchain/tx/import.go | 4 +++- vms/saevm/cchain/tx/tx_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 2497fe624c86..00074884552d 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -63,12 +63,14 @@ func (i *Import) burned(assetID ids.ID) (uint64, error) { return burned, nil } +var errUnexpectedInputType = errors.New("unexpected input type") + func (i *Import) numSigs() (uint64, error) { var n uint64 for _, in := range i.ImportedInputs { input, ok := in.In.(*secp256k1fx.TransferInput) if !ok { - return 0, nil + return 0, fmt.Errorf("%w: got %T ; want %T", errUnexpectedInputType, in.In, input) } n += uint64(len(input.SigIndices)) } diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index fedcca88591f..c05bf374ff1a 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -543,3 +543,28 @@ func TestAsOp(t *testing.T) { }) } } + +func FuzzAsOp_GasFeeCap(f *testing.F) { + for _, test := range tests { + f.Add(test.bytes) + } + f.Fuzz(func(t *testing.T, data []byte) { + oldTx, err := parseOldTx(data) + if err != nil { + t.Skip("invalid tx") + } + + newTx, err := Parse(data) + require.NoError(t, err, "Parse()") + + gasPrice, oldErr := atomic.EffectiveGasPrice(oldTx.UnsignedAtomicTx, avaxAssetID, true) + oldOk := oldErr == nil + op, newErr := newTx.AsOp(avaxAssetID) + newOk := newErr == nil + assert.Equalf(t, oldOk, newOk, "atomic.EffectiveGasPrice(%T) == %T.AsOp().GasFeeCap", oldTx, newTx) + + if newOk { + assert.Equal(t, gasPrice, op.GasFeeCap) + } + }) +} From 3cca8aa67377f701d442b6c79028c25614e03223 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 19:41:34 -0400 Subject: [PATCH 004/120] comments --- vms/saevm/cchain/tx/tx.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 3faf762b9d66..f9a0ad61c93b 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -20,6 +20,9 @@ import ( "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + // Imported for [GasPerByte] comment resolution. + _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" ) // Tx is a signed transaction that interacts with shared memory. @@ -108,9 +111,14 @@ func (t *Tx) AsOp(avaxAssetID ids.ID) (hook.Op, error) { } const ( + // IntrinsicGas is an initial static amount of gas that every [Tx] must pay. IntrinsicGas = ap5.AtomicTxIntrinsicGas - GasPerByte = 1 // atomic.TxBytesGas - GasPerSig = secp256k1fx.CostPerSignature + // GasPerByte is an additional amount of gas that is charged per-byte of an + // [Unsigned] transaction. + GasPerByte = 1 // [atomic.TxBytesGas] + // GasPerSig is an additional amount of gas that is charged per-signature + // included in a [Tx]. + GasPerSig = secp256k1fx.CostPerSignature ) func gasUsed(t Unsigned) (uint64, error) { From 6346d2e74ac2f1bc782cc205228e3b41d547c5ba Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 19:49:10 -0400 Subject: [PATCH 005/120] reduce exported identifiers --- vms/saevm/cchain/tx/tx.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index f9a0ad61c93b..e93f8f56165a 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -85,6 +85,10 @@ func (t *Tx) Bytes() ([]byte, error) { return c.Marshal(codecVersion, t) } +// AsOp converts the transaction into a [hook.Op] that can be processed by SAE. +// +// The operation only includes state changes that impact Ethereum-native state. +// It does not include non-AVAX balance changes or shared memory modifications. func (t *Tx) AsOp(avaxAssetID ids.ID) (hook.Op, error) { gasUsed, err := gasUsed(t.Unsigned) if err != nil { @@ -111,14 +115,14 @@ func (t *Tx) AsOp(avaxAssetID ids.ID) (hook.Op, error) { } const ( - // IntrinsicGas is an initial static amount of gas that every [Tx] must pay. - IntrinsicGas = ap5.AtomicTxIntrinsicGas - // GasPerByte is an additional amount of gas that is charged per-byte of an + // intrinsicGas is an initial static amount of gas that every [Tx] must pay. + intrinsicGas = ap5.AtomicTxIntrinsicGas + // gasPerByte is an additional amount of gas that is charged per-byte of an // [Unsigned] transaction. - GasPerByte = 1 // [atomic.TxBytesGas] - // GasPerSig is an additional amount of gas that is charged per-signature + gasPerByte = 1 // [atomic.TxBytesGas] + // gasPerSig is an additional amount of gas that is charged per-signature // included in a [Tx]. - GasPerSig = secp256k1fx.CostPerSignature + gasPerSig = secp256k1fx.CostPerSignature ) func gasUsed(t Unsigned) (uint64, error) { @@ -126,7 +130,7 @@ func gasUsed(t Unsigned) (uint64, error) { if err != nil { return 0, err } - bytesGas, err := math.Mul(uint64(numBytes), GasPerByte) + bytesGas, err := math.Mul(uint64(numBytes), gasPerByte) if err != nil { return 0, err } @@ -134,7 +138,7 @@ func gasUsed(t Unsigned) (uint64, error) { if err != nil { return 0, err } - sigsGas, err := math.Mul(numSigs, GasPerSig) + sigsGas, err := math.Mul(numSigs, gasPerSig) if err != nil { return 0, err } @@ -142,7 +146,7 @@ func gasUsed(t Unsigned) (uint64, error) { if err != nil { return 0, err } - return math.Add(IntrinsicGas, dynamicGas) + return math.Add(intrinsicGas, dynamicGas) } const x2cRateC = 1_000_000_000 From ab83fe69adf18dd8859e5b80c61ac3e69de26cee Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 20:04:46 -0400 Subject: [PATCH 006/120] wip --- vms/saevm/cchain/tx/tx.go | 10 +++++----- vms/saevm/cchain/tx/tx_test.go | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index e93f8f56165a..958559f0bf48 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -13,6 +13,9 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" + // Imported for [GasPerByte] comment resolution. + _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap5" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/hashing" @@ -20,9 +23,6 @@ import ( "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" - - // Imported for [GasPerByte] comment resolution. - _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" ) // Tx is a signed transaction that interacts with shared memory. @@ -149,11 +149,11 @@ func gasUsed(t Unsigned) (uint64, error) { return math.Add(intrinsicGas, dynamicGas) } -const x2cRateC = 1_000_000_000 +const _x2cRate = 1_000_000_000 // x2cRate is the conversion rate between the smallest denomination on the // X-Chain, 1 nAVAX, and the smallest denomination on the C-Chain 1 aAVAX. -var x2cRate = uint256.NewInt(x2cRateC) +var x2cRate = uint256.NewInt(_x2cRate) // gasPrice takes in the burned amount of AVAX in nAVAX and the gas used and // returns the price per gas in aAVAX/gas. diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index c05bf374ff1a..ba220ed83201 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -141,7 +141,7 @@ var ( Gas: 11230, GasFeeCap: *uint256.NewInt(0), Mint: map[common.Address]uint256.Int{ - common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"): *uint256.NewInt(50_000_000 * x2cRateC), + common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"): *uint256.NewInt(50_000_000 * _x2cRate), }, }, }, @@ -249,12 +249,12 @@ var ( op: hook.Op{ ID: ids.FromStringOrPanic("ng7Dox1r8nctrF6zurhRPYWxkmE2juUhT7Qhpauyo8qSEu6jB"), Gas: 11230, - GasFeeCap: *uint256.NewInt(1_000_000 * x2cRateC / 11230), + GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 11230), Burn: map[common.Address]hook.AccountDebit{ common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"): { Nonce: 0, - Amount: *uint256.NewInt(1_000_001 * x2cRateC), - MinBalance: *uint256.NewInt(1_000_001 * x2cRateC), + Amount: *uint256.NewInt(1_000_001 * _x2cRate), + MinBalance: *uint256.NewInt(1_000_001 * _x2cRate), }, }, }, @@ -544,7 +544,7 @@ func TestAsOp(t *testing.T) { } } -func FuzzAsOp_GasFeeCap(f *testing.F) { +func FuzzAsOp(f *testing.F) { for _, test := range tests { f.Add(test.bytes) } From 8f3f888991a591fdc96538745e7a5c65dee5b8e0 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 20:20:01 -0400 Subject: [PATCH 007/120] wip --- vms/saevm/cchain/tx/tx.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 958559f0bf48..7f35dbac38cd 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -90,7 +90,7 @@ func (t *Tx) Bytes() ([]byte, error) { // The operation only includes state changes that impact Ethereum-native state. // It does not include non-AVAX balance changes or shared memory modifications. func (t *Tx) AsOp(avaxAssetID ids.ID) (hook.Op, error) { - gasUsed, err := gasUsed(t.Unsigned) + gas, err := gasUsed(t.Unsigned) if err != nil { return hook.Op{}, fmt.Errorf("calculating gas used: %w", err) } @@ -107,8 +107,8 @@ func (t *Tx) AsOp(avaxAssetID ids.ID) (hook.Op, error) { return hook.Op{ ID: t.ID(), - Gas: gas.Gas(gasUsed), - GasFeeCap: gasPrice(burned, gasUsed), + Gas: gas, + GasFeeCap: gasPrice(burned, gas), Burn: op.burn, Mint: op.mint, }, nil @@ -122,15 +122,15 @@ const ( gasPerByte = 1 // [atomic.TxBytesGas] // gasPerSig is an additional amount of gas that is charged per-signature // included in a [Tx]. - gasPerSig = secp256k1fx.CostPerSignature + gasPerSig = gas.Gas(secp256k1fx.CostPerSignature) ) -func gasUsed(t Unsigned) (uint64, error) { +func gasUsed(t Unsigned) (gas.Gas, error) { numBytes, err := c.Size(codecVersion, &t) if err != nil { return 0, err } - bytesGas, err := math.Mul(uint64(numBytes), gasPerByte) + bytesGas, err := math.Mul(gas.Gas(numBytes), gasPerByte) if err != nil { return 0, err } @@ -138,7 +138,7 @@ func gasUsed(t Unsigned) (uint64, error) { if err != nil { return 0, err } - sigsGas, err := math.Mul(numSigs, gasPerSig) + sigsGas, err := math.Mul(gas.Gas(numSigs), gasPerSig) if err != nil { return 0, err } @@ -155,16 +155,16 @@ const _x2cRate = 1_000_000_000 // X-Chain, 1 nAVAX, and the smallest denomination on the C-Chain 1 aAVAX. var x2cRate = uint256.NewInt(_x2cRate) -// gasPrice takes in the burned amount of AVAX in nAVAX and the gas used and -// returns the price per gas in aAVAX/gas. +// gasPrice takes in the cost, in nAVAX, and the gas and returns the price per +// gas in aAVAX/gas. // // The result is rounded down to the nearest aAVAX/gas. -func gasPrice(burned, gasUsed uint64) uint256.Int { +func gasPrice(cost uint64, gas gas.Gas) uint256.Int { var u uint256.Int - u.SetUint64(gasUsed) + u.SetUint64(uint64(gas)) var p uint256.Int - p.SetUint64(burned) + p.SetUint64(cost) p.Mul(&p, x2cRate) p.Div(&p, &u) return p From 83cc38b84afa79b36de3de475984ca5d73655203 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 20:42:34 -0400 Subject: [PATCH 008/120] wip --- vms/saevm/cchain/tx/tx_test.go | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index ba220ed83201..09cd641052b3 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -20,6 +20,7 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/saevm/hook" @@ -544,6 +545,70 @@ func TestAsOp(t *testing.T) { } } +func TestAsOp_Errors(t *testing.T) { + tests := []struct { + name string + tx Unsigned + want error + }{ + { + name: "export_multiple_nonces", + tx: &Export{ + Ins: []Input{ + { + Nonce: 0, + }, + { + Nonce: 1, + }, + }, + }, + want: errMultipleNonces, + }, + { + name: "import_burned_underflow", + tx: &Import{ + ImportedInputs: []*avax.TransferableInput{{ + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 1, + }, + }}, + Outs: []Output{{ + AssetID: avaxAssetID, + Amount: 2, + }}, + }, + want: math.ErrUnderflow, + }, + { + name: "export_burned_underflow", + tx: &Export{ + Ins: []Input{{ + AssetID: avaxAssetID, + Amount: 1, + }}, + ExportedOutputs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 2, + }, + }}, + }, + want: math.ErrUnderflow, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := &Tx{ + Unsigned: test.tx, + } + _, err := tx.AsOp(avaxAssetID) + require.ErrorIsf(t, err, test.want, "%T.AsOp(avaxAssetID)", tx) + }) + } +} + func FuzzAsOp(f *testing.F) { for _, test := range tests { f.Add(test.bytes) From d606762484af14a4da09ef40d9d240a69ad415da Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 21:20:55 -0400 Subject: [PATCH 009/120] Extend fuzz tests --- vms/saevm/cchain/tx/tx_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 09cd641052b3..fe373e7ff87a 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -22,6 +22,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -628,8 +629,13 @@ func FuzzAsOp(f *testing.F) { newOk := newErr == nil assert.Equalf(t, oldOk, newOk, "atomic.EffectiveGasPrice(%T) == %T.AsOp().GasFeeCap", oldTx, newTx) - if newOk { - assert.Equal(t, gasPrice, op.GasFeeCap) + if !newOk { + return } + assert.Equalf(t, gasPrice, op.GasFeeCap, "atomic.EffectiveGasPrice(%T) == %T.AsOp().GasFeeCap", oldTx, newTx) + + gasUsed, err := oldTx.UnsignedAtomicTx.GasUsed(true) + require.NoErrorf(t, err, "%T.GasUsed(true)", oldTx.UnsignedAtomicTx) + assert.Equalf(t, gas.Gas(gasUsed), op.Gas, "%T.GasUsed(true) == %T.AsOp().Gas", oldTx.UnsignedAtomicTx, newTx) }) } From 0a8273453a6a88564c4fce5ea543a22512ef4e92 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 25 Apr 2026 21:24:51 -0400 Subject: [PATCH 010/120] nit --- vms/saevm/cchain/tx/tx_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index fe373e7ff87a..3f9374a2f12e 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -541,7 +541,7 @@ func TestAsOp(t *testing.T) { t.Run(test.name, func(t *testing.T) { got, err := test.new.AsOp(avaxAssetID) require.NoErrorf(t, err, "%T.AsOp(avaxAssetID)", test.new) - require.Equalf(t, test.op, got, "%T.AsOp(avaxAssetID)", test.new) + assert.Equalf(t, test.op, got, "%T.AsOp(avaxAssetID)", test.new) }) } } From 5b86b6bf99231f36615db3e484d4fa89af216e4e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 26 Apr 2026 09:15:22 -0400 Subject: [PATCH 011/120] wip --- vms/saevm/cchain/tx/tx_test.go | 232 +++++++++++++++++++++++++++++++-- 1 file changed, 220 insertions(+), 12 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 3f9374a2f12e..9858412d01d1 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -261,6 +261,216 @@ var ( }, }, }, + { + name: "import_multi_input", // Included in https://subnets.avax.network/c-chain/block/132481 + old: &atomic.Tx{ + UnsignedAtomicTx: &atomic.UnsignedImportTx{ + NetworkID: 1, + BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), + SourceChain: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + ImportedInputs: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: ids.FromStringOrPanic("DqRKjysHeiKWetgyqqM2WdnX56yg8wBdY95RhuP3eDbbVoMCH"), + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 99000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: ids.FromStringOrPanic("25YuXY1zoYY3DgLsRbGjdNSx3jYtvqZRgFo6jpy7EMCfUn4S74"), + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 399000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: ids.FromStringOrPanic("2DXSj1kzqWM5HWS2PXcDSD3GUNpEGinynV1qD6LxiECHmZC8fj"), + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 99000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + }, + Outs: []atomic.EVMOutput{ + { + Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), + Amount: 99000000, + AssetID: avaxAssetID, + }, + { + Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), + Amount: 99000000, + AssetID: avaxAssetID, + }, + { + Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), + Amount: 399000000, + AssetID: avaxAssetID, + }, + }, + }, + Creds: []verify.Verifiable{ + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700")), + }, + }, + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700")), + }, + }, + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700")), + }, + }, + }, + }, + new: &Tx{ + Unsigned: &Import{ + NetworkID: 1, + BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), + SourceChain: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + ImportedInputs: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: ids.FromStringOrPanic("DqRKjysHeiKWetgyqqM2WdnX56yg8wBdY95RhuP3eDbbVoMCH"), + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 99000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: ids.FromStringOrPanic("25YuXY1zoYY3DgLsRbGjdNSx3jYtvqZRgFo6jpy7EMCfUn4S74"), + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 399000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: ids.FromStringOrPanic("2DXSj1kzqWM5HWS2PXcDSD3GUNpEGinynV1qD6LxiECHmZC8fj"), + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 99000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + }, + Outs: []Output{ + { + Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), + Amount: 99000000, + AssetID: avaxAssetID, + }, + { + Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), + Amount: 99000000, + AssetID: avaxAssetID, + }, + { + Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), + Amount: 399000000, + AssetID: avaxAssetID, + }, + }, + }, + Creds: []Credential{ + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700")), + }, + }, + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700")), + }, + }, + &secp256k1fx.Credential{ + Sigs: [][65]byte{ + [65]byte(common.FromHex("0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700")), + }, + }, + }, + }, + json: `{ + "unsignedTx":{ + "networkID":1, + "blockchainID":"2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", + "sourceChain":"2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", + "importedInputs":[ + { + "txID":"DqRKjysHeiKWetgyqqM2WdnX56yg8wBdY95RhuP3eDbbVoMCH", + "outputIndex":0, + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID":"11111111111111111111111111111111LpoYY", + "input":{"amount":99000000,"signatureIndices":[0]} + }, + { + "txID":"25YuXY1zoYY3DgLsRbGjdNSx3jYtvqZRgFo6jpy7EMCfUn4S74", + "outputIndex":0, + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID":"11111111111111111111111111111111LpoYY", + "input":{"amount":399000000,"signatureIndices":[0]} + }, + { + "txID":"2DXSj1kzqWM5HWS2PXcDSD3GUNpEGinynV1qD6LxiECHmZC8fj", + "outputIndex":0, + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID":"11111111111111111111111111111111LpoYY", + "input":{"amount":99000000,"signatureIndices":[0]} + } + ], + "outputs":[ + {"address":"0x383c293db6be7ac246f0956ad632344dc2cd1da3","amount":99000000,"assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"}, + {"address":"0x383c293db6be7ac246f0956ad632344dc2cd1da3","amount":99000000,"assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"}, + {"address":"0x383c293db6be7ac246f0956ad632344dc2cd1da3","amount":399000000,"assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"} + ] + }, + "credentials":[ + {"signatures":["0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700"]}, + {"signatures":["0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700"]}, + {"signatures":["0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700"]} + ] + }`, + bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b000000031d249d0aab138afe01e6eff9c4789018a600771d94f5396b5df7b9d05298714d0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000005e69ec000000001000000008e0713e47bfc29bef4cee6e4635da1c74a3aabade68ccad6fca3e99fd827eb1c0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000017c841c00000000100000000a022a8b069a5d5e54c7e09c5c5b0f762c6751068bef15fe951a5e4b349d642200000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000005e69ec0000000010000000000000003383c293db6be7ac246f0956ad632344dc2cd1da30000000005e69ec021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff383c293db6be7ac246f0956ad632344dc2cd1da30000000005e69ec021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff383c293db6be7ac246f0956ad632344dc2cd1da30000000017c841c021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000300000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b342570000000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b342570000000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700"), + op: hook.Op{ + ID: ids.FromStringOrPanic("2Av7bXLRwxiQhbT9EcQd8KRM3Lz6VkpTqf3Y1AT5peHZ4YAohS"), + Gas: 13526, + GasFeeCap: *uint256.NewInt(0), + Mint: map[common.Address]uint256.Int{ + common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"): *uint256.NewInt(597_000_000 * _x2cRate), + }, + }, + }, } oldSlice []*atomic.Tx newSlice []*Tx @@ -615,23 +825,21 @@ func FuzzAsOp(f *testing.F) { f.Add(test.bytes) } f.Fuzz(func(t *testing.T, data []byte) { - oldTx, err := parseOldTx(data) + newTx, err := Parse(data) if err != nil { - t.Skip("invalid tx") + t.Skip("invalid tx bytes") } - newTx, err := Parse(data) - require.NoError(t, err, "Parse()") + op, err := newTx.AsOp(avaxAssetID) + if err != nil { + t.Skip("invalid tx") + } - gasPrice, oldErr := atomic.EffectiveGasPrice(oldTx.UnsignedAtomicTx, avaxAssetID, true) - oldOk := oldErr == nil - op, newErr := newTx.AsOp(avaxAssetID) - newOk := newErr == nil - assert.Equalf(t, oldOk, newOk, "atomic.EffectiveGasPrice(%T) == %T.AsOp().GasFeeCap", oldTx, newTx) + oldTx, err := parseOldTx(data) + require.NoError(t, err, "parseOldTx()") - if !newOk { - return - } + gasPrice, err := atomic.EffectiveGasPrice(oldTx.UnsignedAtomicTx, avaxAssetID, true) + require.NoErrorf(t, err, "atomic.EffectiveGasPrice(%T, avaxAssetID, true)", oldTx) assert.Equalf(t, gasPrice, op.GasFeeCap, "atomic.EffectiveGasPrice(%T) == %T.AsOp().GasFeeCap", oldTx, newTx) gasUsed, err := oldTx.UnsignedAtomicTx.GasUsed(true) From 3d92bd5c8a8148ef60eba049dc158bc32bf3ad64 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 26 Apr 2026 10:12:34 -0400 Subject: [PATCH 012/120] wip --- vms/saevm/cchain/tx/tx_test.go | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 9858412d01d1..da7ff286324c 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -471,6 +471,71 @@ var ( }, }, }, + { + name: "export_same_address_multi_asset", // Synthetic + old: &atomic.Tx{ + UnsignedAtomicTx: &atomic.UnsignedExportTx{ + Ins: []atomic.EVMInput{ + { + Amount: 999, + AssetID: ids.ID{}, + Nonce: 5, + }, + { + Amount: 1_000_000, + AssetID: avaxAssetID, + Nonce: 5, + }, + }, + ExportedOutputs: []*avax.TransferableOutput{}, + }, + Creds: []verify.Verifiable{}, + }, + new: &Tx{ + Unsigned: &Export{ + Ins: []Input{ + { + Amount: 999, + AssetID: ids.ID{}, + Nonce: 5, + }, + { + Amount: 1_000_000, + AssetID: avaxAssetID, + Nonce: 5, + }, + }, + ExportedOutputs: []*avax.TransferableOutput{}, + }, + Creds: []Credential{}, + }, + json: `{ + "unsignedTx":{ + "networkID":0, + "blockchainID":"11111111111111111111111111111111LpoYY", + "destinationChain":"11111111111111111111111111111111LpoYY", + "inputs":[ + {"address":"0x0000000000000000000000000000000000000000","amount":999,"assetID":"11111111111111111111111111111111LpoYY","nonce":5}, + {"address":"0x0000000000000000000000000000000000000000","amount":1000000,"assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z","nonce":5} + ], + "exportedOutputs":[] + }, + "credentials":[] + }`, + bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000050000000000000000"), + op: hook.Op{ + ID: ids.FromStringOrPanic("29cCETWxEUN1QCuex59j46Xtr8urBRo5M7HzwBqC3qDXWd73sX"), + Gas: 12218, + GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 12218), + Burn: map[common.Address]hook.AccountDebit{ + common.Address{}: { + Nonce: 5, + Amount: *uint256.NewInt(1_000_000 * _x2cRate), + MinBalance: *uint256.NewInt(1_000_000 * _x2cRate), + }, + }, + }, + }, } oldSlice []*atomic.Tx newSlice []*Tx From 893e17ececb469dccc9b355ab9f4f7abc8457bf6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 26 Apr 2026 10:20:42 -0400 Subject: [PATCH 013/120] wip --- vms/saevm/cchain/tx/tx_test.go | 72 +++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index da7ff286324c..c0cee4b6d8d9 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -477,9 +477,8 @@ var ( UnsignedAtomicTx: &atomic.UnsignedExportTx{ Ins: []atomic.EVMInput{ { - Amount: 999, - AssetID: ids.ID{}, - Nonce: 5, + Amount: 999, + Nonce: 5, }, { Amount: 1_000_000, @@ -495,9 +494,8 @@ var ( Unsigned: &Export{ Ins: []Input{ { - Amount: 999, - AssetID: ids.ID{}, - Nonce: 5, + Amount: 999, + Nonce: 5, }, { Amount: 1_000_000, @@ -536,6 +534,68 @@ var ( }, }, }, + { + name: "import_non_avax", // Synthetic + old: &atomic.Tx{ + UnsignedAtomicTx: &atomic.UnsignedImportTx{ + ImportedInputs: []*avax.TransferableInput{{ + In: &secp256k1fx.TransferInput{ + Amt: 999, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }}, + Outs: []atomic.EVMOutput{{ + Amount: 999, + }}, + }, + Creds: []verify.Verifiable{}, + }, + new: &Tx{ + Unsigned: &Import{ + ImportedInputs: []*avax.TransferableInput{{ + In: &secp256k1fx.TransferInput{ + Amt: 999, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }}, + Outs: []Output{{ + Amount: 999, + }}, + }, + Creds: []Credential{}, + }, + json: `{ + "unsignedTx":{ + "networkID":0, + "blockchainID":"11111111111111111111111111111111LpoYY", + "sourceChain":"11111111111111111111111111111111LpoYY", + "importedInputs":[{ + "txID":"11111111111111111111111111111111LpoYY", + "outputIndex":0, + "assetID":"11111111111111111111111111111111LpoYY", + "fxID":"11111111111111111111111111111111LpoYY", + "input":{"amount":999,"signatureIndices":[]} + }], + "outputs":[{ + "address":"0x0000000000000000000000000000000000000000", + "amount":999, + "assetID":"11111111111111111111111111111111LpoYY" + }] + }, + "credentials":[] + }`, + bytes: common.FromHex("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000003e70000000000000001000000000000000000000000000000000000000000000000000003e7000000000000000000000000000000000000000000000000000000000000000000000000"), + op: hook.Op{ + ID: ids.FromStringOrPanic("s4xoHkf4rPQYSwjbQo78hcSP1wSeViV1Fx2PHM4AfRiDurFkf"), + Gas: 10226, + GasFeeCap: *uint256.NewInt(0), + Mint: map[common.Address]uint256.Int{}, + }, + }, } oldSlice []*atomic.Tx newSlice []*Tx From 83b0ec01b17393d8400664c84850d96330b6bf52 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 26 Apr 2026 10:23:31 -0400 Subject: [PATCH 014/120] wip --- vms/saevm/cchain/tx/tx_test.go | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index c0cee4b6d8d9..4edc04a28a47 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -534,6 +534,76 @@ var ( }, }, }, + { + name: "export_multi_address_multi_asset", // Synthetic + old: &atomic.Tx{ + UnsignedAtomicTx: &atomic.UnsignedExportTx{ + Ins: []atomic.EVMInput{ + { + Address: common.Address{1}, + Amount: 999, + Nonce: 5, + }, + { + Address: common.Address{2}, + Amount: 1_000_000, + AssetID: avaxAssetID, + Nonce: 7, + }, + }, + ExportedOutputs: []*avax.TransferableOutput{}, + }, + Creds: []verify.Verifiable{}, + }, + new: &Tx{ + Unsigned: &Export{ + Ins: []Input{ + { + Address: common.Address{1}, + Amount: 999, + Nonce: 5, + }, + { + Address: common.Address{2}, + Amount: 1_000_000, + AssetID: avaxAssetID, + Nonce: 7, + }, + }, + ExportedOutputs: []*avax.TransferableOutput{}, + }, + Creds: []Credential{}, + }, + json: `{ + "unsignedTx":{ + "networkID":0, + "blockchainID":"11111111111111111111111111111111LpoYY", + "destinationChain":"11111111111111111111111111111111LpoYY", + "inputs":[ + {"address":"0x0100000000000000000000000000000000000000","amount":999,"assetID":"11111111111111111111111111111111LpoYY","nonce":5}, + {"address":"0x0200000000000000000000000000000000000000","amount":1000000,"assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z","nonce":7} + ], + "exportedOutputs":[] + }, + "credentials":[] + }`, + bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002010000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005020000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000070000000000000000"), + op: hook.Op{ + ID: ids.FromStringOrPanic("8P9XRKhxHeTv3t4Aj9cTV6dD5h78WVFH8nctLuCkeSavfKeEG"), + Gas: 12218, + GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 12218), + Burn: map[common.Address]hook.AccountDebit{ + {1}: { + Nonce: 5, + }, + {2}: { + Nonce: 7, + Amount: *uint256.NewInt(1_000_000 * _x2cRate), + MinBalance: *uint256.NewInt(1_000_000 * _x2cRate), + }, + }, + }, + }, { name: "import_non_avax", // Synthetic old: &atomic.Tx{ From c46bcc260e92404684e3cf89d059ffd9e0809dfa Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 26 Apr 2026 10:41:16 -0400 Subject: [PATCH 015/120] nit --- vms/saevm/cchain/tx/BUILD.bazel | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index 7160fb3adea7..a8f53eab7dfb 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -14,6 +14,7 @@ go_library( deps = [ "//codec", "//codec/linearcodec", + "//graft/coreth/plugin/evm/atomic", "//graft/coreth/plugin/evm/upgrade/ap5", "//ids", "//utils/hashing", @@ -37,7 +38,9 @@ go_test( "//graft/coreth/plugin/evm/atomic", "//graft/coreth/plugin/evm/atomic/vm", "//ids", + "//utils/math", "//vms/components/avax", + "//vms/components/gas", "//vms/components/verify", "//vms/saevm/hook", "//vms/secp256k1fx", From e92638f56f8de11e3da26573de304a207060c1cf Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 26 Apr 2026 10:50:18 -0400 Subject: [PATCH 016/120] nit --- vms/saevm/cchain/tx/export.go | 9 +++------ vms/saevm/cchain/tx/import.go | 5 +---- vms/saevm/cchain/tx/tx.go | 13 ++++++++++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 45292f96126b..880b9f8b18ec 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/ava-labs/libevm/common" - "github.com/holiman/uint256" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/math" @@ -78,12 +77,10 @@ func (e *Export) asOp(avaxAssetID ids.ID) (op, error) { return op{}, fmt.Errorf("%w: address %s has nonces %d and %d", errMultipleNonces, in.Address, debit.Nonce, in.Nonce) } - // Only AVAX assets are transferred through the SAE ops, but SAE owns - // all nonce modifications. + // Non-AVAX inputs still record the address+nonce so SAE will increment + // the nonce, even though no AVAX is debited. if in.AssetID == avaxAssetID { - var amount uint256.Int - amount.SetUint64(in.Amount) - amount.Mul(&amount, x2cRate) + amount := scaleAVAX(in.Amount) if _, overflow := debit.Amount.AddOverflow(&debit.Amount, &amount); overflow { return op{}, fmt.Errorf("%w: for address %s", errOverflow, in.Address) } diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 00074884552d..82b83c6b1091 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -86,10 +86,7 @@ func (i *Import) asOp(avaxAssetID ids.ID) (op, error) { continue } - var amount uint256.Int - amount.SetUint64(out.Amount) - amount.Mul(&amount, x2cRate) - + amount := scaleAVAX(out.Amount) total := mint[out.Address] if _, overflow := total.AddOverflow(&total, &amount); overflow { return op{}, fmt.Errorf("%w: for address %s", errOverflow, out.Address) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 7f35dbac38cd..3294e05f929d 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -155,6 +155,15 @@ const _x2cRate = 1_000_000_000 // X-Chain, 1 nAVAX, and the smallest denomination on the C-Chain 1 aAVAX. var x2cRate = uint256.NewInt(_x2cRate) +// scaleAVAX converts an amount denominated in nAVAX into the C-Chain's aAVAX +// denomination. +func scaleAVAX(nAVAX uint64) uint256.Int { + var aAVAX uint256.Int + aAVAX.SetUint64(nAVAX) + aAVAX.Mul(&aAVAX, x2cRate) + return aAVAX +} + // gasPrice takes in the cost, in nAVAX, and the gas and returns the price per // gas in aAVAX/gas. // @@ -163,9 +172,7 @@ func gasPrice(cost uint64, gas gas.Gas) uint256.Int { var u uint256.Int u.SetUint64(uint64(gas)) - var p uint256.Int - p.SetUint64(cost) - p.Mul(&p, x2cRate) + p := scaleAVAX(cost) p.Div(&p, &u) return p } From f2faffbbedd7076b807ef5aeb5204a81a789ace2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 26 Apr 2026 14:13:16 -0400 Subject: [PATCH 017/120] lint --- vms/saevm/cchain/tx/tx_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 4edc04a28a47..9f5f244c97ab 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -526,7 +526,7 @@ var ( Gas: 12218, GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 12218), Burn: map[common.Address]hook.AccountDebit{ - common.Address{}: { + {}: { Nonce: 5, Amount: *uint256.NewInt(1_000_000 * _x2cRate), MinBalance: *uint256.NewInt(1_000_000 * _x2cRate), From 7447b08c5a7244a817b739a00591b68f1d40413b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 26 Apr 2026 14:35:50 -0400 Subject: [PATCH 018/120] lint --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 3294e05f929d..b7fb8c6ecbef 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -130,7 +130,7 @@ func gasUsed(t Unsigned) (gas.Gas, error) { if err != nil { return 0, err } - bytesGas, err := math.Mul(gas.Gas(numBytes), gasPerByte) + bytesGas, err := math.Mul(gas.Gas(numBytes), gasPerByte) //#nosec G115 -- Known non-negative if err != nil { return 0, err } From 8c3b6f05b3ebc387e048fc2a4a01b6ff5d939172 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 27 Apr 2026 15:31:53 -0400 Subject: [PATCH 019/120] nits --- vms/saevm/cchain/tx/tx_test.go | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index e44c1eda3753..2e530c03e862 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -146,7 +146,6 @@ var ( Address: common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), Amount: 1000001, AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), - Nonce: 0, }}, ExportedOutputs: []*avax.TransferableOutput{{ Asset: avax.Asset{ @@ -155,7 +154,6 @@ var ( Out: &secp256k1fx.TransferOutput{ Amt: 1, OutputOwners: secp256k1fx.OutputOwners{ - Locktime: 0, Threshold: 1, Addrs: []ids.ShortID{ ids.ShortFromStringOrPanic("LanVZgBDVvtarbTXD1uU7r1nXVJyLmPUz"), @@ -181,7 +179,6 @@ var ( Address: common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), Amount: 1000001, AssetID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), - Nonce: 0, }}, ExportedOutputs: []*avax.TransferableOutput{{ Asset: avax.Asset{ @@ -190,7 +187,6 @@ var ( Out: &secp256k1fx.TransferOutput{ Amt: 1, OutputOwners: secp256k1fx.OutputOwners{ - Locktime: 0, Threshold: 1, Addrs: []ids.ShortID{ ids.ShortFromStringOrPanic("LanVZgBDVvtarbTXD1uU7r1nXVJyLmPUz"), @@ -239,16 +235,16 @@ var ( bytes: common.FromHex("0x000000000001000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001eb019ccd325ad53543a7e7e3b04828bdecf3cff600000000000f424121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000000000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500000000010000000900000001254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01"), }, } - oldSlice []*atomic.Tx - newSlice []*Tx + oldTxs []*atomic.Tx + newTxs []*Tx ) func init() { - oldSlice = make([]*atomic.Tx, len(tests)) - newSlice = make([]*Tx, len(tests)) + oldTxs = make([]*atomic.Tx, len(tests)) + newTxs = make([]*Tx, len(tests)) for i, test := range tests { - oldSlice[i] = test.old - newSlice[i] = test.new + oldTxs[i] = test.old + newTxs[i] = test.new } } @@ -286,8 +282,8 @@ func TestBytes(t *testing.T) { } func TestMarshalSlice(t *testing.T) { - want, err := atomic.Codec.Marshal(atomic.CodecVersion, oldSlice) - require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldSlice) + want, err := atomic.Codec.Marshal(atomic.CodecVersion, oldTxs) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldTxs) tests := []struct { name string @@ -296,7 +292,7 @@ func TestMarshalSlice(t *testing.T) { }{ { name: "mainnet", - txs: newSlice, + txs: newTxs, want: want, }, { @@ -331,8 +327,8 @@ func TestParse(t *testing.T) { } func TestParseSlice(t *testing.T) { - bytes, err := atomic.Codec.Marshal(atomic.CodecVersion, oldSlice) - require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldSlice) + bytes, err := atomic.Codec.Marshal(atomic.CodecVersion, oldTxs) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldTxs) tests := []struct { name string @@ -343,7 +339,7 @@ func TestParseSlice(t *testing.T) { { name: "mainnet", bytes: bytes, - want: newSlice, + want: newTxs, }, { name: "empty", @@ -421,7 +417,7 @@ func FuzzParseCompatibility(f *testing.F) { func FuzzParseSliceCompatibility(f *testing.F) { { - b, err := MarshalSlice(newSlice) + b, err := MarshalSlice(newTxs) require.NoError(f, err, "MarshalSlice()") f.Add(b) } @@ -455,7 +451,7 @@ func FuzzParseRoundTrip(f *testing.F) { func FuzzParseSliceRoundTrip(f *testing.F) { { - b, err := MarshalSlice(newSlice) + b, err := MarshalSlice(newTxs) require.NoError(f, err, "MarshalSlice()") f.Add(b) } From 03699669ccdca5f460ede8d449fc0c3b8d511bb9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 09:06:47 -0400 Subject: [PATCH 020/120] fix bazel --- graft/coreth/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/graft/coreth/BUILD.bazel b/graft/coreth/BUILD.bazel index 85f039a81f54..4f1f6adac941 100644 --- a/graft/coreth/BUILD.bazel +++ b/graft/coreth/BUILD.bazel @@ -19,6 +19,7 @@ package_group( "//tests/reexecute/c", "//tests/reexecute/chaos", "//vms/evm/emulate", + "//vms/saevm/cchain/...", "//wallet/chain/c", "//wallet/subnet/primary", ], From 85ae854c1a6b8e8278142741bbc5ed73d84e1c6e Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer Date: Tue, 28 Apr 2026 14:59:51 +0200 Subject: [PATCH 021/120] ci: free runner disk before publishing Antithesis images (#5310) --- .github/workflows/publish_antithesis_images.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/publish_antithesis_images.yml b/.github/workflows/publish_antithesis_images.yml index 77b34e336073..9ad9295f6b3f 100644 --- a/.github/workflows/publish_antithesis_images.yml +++ b/.github/workflows/publish_antithesis_images.yml @@ -21,6 +21,11 @@ jobs: runs-on: ubuntu-latest steps: + - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + tool-cache: false + docker-images: true + - name: Checkout Repository uses: actions/checkout@v4 From 06d2a413d14cb7fc5d871d5078e53da7edd4b9fc Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 12:37:43 -0400 Subject: [PATCH 022/120] nit --- vms/saevm/cchain/tx/tx_test.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 5cb15cc4c86d..159665105329 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -139,9 +139,8 @@ var ( }`, bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001c52b712aa7dce27a650bf509f799673e245edd4fa9e4e1700eb6105202fe579a0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000002faf080000000010000000000000001b8b5a87d1c05676f1f966da49151fa54dbe68c330000000002faf08021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000100000009000000013e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01"), op: hook.Op{ - ID: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), - Gas: 11230, - GasFeeCap: *uint256.NewInt(0), + ID: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), + Gas: 11230, Mint: map[common.Address]uint256.Int{ common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"): *uint256.NewInt(50_000_000 * _x2cRate), }, @@ -458,9 +457,8 @@ var ( }`, bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b000000031d249d0aab138afe01e6eff9c4789018a600771d94f5396b5df7b9d05298714d0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000005e69ec000000001000000008e0713e47bfc29bef4cee6e4635da1c74a3aabade68ccad6fca3e99fd827eb1c0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000017c841c00000000100000000a022a8b069a5d5e54c7e09c5c5b0f762c6751068bef15fe951a5e4b349d642200000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000005e69ec0000000010000000000000003383c293db6be7ac246f0956ad632344dc2cd1da30000000005e69ec021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff383c293db6be7ac246f0956ad632344dc2cd1da30000000005e69ec021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff383c293db6be7ac246f0956ad632344dc2cd1da30000000017c841c021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000300000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b342570000000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b342570000000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700"), op: hook.Op{ - ID: ids.FromStringOrPanic("2Av7bXLRwxiQhbT9EcQd8KRM3Lz6VkpTqf3Y1AT5peHZ4YAohS"), - Gas: 13526, - GasFeeCap: *uint256.NewInt(0), + ID: ids.FromStringOrPanic("2Av7bXLRwxiQhbT9EcQd8KRM3Lz6VkpTqf3Y1AT5peHZ4YAohS"), + Gas: 13526, Mint: map[common.Address]uint256.Int{ common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"): *uint256.NewInt(597_000_000 * _x2cRate), }, @@ -655,10 +653,9 @@ var ( }`, bytes: common.FromHex("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000003e70000000000000001000000000000000000000000000000000000000000000000000003e7000000000000000000000000000000000000000000000000000000000000000000000000"), op: hook.Op{ - ID: ids.FromStringOrPanic("s4xoHkf4rPQYSwjbQo78hcSP1wSeViV1Fx2PHM4AfRiDurFkf"), - Gas: 10226, - GasFeeCap: *uint256.NewInt(0), - Mint: map[common.Address]uint256.Int{}, + ID: ids.FromStringOrPanic("s4xoHkf4rPQYSwjbQo78hcSP1wSeViV1Fx2PHM4AfRiDurFkf"), + Gas: 10226, + Mint: map[common.Address]uint256.Int{}, }, }, } From c28b2d29bb054b0313a799ef94cf4a95e590b448 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 14:52:09 -0400 Subject: [PATCH 023/120] fuzz full op --- vms/saevm/cchain/tx/tx_test.go | 88 +++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 159665105329..588bcef33364 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -6,6 +6,7 @@ package tx import ( "encoding/json" "errors" + "math/big" "testing" "github.com/ava-labs/libevm/common" @@ -20,12 +21,14 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + safemath "github.com/ava-labs/avalanchego/utils/math" ) // tests is defined at the package level to allow sharing between fuzz tests and @@ -977,7 +980,7 @@ func TestAsOp_Errors(t *testing.T) { Amount: 2, }}, }, - want: math.ErrUnderflow, + want: safemath.ErrUnderflow, }, { name: "export_burned_underflow", @@ -993,7 +996,7 @@ func TestAsOp_Errors(t *testing.T) { }, }}, }, - want: math.ErrUnderflow, + want: safemath.ErrUnderflow, }, } for _, test := range tests { @@ -1025,12 +1028,83 @@ func FuzzAsOp(f *testing.F) { oldTx, err := parseOldTx(data) require.NoError(t, err, "parseOldTx()") + gasUsed, err := oldTx.UnsignedAtomicTx.GasUsed(true) + require.NoErrorf(t, err, "%T.GasUsed(true)", oldTx.UnsignedAtomicTx) + gasPrice, err := atomic.EffectiveGasPrice(oldTx.UnsignedAtomicTx, avaxAssetID, true) require.NoErrorf(t, err, "atomic.EffectiveGasPrice(%T, avaxAssetID, true)", oldTx) - assert.Equalf(t, gasPrice, op.GasFeeCap, "atomic.EffectiveGasPrice(%T) == %T.AsOp().GasFeeCap", oldTx, newTx) - gasUsed, err := oldTx.UnsignedAtomicTx.GasUsed(true) - require.NoErrorf(t, err, "%T.GasUsed(true)", oldTx.UnsignedAtomicTx) - assert.Equalf(t, gas.Gas(gasUsed), op.Gas, "%T.GasUsed(true) == %T.AsOp().Gas", oldTx.UnsignedAtomicTx, newTx) + state := newFuzzStateDB() + if export, ok := oldTx.UnsignedAtomicTx.(*atomic.UnsignedExportTx); ok { + for _, in := range export.Ins { + state.initialNonces[in.Address] = in.Nonce + } + } + + ctx := &snow.Context{AVAXAssetID: avaxAssetID} + require.NoErrorf(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, state), "%T.EVMStateTransfer()", oldTx.UnsignedAtomicTx) + + expected := hook.Op{ + ID: oldTx.ID(), + Gas: gas.Gas(gasUsed), + GasFeeCap: gasPrice, + Burn: state.op.burn, + Mint: state.op.mint, + } + if diff := cmp.Diff(expected, op, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("%T.AsOp() diff (-want +got):\n%s", newTx, diff) + } }) } + +// fuzzStateDB is an in-memory [atomic.StateDB] for [FuzzAsOp]. It constructs an +// [op] from the mutations of [atomic.UnsignedAtomicTx.EVMStateTransfer]. +type fuzzStateDB struct { + initialNonces map[common.Address]uint64 + op op +} + +func newFuzzStateDB() *fuzzStateDB { + return &fuzzStateDB{ + initialNonces: make(map[common.Address]uint64), + op: op{ + burn: make(map[common.Address]hook.AccountDebit), + mint: make(map[common.Address]uint256.Int), + }, + } +} + +func (s *fuzzStateDB) AddBalance(addr common.Address, amount *uint256.Int) { + b := s.op.mint[addr] + b.Add(&b, amount) + s.op.mint[addr] = b +} + +func (s *fuzzStateDB) SubBalance(addr common.Address, amount *uint256.Int) { + d := s.op.burn[addr] + d.Amount.Add(&d.Amount, amount) + d.MinBalance = d.Amount + s.op.burn[addr] = d +} + +func (*fuzzStateDB) GetBalance(common.Address) *uint256.Int { + return new(uint256.Int).Lsh(uint256.NewInt(1), 128) +} + +func (*fuzzStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} + +func (*fuzzStateDB) SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} + +func (*fuzzStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { + return new(big.Int).Lsh(big.NewInt(1), 128) +} + +func (s *fuzzStateDB) SetNonce(addr common.Address, nonce uint64) { + d := s.op.burn[addr] + d.Nonce = nonce - 1 + s.op.burn[addr] = d +} + +func (s *fuzzStateDB) GetNonce(addr common.Address) uint64 { + return s.initialNonces[addr] +} From aba7590a10700d6ff38b3330712c2ecdde2f1d49 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 14:55:22 -0400 Subject: [PATCH 024/120] mark as compatibility --- vms/saevm/cchain/tx/tx_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 588bcef33364..f0de37d520c9 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1010,7 +1010,7 @@ func TestAsOp_Errors(t *testing.T) { } } -func FuzzAsOp(f *testing.F) { +func FuzzAsOpCompatibility(f *testing.F) { for _, test := range tests { f.Add(test.bytes) } From ca2a5eae9ec13784b09e544aa1da3816b831537c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 14:59:04 -0400 Subject: [PATCH 025/120] nit --- vms/saevm/cchain/tx/tx_test.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index f0de37d520c9..f70eb572113a 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1074,20 +1074,21 @@ func newFuzzStateDB() *fuzzStateDB { } } -func (s *fuzzStateDB) AddBalance(addr common.Address, amount *uint256.Int) { - b := s.op.mint[addr] +func (f *fuzzStateDB) AddBalance(addr common.Address, amount *uint256.Int) { + b := f.op.mint[addr] b.Add(&b, amount) - s.op.mint[addr] = b + f.op.mint[addr] = b } -func (s *fuzzStateDB) SubBalance(addr common.Address, amount *uint256.Int) { - d := s.op.burn[addr] +func (f *fuzzStateDB) SubBalance(addr common.Address, amount *uint256.Int) { + d := f.op.burn[addr] d.Amount.Add(&d.Amount, amount) d.MinBalance = d.Amount - s.op.burn[addr] = d + f.op.burn[addr] = d } func (*fuzzStateDB) GetBalance(common.Address) *uint256.Int { + // Large enough to never underflow, but small enough to never overflow. return new(uint256.Int).Lsh(uint256.NewInt(1), 128) } @@ -1096,15 +1097,16 @@ func (*fuzzStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) { func (*fuzzStateDB) SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} func (*fuzzStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { + // Large enough to never underflow, but small enough to never overflow. return new(big.Int).Lsh(big.NewInt(1), 128) } -func (s *fuzzStateDB) SetNonce(addr common.Address, nonce uint64) { - d := s.op.burn[addr] +func (f *fuzzStateDB) SetNonce(addr common.Address, nonce uint64) { + d := f.op.burn[addr] d.Nonce = nonce - 1 - s.op.burn[addr] = d + f.op.burn[addr] = d } -func (s *fuzzStateDB) GetNonce(addr common.Address) uint64 { - return s.initialNonces[addr] +func (f *fuzzStateDB) GetNonce(addr common.Address) uint64 { + return f.initialNonces[addr] } From 4be05cfcfcc318b383fd0ea68629fb0078643500 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 15:17:29 -0400 Subject: [PATCH 026/120] nit --- vms/saevm/cchain/tx/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index a8f53eab7dfb..d7bd0b7fcea4 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -38,6 +38,7 @@ go_test( "//graft/coreth/plugin/evm/atomic", "//graft/coreth/plugin/evm/atomic/vm", "//ids", + "//snow", "//utils/math", "//vms/components/avax", "//vms/components/gas", From e721e6cb2943f352ce8f560f2a8b3586b0299c61 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 17:14:17 -0400 Subject: [PATCH 027/120] wip --- vms/saevm/cchain/tx/export.go | 31 +++++++++++++++++++++++++++++++ vms/saevm/cchain/tx/import.go | 10 ++++++++++ vms/saevm/cchain/tx/tx.go | 13 ++++++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 880b9f8b18ec..84740831a86d 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -9,6 +9,7 @@ import ( "github.com/ava-labs/libevm/common" + "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -94,3 +95,33 @@ func (e *Export) asOp(avaxAssetID ids.ID) (op, error) { burn: burn, }, nil } + +func (e *Export) atomicRequests(txID ids.ID) (ids.ID, *atomic.Requests, error) { + elems := make([]*atomic.Element, len(e.ExportedOutputs)) + for i, out := range e.ExportedOutputs { + utxo := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(i), + }, + Asset: out.Asset, + Out: out.Out, + } + + utxoBytes, err := c.Marshal(codecVersion, utxo) + if err != nil { + return ids.ID{}, nil, err + } + utxoID := utxo.InputID() + elem := &atomic.Element{ + Key: utxoID[:], + Value: utxoBytes, + } + if out, ok := utxo.Out.(avax.Addressable); ok { + elem.Traits = out.Addresses() + } + + elems[i] = elem + } + return e.DestinationChain, &atomic.Requests{PutRequests: elems}, nil +} diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 82b83c6b1091..56d553104192 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" + "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -97,3 +98,12 @@ func (i *Import) asOp(avaxAssetID ids.ID) (op, error) { mint: mint, }, nil } + +func (i *Import) atomicRequests(ids.ID) (ids.ID, *atomic.Requests, error) { + utxoIDs := make([][]byte, len(i.ImportedInputs)) + for j, in := range i.ImportedInputs { + inputID := in.InputID() + utxoIDs[j] = inputID[:] + } + return i.SourceChain, &atomic.Requests{RemoveRequests: utxoIDs}, nil +} diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index b7fb8c6ecbef..d2991d20dc90 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -13,9 +13,10 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" - // Imported for [GasPerByte] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + // Imported for [GasPerByte] comment resolution. + "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap5" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/hashing" @@ -49,6 +50,10 @@ type Unsigned interface { // asOp returns the operation that this transaction performs on the EVM // state. asOp(avaxAssetID ids.ID) (op, error) + + // atomicRequests returns the operations that should be applied to shared + // memory when this transaction is executed. + atomicRequests(txID ids.ID) (chainID ids.ID, requests *atomic.Requests, err error) } type op struct { @@ -177,6 +182,12 @@ func gasPrice(cost uint64, gas gas.Gas) uint256.Int { return p } +// AtomicRequests returns chainID and modifications into shared memory that this +// transaction should perform during execution. +func (t *Tx) AtomicRequests() (ids.ID, *atomic.Requests, error) { + return t.Unsigned.atomicRequests(t.ID()) +} + // Parse deserializes a [Tx] from its canonical binary format. func Parse(b []byte) (*Tx, error) { var tx Tx From 9b4c83f231b79b613421305c96ba0e8e5363a681 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 17:15:15 -0400 Subject: [PATCH 028/120] nit --- vms/saevm/cchain/tx/codec.go | 28 ++++++++++++++++++++++++++++ vms/saevm/cchain/tx/tx.go | 28 ---------------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/vms/saevm/cchain/tx/codec.go b/vms/saevm/cchain/tx/codec.go index 3e527228b5a8..ff1debd64b50 100644 --- a/vms/saevm/cchain/tx/codec.go +++ b/vms/saevm/cchain/tx/codec.go @@ -4,6 +4,8 @@ package tx import ( + "errors" + "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/codec/linearcodec" "github.com/ava-labs/avalanchego/utils/wrappers" @@ -45,3 +47,29 @@ func init() { panic(errs.Err) } } + +// MarshalSlice returns the canonical binary format of a slice of transactions. +func MarshalSlice(txs []*Tx) ([]byte, error) { + if len(txs) == 0 { + return nil, nil + } + return c.Marshal(codecVersion, txs) +} + +var errInefficientSlicePacking = errors.New("inefficient slice packing: empty slices should be packed as nil") + +// ParseSlice deserializes a slice of [Tx] from its canonical binary format. +func ParseSlice(b []byte) ([]*Tx, error) { + if len(b) == 0 { + return nil, nil + } + + var txs []*Tx + if _, err := c.Unmarshal(b, &txs); err != nil { + return nil, err + } + if len(txs) == 0 { + return nil, errInefficientSlicePacking + } + return txs, nil +} diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 8cfa462b89d5..540438eb010d 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -7,8 +7,6 @@ package tx import ( - "errors" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -72,29 +70,3 @@ func Parse(b []byte) (*Tx, error) { } return &tx, nil } - -// MarshalSlice returns the canonical binary format of a slice of transactions. -func MarshalSlice(txs []*Tx) ([]byte, error) { - if len(txs) == 0 { - return nil, nil - } - return c.Marshal(codecVersion, txs) -} - -var errInefficientSlicePacking = errors.New("inefficient slice packing: empty slices should be packed as nil") - -// ParseSlice deserializes a slice of [Tx] from its canonical binary format. -func ParseSlice(b []byte) ([]*Tx, error) { - if len(b) == 0 { - return nil, nil - } - - var txs []*Tx - if _, err := c.Unmarshal(b, &txs); err != nil { - return nil, err - } - if len(txs) == 0 { - return nil, errInefficientSlicePacking - } - return txs, nil -} From af11c7e162e2971c12d10c650d261c58aba16cad Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 17:48:07 -0400 Subject: [PATCH 029/120] sae: Implement AtomicRequests on custom txs --- vms/saevm/cchain/tx/tx_test.go | 84 +++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index f70eb572113a..0c5eeb7bb723 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -28,6 +28,7 @@ import ( "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + chainsatomic "github.com/ava-labs/avalanchego/chains/atomic" safemath "github.com/ava-labs/avalanchego/utils/math" ) @@ -36,12 +37,14 @@ import ( var ( avaxAssetID = ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z") tests = [...]struct { - name string - old *atomic.Tx - new *Tx - json string - bytes []byte - op hook.Op + name string + old *atomic.Tx + new *Tx + json string + bytes []byte + op hook.Op + atomicRequestsChainID ids.ID + atomicRequests *chainsatomic.Requests }{ { name: "import", // Included in https://subnets.avax.network/c-chain/block/4 @@ -148,6 +151,12 @@ var ( common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"): *uint256.NewInt(50_000_000 * _x2cRate), }, }, + atomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + atomicRequests: &chainsatomic.Requests{ + RemoveRequests: [][]byte{ + common.FromHex("0xfd9e10917c4a2dab395683cfb766cdc584eba118bc22d3d0fc356fb79345cf64"), + }, + }, }, { name: "export", // Included in https://subnets.avax.network/c-chain/block/48 @@ -257,6 +266,16 @@ var ( }, }, }, + atomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + atomicRequests: &chainsatomic.Requests{ + PutRequests: []*chainsatomic.Element{{ + Key: common.FromHex("0x38ebe8fc127b2eaeeb25c72a747e0ef27460fb04b5929568ed959d67ec3e4948"), + Value: common.FromHex("0x000067b5812292324365c6e2a479b2601cd1cd1facc2fcc8c29d58b5ed96583ea17e0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500"), + Traits: [][]byte{ + ids.ShortFromStringOrPanic("LanVZgBDVvtarbTXD1uU7r1nXVJyLmPUz").Bytes(), + }, + }}, + }, }, { name: "import_multi_input", // Included in https://subnets.avax.network/c-chain/block/132481 @@ -466,6 +485,14 @@ var ( common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"): *uint256.NewInt(597_000_000 * _x2cRate), }, }, + atomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + atomicRequests: &chainsatomic.Requests{ + RemoveRequests: [][]byte{ + common.FromHex("0x821514ed5d925142159bc2c78bc56b043200e53aab79e97ca75e7ca7f6a96d05"), + common.FromHex("0xea05e5c7135613b689d9f6b9903f431067ed72a2957ca82a652de1e8fef2c630"), + common.FromHex("0xd71fb48751f6d5732e7ff63168ed311b40bf517b36279e326878fc3f5169a656"), + }, + }, }, { name: "export_same_address_multi_asset", // Synthetic @@ -529,6 +556,9 @@ var ( }, }, }, + atomicRequests: &chainsatomic.Requests{ + PutRequests: []*chainsatomic.Element{}, + }, }, { name: "export_multi_address_multi_asset", // Synthetic @@ -599,6 +629,9 @@ var ( }, }, }, + atomicRequests: &chainsatomic.Requests{ + PutRequests: []*chainsatomic.Element{}, + }, }, { name: "import_non_avax", // Synthetic @@ -660,6 +693,11 @@ var ( Gas: 10226, Mint: map[common.Address]uint256.Int{}, }, + atomicRequests: &chainsatomic.Requests{ + RemoveRequests: [][]byte{ + common.FromHex("0x2c34ce1df23b838c5abf2a7f6437cca3d3067ed509ff25f11df6b11b582b51eb"), + }, + }, }, } oldTxs []*atomic.Tx @@ -1110,3 +1148,37 @@ func (f *fuzzStateDB) SetNonce(addr common.Address, nonce uint64) { func (f *fuzzStateDB) GetNonce(addr common.Address) uint64 { return f.initialNonces[addr] } + +func TestAtomicRequests(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + chainID, requests, err := test.new.AtomicRequests() + require.NoErrorf(t, err, "%T.AtomicRequests()", test.new) + assert.Equalf(t, test.atomicRequestsChainID, chainID, "%T.AtomicRequests().ChainID", test.new) + assert.Equalf(t, test.atomicRequests, requests, "%T.AtomicRequests().Requests", test.new) + }) + } +} + +func FuzzAtomicRequestsCompatibility(f *testing.F) { + for _, test := range tests { + f.Add(test.bytes) + } + f.Fuzz(func(t *testing.T, data []byte) { + oldTx, err := parseOldTx(data) + if err != nil { + t.Skip("invalid tx bytes") + } + + oldChainID, oldRequests, err := oldTx.UnsignedAtomicTx.AtomicOps() + require.NoErrorf(t, err, "%T.AtomicOps()", oldTx.UnsignedAtomicTx) + + newTx, err := Parse(data) + require.NoError(t, err, "Parse()") + + newChainID, newRequests, err := newTx.AtomicRequests() + require.NoErrorf(t, err, "%T.AtomicRequests()", newTx) + assert.Equal(t, oldChainID, newChainID, "chainID") + assert.Equal(t, oldRequests, newRequests, "requests") + }) +} From 57b2f73e7adc97820b814d21a8ee8dc040045552 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 18:35:03 -0400 Subject: [PATCH 030/120] wip --- vms/saevm/cchain/tx/export.go | 20 ++++++++++++++++++++ vms/saevm/cchain/tx/import.go | 15 +++++++++++++++ vms/saevm/cchain/tx/tx.go | 5 +++++ 3 files changed, 40 insertions(+) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 84740831a86d..65d7302438ef 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -6,10 +6,12 @@ package tx import ( "errors" "fmt" + "math/big" "github.com/ava-labs/libevm/common" "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/graft/coreth/core/extstate" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -125,3 +127,21 @@ func (e *Export) atomicRequests(txID ids.ID) (ids.ID, *atomic.Requests, error) { } return e.DestinationChain, &atomic.Requests{PutRequests: elems}, nil } + +var errInsufficientFunds = errors.New("insufficient funds") + +func (e *Export) TransferNonAVAX(avaxAssetID ids.ID, statedb *extstate.StateDB) error { + for _, in := range e.Ins { + if in.AssetID == avaxAssetID { + continue + } + + coinID := common.Hash(in.AssetID) + amount := new(big.Int).SetUint64(in.Amount) + if statedb.GetBalanceMultiCoin(in.Address, coinID).Cmp(amount) < 0 { + return errInsufficientFunds + } + statedb.SubBalanceMultiCoin(in.Address, coinID, amount) + } + return nil +} diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 56d553104192..21284cd95d82 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -6,11 +6,13 @@ package tx import ( "errors" "fmt" + "math/big" "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/graft/coreth/core/extstate" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -107,3 +109,16 @@ func (i *Import) atomicRequests(ids.ID) (ids.ID, *atomic.Requests, error) { } return i.SourceChain, &atomic.Requests{RemoveRequests: utxoIDs}, nil } + +func (i *Import) TransferNonAVAX(avaxAssetID ids.ID, statedb *extstate.StateDB) error { + for _, out := range i.Outs { + if out.AssetID == avaxAssetID { + continue + } + + coinID := common.Hash(out.AssetID) + amount := new(big.Int).SetUint64(out.Amount) + statedb.AddBalanceMultiCoin(out.Address, coinID, amount) + } + return nil +} diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 3aa754e44349..f481c618d4de 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" + "github.com/ava-labs/avalanchego/graft/coreth/core/extstate" _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" // Imported for [GasPerByte] comment resolution. @@ -38,6 +39,10 @@ type Tx struct { // TODO(StephenButtolph): Expand this interface to include UTXO handling, // verification, and state execution. type Unsigned interface { + // TransferNonAVAX transfers the non-AVAX balances requested by this + // transaction. + TransferNonAVAX(avaxAssetID ids.ID, statedb *extstate.StateDB) error + // burned returns the amount of assetID that is consumed but not produced by // this transaction. burned(assetID ids.ID) (uint64, error) From c58359996cec62b2b55eae2195e2f0f08bcd19d8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 18:44:02 -0400 Subject: [PATCH 031/120] lint + bazel --- vms/saevm/cchain/tx/BUILD.bazel | 2 ++ vms/saevm/cchain/tx/export.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index d7bd0b7fcea4..16fce973ebf9 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -12,6 +12,7 @@ go_library( importpath = "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx", visibility = ["//visibility:public"], deps = [ + "//chains/atomic", "//codec", "//codec/linearcodec", "//graft/coreth/plugin/evm/atomic", @@ -35,6 +36,7 @@ go_test( data = glob(["testdata/**"]), embed = [":tx"], deps = [ + "//chains/atomic", "//graft/coreth/plugin/evm/atomic", "//graft/coreth/plugin/evm/atomic/vm", "//ids", diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 84740831a86d..aefe316209bf 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -102,7 +102,7 @@ func (e *Export) atomicRequests(txID ids.ID) (ids.ID, *atomic.Requests, error) { utxo := &avax.UTXO{ UTXOID: avax.UTXOID{ TxID: txID, - OutputIndex: uint32(i), + OutputIndex: uint32(i), //#nosec G115 -- Won't overflow }, Asset: out.Asset, Out: out.Out, From 2ffbf744ab381c62e4c9c80dcc6aefb98a5b4295 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 18:55:59 -0400 Subject: [PATCH 032/120] fix flake --- vms/saevm/cchain/tx/tx_test.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 0c5eeb7bb723..fcdf3ec8c26c 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -702,6 +702,13 @@ var ( } oldTxs []*atomic.Tx newTxs []*Tx + + // txIgnore ignores caching/context fields populated lazily on the txs. + txIgnore = cmpopts.IgnoreUnexported( + atomic.Metadata{}, + avax.UTXOID{}, + secp256k1fx.OutputOwners{}, + ) ) func init() { @@ -780,12 +787,16 @@ func TestParse(t *testing.T) { got := new(atomic.Tx) _, err := atomic.Codec.Unmarshal(test.bytes, got) require.NoErrorf(t, err, "%T.Unmarshal(, %T)", atomic.Codec, got) - assert.Equalf(t, test.old, got, "%T.Unmarshal(, %T)", atomic.Codec, got) + if diff := cmp.Diff(test.old, got, txIgnore); diff != "" { + t.Errorf("%T.Unmarshal(, %T) diff (-want +got):\n%s", atomic.Codec, got, diff) + } }) t.Run("new", func(t *testing.T) { got, err := Parse(test.bytes) require.NoError(t, err, "Parse()") - assert.Equal(t, test.new, got, "Parse()") + if diff := cmp.Diff(test.new, got, txIgnore); diff != "" { + t.Errorf("Parse() diff (-want +got):\n%s", diff) + } }) }) } @@ -824,7 +835,9 @@ func TestParseSlice(t *testing.T) { t.Run(test.name, func(t *testing.T) { got, err := ParseSlice(test.bytes) require.ErrorIs(t, err, test.wantErr, "ParseSlice()") - assert.Equal(t, test.want, got, "ParseSlice()") + if diff := cmp.Diff(test.want, got, txIgnore); diff != "" { + t.Errorf("ParseSlice() diff (-want +got):\n%s", diff) + } }) } } From 093fbdc9d039451809c06177dd1deec17b5013d4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 18:57:46 -0400 Subject: [PATCH 033/120] nit --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 3aa754e44349..f4c127f88759 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -12,9 +12,9 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" + // Imported for [gasPerByte] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" - // Imported for [GasPerByte] comment resolution. "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap5" "github.com/ava-labs/avalanchego/ids" From 7e58c8ea35128d05a9a34dfc53f4e43b795087a7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 18:58:30 -0400 Subject: [PATCH 034/120] nit --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index e378870e8319..93b630da97bf 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -12,7 +12,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" - // Imported for [GasPerByte] comment resolution. + // Imported for [gasPerByte] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap5" From 85735655238c47f4d7bd8b4318b4d3a310ccc69f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 19:05:58 -0400 Subject: [PATCH 035/120] nit --- vms/saevm/cchain/tx/export.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 880b9f8b18ec..83e9dbeb47a5 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -67,7 +67,7 @@ func (e *Export) numSigs() (uint64, error) { return uint64(len(e.Ins)), nil } -var errMultipleNonces = errors.New("multiple inputs for address with different nonces") +var errMultipleNonces = errors.New("multiple nonces for address") func (e *Export) asOp(avaxAssetID ids.ID) (op, error) { burn := make(map[common.Address]hook.AccountDebit, len(e.Ins)) From 6741731df9628ccfc2eb539270ac1369a5dbb50f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 20:08:17 -0400 Subject: [PATCH 036/120] wip --- vms/saevm/cchain/tx/tx_test.go | 351 +++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index fcdf3ec8c26c..ac9849aba3fc 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -7,9 +7,13 @@ import ( "encoding/json" "errors" "math/big" + "os" "testing" "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/core/types" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/holiman/uint256" @@ -19,7 +23,9 @@ import ( // Imported for [parseOldTx] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/vm" + "github.com/ava-labs/avalanchego/graft/coreth/core/extstate" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -32,6 +38,11 @@ import ( safemath "github.com/ava-labs/avalanchego/utils/math" ) +func TestMain(m *testing.M) { + customtypes.Register() + os.Exit(m.Run()) +} + // tests is defined at the package level to allow sharing between fuzz tests and // unit tests. var ( @@ -1195,3 +1206,343 @@ func FuzzAtomicRequestsCompatibility(f *testing.F) { assert.Equal(t, oldRequests, newRequests, "requests") }) } + +func newStateDB(t testing.TB) *extstate.StateDB { + t.Helper() + + db := state.NewDatabase(rawdb.NewMemoryDatabase()) + sdb, err := state.New(types.EmptyRootHash, db, nil) + require.NoError(t, err) + return extstate.New(sdb) +} + +func TestTransferNonAVAX(t *testing.T) { + var ( + alice = common.Address{1} + bob = common.Address{2} + btc = ids.ID{3} + eth = ids.ID{4} + ) + tests := []struct { + name string + init map[common.Address]map[ids.ID]uint64 + tx Unsigned + want map[common.Address]map[ids.ID]uint64 + wantErr error + }{ + { + name: "import_avax", + tx: &Import{ + Outs: []Output{{ + Address: alice, + Amount: 1, + AssetID: avaxAssetID, + }}, + }, + }, + { + name: "import_non_avax", + tx: &Import{ + Outs: []Output{ + { + Address: alice, + Amount: 1, + AssetID: btc, + }, + }, + }, + want: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 1, + }, + }, + }, + { + name: "import_many", + tx: &Import{ + Outs: []Output{ + { + Address: alice, + Amount: 1, + AssetID: avaxAssetID, + }, + { + Address: alice, + Amount: 10, + AssetID: avaxAssetID, + }, + { + Address: bob, + Amount: 100, + AssetID: avaxAssetID, + }, + { + Address: alice, + Amount: 1_000, + AssetID: btc, + }, + { + Address: alice, + Amount: 10_000, + AssetID: btc, + }, + { + Address: bob, + Amount: 100_000, + AssetID: btc, + }, + { + Address: bob, + Amount: 1_000_000, + AssetID: eth, + }, + }, + }, + want: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 11_000, + }, + bob: { + btc: 100_000, + eth: 1_000_000, + }, + }, + }, + { + name: "export_avax", + tx: &Export{ + Ins: []Input{ + { + Address: alice, + Amount: 1, + AssetID: avaxAssetID, + }, + }, + }, + }, + { + name: "export_non_avax", + init: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 2, + }, + }, + tx: &Export{ + Ins: []Input{ + { + Address: alice, + Amount: 1, + AssetID: btc, + }, + }, + }, + want: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 1, + }, + }, + }, + + { + name: "export_many", + init: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 22_000, + }, + bob: { + btc: 200_000, + eth: 2_000_000, + }, + }, + tx: &Export{ + Ins: []Input{ + { + Address: alice, + Amount: 1, + AssetID: avaxAssetID, + }, + { + Address: alice, + Amount: 10, + AssetID: avaxAssetID, + }, + { + Address: bob, + Amount: 100, + AssetID: avaxAssetID, + }, + { + Address: alice, + Amount: 1_000, + AssetID: btc, + }, + { + Address: alice, + Amount: 10_000, + AssetID: btc, + }, + { + Address: bob, + Amount: 100_000, + AssetID: btc, + }, + { + Address: bob, + Amount: 1_000_000, + AssetID: eth, + }, + }, + }, + want: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 11_000, + }, + bob: { + btc: 100_000, + eth: 1_000_000, + }, + }, + }, + { + name: "export_non_avax_insufficient", + tx: &Export{ + Ins: []Input{ + { + Address: alice, + Amount: 1, + AssetID: btc, + }, + }, + }, + wantErr: errInsufficientFunds, + }, + { + name: "export_non_avax_total_insufficient", + init: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 1, + }, + }, + tx: &Export{ + Ins: []Input{ + { + Address: alice, + Amount: 1, + AssetID: btc, + }, + { + Address: alice, + Amount: 1, + AssetID: btc, + }, + }, + }, + wantErr: errInsufficientFunds, + }, + } + big := func(v uint64) *big.Int { + return new(big.Int).SetUint64(v) + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sdb := newStateDB(t) + for addr, balances := range test.init { + for assetID, amount := range balances { + coinID := common.Hash(assetID) + sdb.AddBalanceMultiCoin(addr, coinID, big(amount)) + } + } + + err := test.tx.TransferNonAVAX(avaxAssetID, sdb) + require.ErrorIs(t, err, test.wantErr) + for addr, balances := range test.want { + for assetID, want := range balances { + coinID := common.Hash(assetID) + got := sdb.GetBalanceMultiCoin(addr, coinID) + require.Zerof(t, got.Cmp(big(want)), "addr=%s asset=%s got=%s want=%d", addr, assetID, got, want) + } + } + }) + } +} + +func FuzzTransferNonAVAXCompatibility(f *testing.F) { + for _, test := range tests { + f.Add(test.bytes) + } + f.Fuzz(func(t *testing.T, data []byte) { + oldTx, err := parseOldTx(data) + if err != nil { + t.Skip("invalid tx bytes") + } + newTx, err := Parse(data) + require.NoError(t, err, "Parse()") + + addrs, coinIDs := referencedAccounts(oldTx) + + oldSDB := newStateDB(t) + newSDB := newStateDB(t) + + // Pre-fund identically with a huge multi-coin balance for every + // (addr, coinID) so neither implementation can fail on the + // multi-coin branch and we exercise only the success path. + hugeBig := new(big.Int).Lsh(big.NewInt(1), 128) + prefund := func(sdb *extstate.StateDB) { + for addr := range addrs { + for coinID := range coinIDs { + sdb.AddBalanceMultiCoin(addr, common.Hash(coinID), hugeBig) + } + } + } + prefund(oldSDB) + prefund(newSDB) + + // EVMStateTransfer also touches AVAX balances and nonces; pre-fund + // AVAX and seed nonces only on the old state so those branches don't + // fail. TransferNonAVAX skips both, so newSDB doesn't need them. + hugeAVAX := new(uint256.Int).Lsh(uint256.NewInt(1), 128) + for addr := range addrs { + oldSDB.AddBalance(addr, hugeAVAX) + } + if export, ok := oldTx.UnsignedAtomicTx.(*atomic.UnsignedExportTx); ok { + for _, in := range export.Ins { + oldSDB.SetNonce(in.Address, in.Nonce) + } + } + + ctx := &snow.Context{AVAXAssetID: avaxAssetID} + if err := oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldSDB); err != nil { + // Repeated address with conflicting nonces produces ErrInvalidNonce + // in old, which doesn't apply to the multi-coin equivalence we are + // checking. Skip. + t.Skipf("EVMStateTransfer: %s", err) + } + require.NoError(t, newTx.TransferNonAVAX(avaxAssetID, newSDB)) + + for addr := range addrs { + for coinID := range coinIDs { + oldBal := oldSDB.GetBalanceMultiCoin(addr, common.Hash(coinID)) + newBal := newSDB.GetBalanceMultiCoin(addr, common.Hash(coinID)) + require.Zerof(t, oldBal.Cmp(newBal), "addr=%s coin=%s old=%s new=%s", addr, coinID, oldBal, newBal) + } + } + }) +} + +func referencedAccounts(tx *atomic.Tx) (map[common.Address]struct{}, map[ids.ID]struct{}) { + addrs := map[common.Address]struct{}{} + coins := map[ids.ID]struct{}{} + switch utx := tx.UnsignedAtomicTx.(type) { + case *atomic.UnsignedImportTx: + for _, out := range utx.Outs { + addrs[out.Address] = struct{}{} + coins[out.AssetID] = struct{}{} + } + case *atomic.UnsignedExportTx: + for _, in := range utx.Ins { + addrs[in.Address] = struct{}{} + coins[in.AssetID] = struct{}{} + } + } + return addrs, coins +} From e70eabb2f5d9ebf95221a67c3d0554c24fca40cb Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 21:20:11 -0400 Subject: [PATCH 037/120] simplify --- vms/saevm/cchain/tx/tx_test.go | 77 ++++++++++------------------------ 1 file changed, 21 insertions(+), 56 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index ac9849aba3fc..eb92c70ceede 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -31,6 +31,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/saevm/cmputils" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -1471,78 +1472,42 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { f.Add(test.bytes) } f.Fuzz(func(t *testing.T, data []byte) { - oldTx, err := parseOldTx(data) + newTx, err := Parse(data) if err != nil { t.Skip("invalid tx bytes") } - newTx, err := Parse(data) - require.NoError(t, err, "Parse()") + if _, err := newTx.AsOp(avaxAssetID); err != nil { + t.Skip("invalid tx") + } - addrs, coinIDs := referencedAccounts(oldTx) + oldTx, err := parseOldTx(data) + require.NoError(t, err, "parseOldTx()") oldSDB := newStateDB(t) newSDB := newStateDB(t) - // Pre-fund identically with a huge multi-coin balance for every - // (addr, coinID) so neither implementation can fail on the - // multi-coin branch and we exercise only the success path. + hugeAVAX := new(uint256.Int).Lsh(uint256.NewInt(1), 128) hugeBig := new(big.Int).Lsh(big.NewInt(1), 128) - prefund := func(sdb *extstate.StateDB) { - for addr := range addrs { - for coinID := range coinIDs { - sdb.AddBalanceMultiCoin(addr, common.Hash(coinID), hugeBig) + for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { + if tx, ok := newTx.Unsigned.(*Export); ok { + for _, in := range tx.Ins { + sdb.AddBalance(in.Address, hugeAVAX) + sdb.SetNonce(in.Address, in.Nonce) + sdb.AddBalanceMultiCoin(in.Address, common.Hash(in.AssetID), hugeBig) } } } - prefund(oldSDB) - prefund(newSDB) - - // EVMStateTransfer also touches AVAX balances and nonces; pre-fund - // AVAX and seed nonces only on the old state so those branches don't - // fail. TransferNonAVAX skips both, so newSDB doesn't need them. - hugeAVAX := new(uint256.Int).Lsh(uint256.NewInt(1), 128) - for addr := range addrs { - oldSDB.AddBalance(addr, hugeAVAX) - } - if export, ok := oldTx.UnsignedAtomicTx.(*atomic.UnsignedExportTx); ok { - for _, in := range export.Ins { - oldSDB.SetNonce(in.Address, in.Nonce) - } - } ctx := &snow.Context{AVAXAssetID: avaxAssetID} - if err := oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldSDB); err != nil { - // Repeated address with conflicting nonces produces ErrInvalidNonce - // in old, which doesn't apply to the multi-coin equivalence we are - // checking. Skip. - t.Skipf("EVMStateTransfer: %s", err) - } + require.NoError(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldSDB)) require.NoError(t, newTx.TransferNonAVAX(avaxAssetID, newSDB)) - for addr := range addrs { - for coinID := range coinIDs { - oldBal := oldSDB.GetBalanceMultiCoin(addr, common.Hash(coinID)) - newBal := newSDB.GetBalanceMultiCoin(addr, common.Hash(coinID)) - require.Zerof(t, oldBal.Cmp(newBal), "addr=%s coin=%s old=%s new=%s", addr, coinID, oldBal, newBal) - } - } - }) -} - -func referencedAccounts(tx *atomic.Tx) (map[common.Address]struct{}, map[ids.ID]struct{}) { - addrs := map[common.Address]struct{}{} - coins := map[ids.ID]struct{}{} - switch utx := tx.UnsignedAtomicTx.(type) { - case *atomic.UnsignedImportTx: - for _, out := range utx.Outs { - addrs[out.Address] = struct{}{} - coins[out.AssetID] = struct{}{} + opts := []cmp.Option{ + cmpopts.IgnoreUnexported(extstate.StateDB{}), + cmputils.StateDBs(), } - case *atomic.UnsignedExportTx: - for _, in := range utx.Ins { - addrs[in.Address] = struct{}{} - coins[in.AssetID] = struct{}{} + if diff := cmp.Diff(oldSDB, newSDB, opts...); diff != "" { + t.Errorf("%T.AsOp() diff (-want +got):\n%s", newTx, diff) } - } - return addrs, coins + }) } From e74bda269d79b9426f797a65554c6e1ca20978a5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 28 Apr 2026 23:28:09 -0400 Subject: [PATCH 038/120] wip --- vms/saevm/cchain/tx/fuzz_test.go | 206 +++++++++++++++++++++++++++++++ vms/saevm/cchain/tx/tx_test.go | 68 ++++------ 2 files changed, 228 insertions(+), 46 deletions(-) create mode 100644 vms/saevm/cchain/tx/fuzz_test.go diff --git a/vms/saevm/cchain/tx/fuzz_test.go b/vms/saevm/cchain/tx/fuzz_test.go new file mode 100644 index 000000000000..1e21355edbd9 --- /dev/null +++ b/vms/saevm/cchain/tx/fuzz_test.go @@ -0,0 +1,206 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import ( + "encoding/binary" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +// fuzzTx works like [testing.F.Fuzz], but produces a [Tx] rather than a byte +// slice for fuzzing. +// +// This function should be used over [testing.F.Fuzz] when it is desired to +// consistently produce a parseable transaction. +func fuzzTx(f *testing.F, test func(t *testing.T, tx *Tx)) { + for _, test := range tests { + f.Add(test.bytes) + } + f.Fuzz(func(t *testing.T, data []byte) { + f := fuzzer(data) + genTx := f.tx() + + // genTx isn't always ideally formatted, so we round-trip through + // parsing before providing it to the test body. + bytes, err := genTx.Bytes() + if err != nil { + t.Skipf("invalid tx: %s", err) + } + tx, err := Parse(bytes) + require.NoError(t, err, "Parse()") + test(t, tx) + }) +} + +// fuzzer turns a byte stream into structured data. +type fuzzer []byte + +// Once the fuzzer is exhausted, bytes yields zero bytes — this keeps the +// generator deterministic on short inputs without rejecting them. +func (f *fuzzer) bytes(n int) []byte { + b := *f + defer func() { + *f = b + }() + + out := make([]byte, n) + copy(out, b) + if n >= len(b) { + b = nil + } else { + b = b[n:] + } + return out +} + +func (f *fuzzer) bool() bool { return f.bytes(1)[0]&1 != 0 } +func (f *fuzzer) u32() uint32 { return binary.BigEndian.Uint32(f.bytes(4)) } +func (f *fuzzer) u64() uint64 { return binary.BigEndian.Uint64(f.bytes(8)) } +func (f *fuzzer) id() ids.ID { return ids.ID(f.bytes(ids.IDLen)) } +func (f *fuzzer) shortID() ids.ShortID { return ids.ShortID(f.bytes(ids.ShortIDLen)) } +func (f *fuzzer) signature() [65]byte { return [65]byte(f.bytes(65)) } + +// intn returns a value in [0, x). +func (f *fuzzer) intn(x int) int { + if x < 1 { + return 0 + } + return int(f.u64() % uint64(x)) +} + +// element returns a random element in s. It panics if s is empty. +func element[T any](f *fuzzer, s []T) T { + return s[f.intn(len(s))] +} + +// sliceOf generates a random slice of randomly generate entries. +func sliceOf[T any](f *fuzzer, gen func(*fuzzer) T) []T { + var out []T + // bool defaults to false once the generation has been exausted, so this + // will eventually terminate. + for f.bool() { + out = append(out, gen(f)) + } + return out +} + +func (f *fuzzer) address() common.Address { + if !f.bool() { + return common.Address(f.bytes(common.AddressLength)) + } + return element(f, []common.Address{ + {}, + {0x01}, + {0x02}, + common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"), + common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), + }) +} + +func (f *fuzzer) assetID() ids.ID { + if !f.bool() { + return f.id() + } + return element(f, []ids.ID{ + {}, + {0x01}, + {0x02}, + avaxAssetID, + }) +} + +func (f *fuzzer) transferableInput() *avax.TransferableInput { + return &avax.TransferableInput{ + UTXOID: avax.UTXOID{ + TxID: f.id(), + OutputIndex: f.u32(), + }, + Asset: avax.Asset{ID: f.assetID()}, + In: &secp256k1fx.TransferInput{ + Amt: f.u64(), + Input: secp256k1fx.Input{ + SigIndices: sliceOf(f, (*fuzzer).u32), + }, + }, + } +} + +func (f *fuzzer) transferableOutput() *avax.TransferableOutput { + return &avax.TransferableOutput{ + Asset: avax.Asset{ID: f.assetID()}, + Out: &secp256k1fx.TransferOutput{ + Amt: f.u64(), + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: f.u64(), + Threshold: f.u32(), + Addrs: sliceOf(f, (*fuzzer).shortID), + }, + }, + } +} + +func (f *fuzzer) input() Input { + return Input{ + Address: f.address(), + Amount: f.u64(), + AssetID: f.assetID(), + Nonce: f.u64(), + } +} + +func (f *fuzzer) output() Output { + return Output{ + Address: f.address(), + Amount: f.u64(), + AssetID: f.assetID(), + } +} + +func (f *fuzzer) importTx() *Import { + return &Import{ + NetworkID: f.u32(), + BlockchainID: f.id(), + SourceChain: f.id(), + ImportedInputs: sliceOf(f, (*fuzzer).transferableInput), + Outs: sliceOf(f, (*fuzzer).output), + } +} + +func (f *fuzzer) exportTx() *Export { + return &Export{ + NetworkID: f.u32(), + BlockchainID: f.id(), + DestinationChain: f.id(), + Ins: sliceOf(f, (*fuzzer).input), + ExportedOutputs: sliceOf(f, (*fuzzer).transferableOutput), + } +} + +func (f *fuzzer) unsigned() Unsigned { + if f.bool() { + return f.importTx() + } else { + return f.exportTx() + } +} + +func (f *fuzzer) credential() Credential { + return &secp256k1fx.Credential{ + Sigs: sliceOf(f, (*fuzzer).signature), + } +} + +func (f *fuzzer) tx() *Tx { + return &Tx{ + Unsigned: f.unsigned(), + Creds: sliceOf(f, (*fuzzer).credential), + } +} diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index eb92c70ceede..d0f0e1838031 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -978,17 +978,8 @@ func TestJSONMarshal(t *testing.T) { } func FuzzJSONCompatibility(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) - } - f.Fuzz(func(t *testing.T, data []byte) { - oldTx, err := parseOldTx(data) - if err != nil { - t.Skip("invalid tx") - } - - newTx, err := Parse(data) - require.NoError(t, err, "Parse()") + fuzzTx(f, func(t *testing.T, newTx *Tx) { + oldTx := toOldTx(t, newTx) oldJSON, err := json.Marshal(oldTx) require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) @@ -999,6 +990,17 @@ func FuzzJSONCompatibility(f *testing.F) { }) } +func toOldTx(tb testing.TB, newTx *Tx) *atomic.Tx { + tb.Helper() + + bytes, err := newTx.Bytes() + require.NoErrorf(tb, err, "%T.Bytes()", newTx) + + oldTx, err := parseOldTx(bytes) + require.NoError(tb, err, "parseOldTx()") + return oldTx +} + func TestAsOp(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -1074,23 +1076,13 @@ func TestAsOp_Errors(t *testing.T) { } func FuzzAsOpCompatibility(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) - } - f.Fuzz(func(t *testing.T, data []byte) { - newTx, err := Parse(data) - if err != nil { - t.Skip("invalid tx bytes") - } - + fuzzTx(f, func(t *testing.T, newTx *Tx) { op, err := newTx.AsOp(avaxAssetID) if err != nil { t.Skip("invalid tx") } - oldTx, err := parseOldTx(data) - require.NoError(t, err, "parseOldTx()") - + oldTx := toOldTx(t, newTx) gasUsed, err := oldTx.UnsignedAtomicTx.GasUsed(true) require.NoErrorf(t, err, "%T.GasUsed(true)", oldTx.UnsignedAtomicTx) @@ -1186,21 +1178,12 @@ func TestAtomicRequests(t *testing.T) { } func FuzzAtomicRequestsCompatibility(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) - } - f.Fuzz(func(t *testing.T, data []byte) { - oldTx, err := parseOldTx(data) - if err != nil { - t.Skip("invalid tx bytes") - } + fuzzTx(f, func(t *testing.T, newTx *Tx) { + oldTx := toOldTx(t, newTx) oldChainID, oldRequests, err := oldTx.UnsignedAtomicTx.AtomicOps() require.NoErrorf(t, err, "%T.AtomicOps()", oldTx.UnsignedAtomicTx) - newTx, err := Parse(data) - require.NoError(t, err, "Parse()") - newChainID, newRequests, err := newTx.AtomicRequests() require.NoErrorf(t, err, "%T.AtomicRequests()", newTx) assert.Equal(t, oldChainID, newChainID, "chainID") @@ -1468,21 +1451,11 @@ func TestTransferNonAVAX(t *testing.T) { } func FuzzTransferNonAVAXCompatibility(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) - } - f.Fuzz(func(t *testing.T, data []byte) { - newTx, err := Parse(data) - if err != nil { - t.Skip("invalid tx bytes") - } + fuzzTx(f, func(t *testing.T, newTx *Tx) { if _, err := newTx.AsOp(avaxAssetID); err != nil { t.Skip("invalid tx") } - oldTx, err := parseOldTx(data) - require.NoError(t, err, "parseOldTx()") - oldSDB := newStateDB(t) newSDB := newStateDB(t) @@ -1498,7 +1471,10 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { } } - ctx := &snow.Context{AVAXAssetID: avaxAssetID} + var ( + oldTx = toOldTx(t, newTx) + ctx = &snow.Context{AVAXAssetID: avaxAssetID} + ) require.NoError(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldSDB)) require.NoError(t, newTx.TransferNonAVAX(avaxAssetID, newSDB)) From 5a741ec9c0543c8e8ecc2b149904cb97f7d398ea Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 00:37:44 -0400 Subject: [PATCH 039/120] wip --- vms/saevm/cchain/tx/fuzz_test.go | 110 ++++++++++++++++++++++++++++++- vms/saevm/cchain/tx/tx_test.go | 7 ++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/fuzz_test.go b/vms/saevm/cchain/tx/fuzz_test.go index 1e21355edbd9..64af20f9bf1e 100644 --- a/vms/saevm/cchain/tx/fuzz_test.go +++ b/vms/saevm/cchain/tx/fuzz_test.go @@ -22,7 +22,7 @@ import ( // consistently produce a parseable transaction. func fuzzTx(f *testing.F, test func(t *testing.T, tx *Tx)) { for _, test := range tests { - f.Add(test.bytes) + f.Add(encodeTx(test.new)) } f.Fuzz(func(t *testing.T, data []byte) { f := fuzzer(data) @@ -204,3 +204,111 @@ func (f *fuzzer) tx() *Tx { Creds: sliceOf(f, (*fuzzer).credential), } } + +// encoder writes a byte stream that [fuzzer] decodes back into the same +// structure. Used to seed the corpus with bytes that round-trip to known txs. +type encoder []byte + +func (e *encoder) bytes(b []byte) { *e = append(*e, b...) } +func (e *encoder) u32(v uint32) { *e = binary.BigEndian.AppendUint32(*e, v) } +func (e *encoder) u64(v uint64) { *e = binary.BigEndian.AppendUint64(*e, v) } +func (e *encoder) id(v ids.ID) { e.bytes(v[:]) } +func (e *encoder) shortID(v ids.ShortID) { e.bytes(v[:]) } +func (e *encoder) signature(v [65]byte) { e.bytes(v[:]) } + +func (e *encoder) bool(b bool) { + if b { + *e = append(*e, 1) + } else { + *e = append(*e, 0) + } +} + +// address and assetID always pick the raw-bytes branch in [fuzzer] so the +// encoded value is independent of the alphabet ordering. +func (e *encoder) address(v common.Address) { + e.bool(false) + e.bytes(v[:]) +} + +func (e *encoder) assetID(v ids.ID) { + e.bool(false) + e.id(v) +} + +func encodeSlice[T any](e *encoder, items []T, gen func(*encoder, T)) { + for _, item := range items { + e.bool(true) + gen(e, item) + } + e.bool(false) +} + +func (e *encoder) transferableInput(in *avax.TransferableInput) { + e.id(in.UTXOID.TxID) + e.u32(in.UTXOID.OutputIndex) + e.assetID(in.Asset.ID) + ti := in.In.(*secp256k1fx.TransferInput) + e.u64(ti.Amt) + encodeSlice(e, ti.Input.SigIndices, (*encoder).u32) +} + +func (e *encoder) transferableOutput(out *avax.TransferableOutput) { + e.assetID(out.Asset.ID) + to := out.Out.(*secp256k1fx.TransferOutput) + e.u64(to.Amt) + e.u64(to.OutputOwners.Locktime) + e.u32(to.OutputOwners.Threshold) + encodeSlice(e, to.OutputOwners.Addrs, (*encoder).shortID) +} + +func (e *encoder) input(i Input) { + e.address(i.Address) + e.u64(i.Amount) + e.assetID(i.AssetID) + e.u64(i.Nonce) +} + +func (e *encoder) output(o Output) { + e.address(o.Address) + e.u64(o.Amount) + e.assetID(o.AssetID) +} + +func (e *encoder) credential(c Credential) { + encodeSlice(e, c.Self().Sigs, (*encoder).signature) +} + +func (e *encoder) importTx(t *Import) { + e.u32(t.NetworkID) + e.id(t.BlockchainID) + e.id(t.SourceChain) + encodeSlice(e, t.ImportedInputs, (*encoder).transferableInput) + encodeSlice(e, t.Outs, (*encoder).output) +} + +func (e *encoder) exportTx(t *Export) { + e.u32(t.NetworkID) + e.id(t.BlockchainID) + e.id(t.DestinationChain) + encodeSlice(e, t.Ins, (*encoder).input) + encodeSlice(e, t.ExportedOutputs, (*encoder).transferableOutput) +} + +func (e *encoder) unsigned(u Unsigned) { + switch u := u.(type) { + case *Import: + e.bool(true) + e.importTx(u) + case *Export: + e.bool(false) + e.exportTx(u) + } +} + +func encodeTx(tx *Tx) []byte { + var e encoder + e.unsigned(tx.Unsigned) + encodeSlice(&e, tx.Creds, (*encoder).credential) + return e +} diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index d0f0e1838031..383ad88d294d 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1478,6 +1478,13 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { require.NoError(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldSDB)) require.NoError(t, newTx.TransferNonAVAX(avaxAssetID, newSDB)) + // Materialize journaled writes into the trie so that + // [cmputils.StateDBs], which dumps the trie, reflects them. + for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { + sdb.Finalise(true) + sdb.IntermediateRoot(true) + } + opts := []cmp.Option{ cmpopts.IgnoreUnexported(extstate.StateDB{}), cmputils.StateDBs(), From 7709421e687572b91c4383cf8c8e54b6ec77065f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 10:12:07 -0400 Subject: [PATCH 040/120] wip --- vms/saevm/cchain/tx/compatibility_test.go | 255 +++++++++++ vms/saevm/cchain/tx/fuzz_test.go | 314 ------------- vms/saevm/cchain/tx/tx_test.go | 522 +++++++--------------- vms/saevm/cchain/txtest/fuzzer.go | 410 +++++++++++++++++ 4 files changed, 823 insertions(+), 678 deletions(-) create mode 100644 vms/saevm/cchain/tx/compatibility_test.go delete mode 100644 vms/saevm/cchain/tx/fuzz_test.go create mode 100644 vms/saevm/cchain/txtest/fuzzer.go diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go new file mode 100644 index 000000000000..039e754198d0 --- /dev/null +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -0,0 +1,255 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx_test + +import ( + "encoding/json" + "math" + "math/big" + "os" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/graft/coreth/core/extstate" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/avalanchego/vms/saevm/cchain/txtest" + "github.com/ava-labs/avalanchego/vms/saevm/cmputils" + "github.com/ava-labs/avalanchego/vms/saevm/hook" + + . "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" +) + +func TestMain(m *testing.M) { + customtypes.Register() + os.Exit(m.Run()) +} + +// newFuzzF wraps f with the alphabets used to bias [txtest.Fuzzer] toward +// repeated addresses and assets across iterations. It also seeds the fuzzer +// with [NewTxs]. +func newFuzzF(f *testing.F) *txtest.F { + fuzzF := &txtest.F{ + F: f, + Addresses: []common.Address{ + {1}, + }, + AssetIDs: []ids.ID{ + AVAXAssetID, + }, + } + for _, tx := range NewTxs { + fuzzF.Add(tx) + } + return fuzzF +} + +func FuzzParseCompatibility(f *testing.F) { + for _, test := range Tests { + f.Add(test.Bytes) + } + f.Fuzz(func(t *testing.T, data []byte) { + _, oldErr := ParseOldTx(data) + oldOk := oldErr == nil + + _, newErr := Parse(data) + newOk := newErr == nil + + assert.Equal(t, oldOk, newOk, "Parse(b) == ParseOldTx(b)") + }) +} + +func FuzzParseSliceCompatibility(f *testing.F) { + { + b, err := MarshalSlice(NewTxs) + require.NoError(f, err, "MarshalSlice()") + f.Add(b) + } + + f.Fuzz(func(t *testing.T, data []byte) { + _, oldErr := ParseOldTxs(data) + oldOk := oldErr == nil + + _, newErr := ParseSlice(data) + newOk := newErr == nil + + assert.Equal(t, oldOk, newOk, "ParseSlice(b) == ParseOldTxs(b)") + }) +} + +func FuzzJSONCompatibility(f *testing.F) { + newFuzzF(f).Fuzz(func(t *testing.T, newTx *Tx) { + oldTx := ToOldTx(t, newTx) + + oldJSON, err := json.Marshal(oldTx) + require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) + + newJSON, err := json.Marshal(newTx) + require.NoErrorf(t, err, "json.Marshal(%T)", newTx) + assert.JSONEq(t, string(oldJSON), string(newJSON)) + }) +} + +func FuzzAsOpCompatibility(f *testing.F) { + newFuzzF(f).Fuzz(func(t *testing.T, newTx *Tx) { + op, err := newTx.AsOp(AVAXAssetID) + if err != nil { + t.Skip("invalid tx") + } + + oldTx := ToOldTx(t, newTx) + gasUsed, err := oldTx.UnsignedAtomicTx.GasUsed(true) + require.NoErrorf(t, err, "%T.GasUsed(true)", oldTx.UnsignedAtomicTx) + + gasPrice, err := atomic.EffectiveGasPrice(oldTx.UnsignedAtomicTx, AVAXAssetID, true) + require.NoErrorf(t, err, "atomic.EffectiveGasPrice(%T, AVAXAssetID, true)", oldTx) + + state := newFuzzStateDB() + if export, ok := oldTx.UnsignedAtomicTx.(*atomic.UnsignedExportTx); ok { + for _, in := range export.Ins { + state.initialNonces[in.Address] = in.Nonce + } + } + + ctx := &snow.Context{AVAXAssetID: AVAXAssetID} + require.NoErrorf(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, state), "%T.EVMStateTransfer()", oldTx.UnsignedAtomicTx) + + expected := hook.Op{ + ID: oldTx.ID(), + Gas: gas.Gas(gasUsed), + GasFeeCap: gasPrice, + Burn: state.op.Burn, + Mint: state.op.Mint, + } + if diff := cmp.Diff(expected, op, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("%T.AsOp() diff (-want +got):\n%s", newTx, diff) + } + }) +} + +// fuzzStateDB is an in-memory [atomic.StateDB] for [FuzzAsOpCompatibility]. It +// constructs a [hook.Op] from [atomic.UnsignedAtomicTx.EVMStateTransfer]. +type fuzzStateDB struct { + initialNonces map[common.Address]uint64 + op hook.Op +} + +func newFuzzStateDB() *fuzzStateDB { + return &fuzzStateDB{ + initialNonces: make(map[common.Address]uint64), + op: hook.Op{ + Burn: make(map[common.Address]hook.AccountDebit), + Mint: make(map[common.Address]uint256.Int), + }, + } +} + +func (f *fuzzStateDB) AddBalance(addr common.Address, amount *uint256.Int) { + b := f.op.Mint[addr] + b.Add(&b, amount) + f.op.Mint[addr] = b +} + +func (f *fuzzStateDB) SubBalance(addr common.Address, amount *uint256.Int) { + d := f.op.Burn[addr] + d.Amount.Add(&d.Amount, amount) + d.MinBalance = d.Amount + f.op.Burn[addr] = d +} + +func (*fuzzStateDB) GetBalance(common.Address) *uint256.Int { + // Large enough to never underflow, but small enough to never overflow. + return new(uint256.Int).Lsh(uint256.NewInt(1), 128) +} + +func (*fuzzStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} + +func (*fuzzStateDB) SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} + +func (*fuzzStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { + // Large enough to never underflow, but small enough to never overflow. + return new(big.Int).Lsh(big.NewInt(1), 128) +} + +func (f *fuzzStateDB) SetNonce(addr common.Address, nonce uint64) { + d := f.op.Burn[addr] + d.Nonce = nonce - 1 + f.op.Burn[addr] = d +} + +func (f *fuzzStateDB) GetNonce(addr common.Address) uint64 { + return f.initialNonces[addr] +} + +func FuzzAtomicRequestsCompatibility(f *testing.F) { + newFuzzF(f).Fuzz(func(t *testing.T, newTx *Tx) { + oldTx := ToOldTx(t, newTx) + + oldChainID, oldRequests, err := oldTx.UnsignedAtomicTx.AtomicOps() + require.NoErrorf(t, err, "%T.AtomicOps()", oldTx.UnsignedAtomicTx) + + newChainID, newRequests, err := newTx.AtomicRequests() + require.NoErrorf(t, err, "%T.AtomicRequests()", newTx) + assert.Equal(t, oldChainID, newChainID, "chainID") + assert.Equal(t, oldRequests, newRequests, "requests") + }) +} + +func FuzzTransferNonAVAXCompatibility(f *testing.F) { + newFuzzF(f).Fuzz(func(t *testing.T, newTx *Tx) { + op, err := newTx.AsOp(AVAXAssetID) + if err != nil { + t.Skip("invalid tx") + } + + oldSDB := NewStateDB(t) + newSDB := NewStateDB(t) + + hugeAVAX := new(uint256.Int).Lsh(uint256.NewInt(1), 128) + hugeBig := new(big.Int).Lsh(big.NewInt(1), 128) + for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { + if tx, ok := newTx.Unsigned.(*Export); ok { + for _, in := range tx.Ins { + if in.Nonce == math.MaxUint64 { + t.Skip("nonce overflow") + } + sdb.AddBalance(in.Address, hugeAVAX) + sdb.SetNonce(in.Address, in.Nonce) + sdb.AddBalanceMultiCoin(in.Address, common.Hash(in.AssetID), hugeBig) + } + } + } + + var ( + oldTx = ToOldTx(t, newTx) + ctx = &snow.Context{AVAXAssetID: AVAXAssetID} + ) + require.NoError(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldSDB)) + require.NoError(t, newTx.TransferNonAVAX(AVAXAssetID, newSDB)) + require.NoError(t, op.ApplyTo(newSDB.StateDB)) + + // Finalize the trie structures for comparison. + for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { + sdb.Finalise(true) + sdb.IntermediateRoot(true) + } + + opts := []cmp.Option{ + cmpopts.IgnoreUnexported(extstate.StateDB{}), + cmputils.StateDBs(), + } + if diff := cmp.Diff(oldSDB, newSDB, opts...); diff != "" { + t.Errorf("%T.AsOp() diff (-want +got):\n%s", newTx, diff) + } + }) +} diff --git a/vms/saevm/cchain/tx/fuzz_test.go b/vms/saevm/cchain/tx/fuzz_test.go deleted file mode 100644 index 64af20f9bf1e..000000000000 --- a/vms/saevm/cchain/tx/fuzz_test.go +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package tx - -import ( - "encoding/binary" - "testing" - - "github.com/ava-labs/libevm/common" - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/vms/components/avax" - "github.com/ava-labs/avalanchego/vms/secp256k1fx" -) - -// fuzzTx works like [testing.F.Fuzz], but produces a [Tx] rather than a byte -// slice for fuzzing. -// -// This function should be used over [testing.F.Fuzz] when it is desired to -// consistently produce a parseable transaction. -func fuzzTx(f *testing.F, test func(t *testing.T, tx *Tx)) { - for _, test := range tests { - f.Add(encodeTx(test.new)) - } - f.Fuzz(func(t *testing.T, data []byte) { - f := fuzzer(data) - genTx := f.tx() - - // genTx isn't always ideally formatted, so we round-trip through - // parsing before providing it to the test body. - bytes, err := genTx.Bytes() - if err != nil { - t.Skipf("invalid tx: %s", err) - } - tx, err := Parse(bytes) - require.NoError(t, err, "Parse()") - test(t, tx) - }) -} - -// fuzzer turns a byte stream into structured data. -type fuzzer []byte - -// Once the fuzzer is exhausted, bytes yields zero bytes — this keeps the -// generator deterministic on short inputs without rejecting them. -func (f *fuzzer) bytes(n int) []byte { - b := *f - defer func() { - *f = b - }() - - out := make([]byte, n) - copy(out, b) - if n >= len(b) { - b = nil - } else { - b = b[n:] - } - return out -} - -func (f *fuzzer) bool() bool { return f.bytes(1)[0]&1 != 0 } -func (f *fuzzer) u32() uint32 { return binary.BigEndian.Uint32(f.bytes(4)) } -func (f *fuzzer) u64() uint64 { return binary.BigEndian.Uint64(f.bytes(8)) } -func (f *fuzzer) id() ids.ID { return ids.ID(f.bytes(ids.IDLen)) } -func (f *fuzzer) shortID() ids.ShortID { return ids.ShortID(f.bytes(ids.ShortIDLen)) } -func (f *fuzzer) signature() [65]byte { return [65]byte(f.bytes(65)) } - -// intn returns a value in [0, x). -func (f *fuzzer) intn(x int) int { - if x < 1 { - return 0 - } - return int(f.u64() % uint64(x)) -} - -// element returns a random element in s. It panics if s is empty. -func element[T any](f *fuzzer, s []T) T { - return s[f.intn(len(s))] -} - -// sliceOf generates a random slice of randomly generate entries. -func sliceOf[T any](f *fuzzer, gen func(*fuzzer) T) []T { - var out []T - // bool defaults to false once the generation has been exausted, so this - // will eventually terminate. - for f.bool() { - out = append(out, gen(f)) - } - return out -} - -func (f *fuzzer) address() common.Address { - if !f.bool() { - return common.Address(f.bytes(common.AddressLength)) - } - return element(f, []common.Address{ - {}, - {0x01}, - {0x02}, - common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"), - common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), - }) -} - -func (f *fuzzer) assetID() ids.ID { - if !f.bool() { - return f.id() - } - return element(f, []ids.ID{ - {}, - {0x01}, - {0x02}, - avaxAssetID, - }) -} - -func (f *fuzzer) transferableInput() *avax.TransferableInput { - return &avax.TransferableInput{ - UTXOID: avax.UTXOID{ - TxID: f.id(), - OutputIndex: f.u32(), - }, - Asset: avax.Asset{ID: f.assetID()}, - In: &secp256k1fx.TransferInput{ - Amt: f.u64(), - Input: secp256k1fx.Input{ - SigIndices: sliceOf(f, (*fuzzer).u32), - }, - }, - } -} - -func (f *fuzzer) transferableOutput() *avax.TransferableOutput { - return &avax.TransferableOutput{ - Asset: avax.Asset{ID: f.assetID()}, - Out: &secp256k1fx.TransferOutput{ - Amt: f.u64(), - OutputOwners: secp256k1fx.OutputOwners{ - Locktime: f.u64(), - Threshold: f.u32(), - Addrs: sliceOf(f, (*fuzzer).shortID), - }, - }, - } -} - -func (f *fuzzer) input() Input { - return Input{ - Address: f.address(), - Amount: f.u64(), - AssetID: f.assetID(), - Nonce: f.u64(), - } -} - -func (f *fuzzer) output() Output { - return Output{ - Address: f.address(), - Amount: f.u64(), - AssetID: f.assetID(), - } -} - -func (f *fuzzer) importTx() *Import { - return &Import{ - NetworkID: f.u32(), - BlockchainID: f.id(), - SourceChain: f.id(), - ImportedInputs: sliceOf(f, (*fuzzer).transferableInput), - Outs: sliceOf(f, (*fuzzer).output), - } -} - -func (f *fuzzer) exportTx() *Export { - return &Export{ - NetworkID: f.u32(), - BlockchainID: f.id(), - DestinationChain: f.id(), - Ins: sliceOf(f, (*fuzzer).input), - ExportedOutputs: sliceOf(f, (*fuzzer).transferableOutput), - } -} - -func (f *fuzzer) unsigned() Unsigned { - if f.bool() { - return f.importTx() - } else { - return f.exportTx() - } -} - -func (f *fuzzer) credential() Credential { - return &secp256k1fx.Credential{ - Sigs: sliceOf(f, (*fuzzer).signature), - } -} - -func (f *fuzzer) tx() *Tx { - return &Tx{ - Unsigned: f.unsigned(), - Creds: sliceOf(f, (*fuzzer).credential), - } -} - -// encoder writes a byte stream that [fuzzer] decodes back into the same -// structure. Used to seed the corpus with bytes that round-trip to known txs. -type encoder []byte - -func (e *encoder) bytes(b []byte) { *e = append(*e, b...) } -func (e *encoder) u32(v uint32) { *e = binary.BigEndian.AppendUint32(*e, v) } -func (e *encoder) u64(v uint64) { *e = binary.BigEndian.AppendUint64(*e, v) } -func (e *encoder) id(v ids.ID) { e.bytes(v[:]) } -func (e *encoder) shortID(v ids.ShortID) { e.bytes(v[:]) } -func (e *encoder) signature(v [65]byte) { e.bytes(v[:]) } - -func (e *encoder) bool(b bool) { - if b { - *e = append(*e, 1) - } else { - *e = append(*e, 0) - } -} - -// address and assetID always pick the raw-bytes branch in [fuzzer] so the -// encoded value is independent of the alphabet ordering. -func (e *encoder) address(v common.Address) { - e.bool(false) - e.bytes(v[:]) -} - -func (e *encoder) assetID(v ids.ID) { - e.bool(false) - e.id(v) -} - -func encodeSlice[T any](e *encoder, items []T, gen func(*encoder, T)) { - for _, item := range items { - e.bool(true) - gen(e, item) - } - e.bool(false) -} - -func (e *encoder) transferableInput(in *avax.TransferableInput) { - e.id(in.UTXOID.TxID) - e.u32(in.UTXOID.OutputIndex) - e.assetID(in.Asset.ID) - ti := in.In.(*secp256k1fx.TransferInput) - e.u64(ti.Amt) - encodeSlice(e, ti.Input.SigIndices, (*encoder).u32) -} - -func (e *encoder) transferableOutput(out *avax.TransferableOutput) { - e.assetID(out.Asset.ID) - to := out.Out.(*secp256k1fx.TransferOutput) - e.u64(to.Amt) - e.u64(to.OutputOwners.Locktime) - e.u32(to.OutputOwners.Threshold) - encodeSlice(e, to.OutputOwners.Addrs, (*encoder).shortID) -} - -func (e *encoder) input(i Input) { - e.address(i.Address) - e.u64(i.Amount) - e.assetID(i.AssetID) - e.u64(i.Nonce) -} - -func (e *encoder) output(o Output) { - e.address(o.Address) - e.u64(o.Amount) - e.assetID(o.AssetID) -} - -func (e *encoder) credential(c Credential) { - encodeSlice(e, c.Self().Sigs, (*encoder).signature) -} - -func (e *encoder) importTx(t *Import) { - e.u32(t.NetworkID) - e.id(t.BlockchainID) - e.id(t.SourceChain) - encodeSlice(e, t.ImportedInputs, (*encoder).transferableInput) - encodeSlice(e, t.Outs, (*encoder).output) -} - -func (e *encoder) exportTx(t *Export) { - e.u32(t.NetworkID) - e.id(t.BlockchainID) - e.id(t.DestinationChain) - encodeSlice(e, t.Ins, (*encoder).input) - encodeSlice(e, t.ExportedOutputs, (*encoder).transferableOutput) -} - -func (e *encoder) unsigned(u Unsigned) { - switch u := u.(type) { - case *Import: - e.bool(true) - e.importTx(u) - case *Export: - e.bool(false) - e.exportTx(u) - } -} - -func encodeTx(tx *Tx) []byte { - var e encoder - e.unsigned(tx.Unsigned) - encodeSlice(&e, tx.Creds, (*encoder).credential) - return e -} diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 383ad88d294d..4cae9ac54678 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "math/big" - "os" "testing" "github.com/ava-labs/libevm/common" @@ -20,18 +19,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - // Imported for [parseOldTx] comment resolution. + // Imported for [ParseOldTx] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/vm" "github.com/ava-labs/avalanchego/graft/coreth/core/extstate" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" - "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/vms/components/avax" - "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" - "github.com/ava-labs/avalanchego/vms/saevm/cmputils" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -39,28 +34,23 @@ import ( safemath "github.com/ava-labs/avalanchego/utils/math" ) -func TestMain(m *testing.M) { - customtypes.Register() - os.Exit(m.Run()) -} - -// tests is defined at the package level to allow sharing between fuzz tests and -// unit tests. +// Tests is defined at the package level to allow sharing between fuzz Tests and +// unit Tests. var ( - avaxAssetID = ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z") - tests = [...]struct { - name string - old *atomic.Tx - new *Tx - json string - bytes []byte - op hook.Op - atomicRequestsChainID ids.ID - atomicRequests *chainsatomic.Requests + AVAXAssetID = ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z") + Tests = [...]struct { + Name string + Old *atomic.Tx + New *Tx + JSON string + Bytes []byte + Op hook.Op + AtomicRequestsChainID ids.ID + AtomicRequests *chainsatomic.Requests }{ { - name: "import", // Included in https://subnets.avax.network/c-chain/block/4 - old: &atomic.Tx{ + Name: "import", // Included in https://subnets.avax.network/c-chain/block/4 + Old: &atomic.Tx{ UnsignedAtomicTx: &atomic.UnsignedImportTx{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -71,7 +61,7 @@ var ( OutputIndex: 1, }, Asset: avax.Asset{ - ID: avaxAssetID, + ID: AVAXAssetID, }, In: &secp256k1fx.TransferInput{ Amt: 50000000, @@ -83,7 +73,7 @@ var ( Outs: []atomic.EVMOutput{{ Address: common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"), Amount: 50000000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }}, }, Creds: []verify.Verifiable{ @@ -94,7 +84,7 @@ var ( }, }, }, - new: &Tx{ + New: &Tx{ Unsigned: &Import{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -105,7 +95,7 @@ var ( OutputIndex: 1, }, Asset: avax.Asset{ - ID: avaxAssetID, + ID: AVAXAssetID, }, In: &secp256k1fx.TransferInput{ Amt: 50000000, @@ -117,7 +107,7 @@ var ( Outs: []Output{{ Address: common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"), Amount: 50000000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }}, }, Creds: []Credential{ @@ -128,7 +118,7 @@ var ( }, }, }, - json: `{ + JSON: `{ "unsignedTx":{ "networkID":1, "blockchainID":"2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", @@ -155,24 +145,24 @@ var ( ] }] }`, - bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001c52b712aa7dce27a650bf509f799673e245edd4fa9e4e1700eb6105202fe579a0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000002faf080000000010000000000000001b8b5a87d1c05676f1f966da49151fa54dbe68c330000000002faf08021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000100000009000000013e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01"), - op: hook.Op{ + Bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001c52b712aa7dce27a650bf509f799673e245edd4fa9e4e1700eb6105202fe579a0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000002faf080000000010000000000000001b8b5a87d1c05676f1f966da49151fa54dbe68c330000000002faf08021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000100000009000000013e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01"), + Op: hook.Op{ ID: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), Gas: 11230, Mint: map[common.Address]uint256.Int{ common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"): *uint256.NewInt(50_000_000 * _x2cRate), }, }, - atomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), - atomicRequests: &chainsatomic.Requests{ + AtomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + AtomicRequests: &chainsatomic.Requests{ RemoveRequests: [][]byte{ common.FromHex("0xfd9e10917c4a2dab395683cfb766cdc584eba118bc22d3d0fc356fb79345cf64"), }, }, }, { - name: "export", // Included in https://subnets.avax.network/c-chain/block/48 - old: &atomic.Tx{ + Name: "export", // Included in https://subnets.avax.network/c-chain/block/48 + Old: &atomic.Tx{ UnsignedAtomicTx: &atomic.UnsignedExportTx{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -180,11 +170,11 @@ var ( Ins: []atomic.EVMInput{{ Address: common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), Amount: 1000001, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }}, ExportedOutputs: []*avax.TransferableOutput{{ Asset: avax.Asset{ - ID: avaxAssetID, + ID: AVAXAssetID, }, Out: &secp256k1fx.TransferOutput{ Amt: 1, @@ -205,7 +195,7 @@ var ( }, }, }, - new: &Tx{ + New: &Tx{ Unsigned: &Export{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -213,11 +203,11 @@ var ( Ins: []Input{{ Address: common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"), Amount: 1000001, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }}, ExportedOutputs: []*avax.TransferableOutput{{ Asset: avax.Asset{ - ID: avaxAssetID, + ID: AVAXAssetID, }, Out: &secp256k1fx.TransferOutput{ Amt: 1, @@ -238,7 +228,7 @@ var ( }, }, }, - json: `{ + JSON: `{ "unsignedTx":{ "networkID":1, "blockchainID":"2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", @@ -266,8 +256,8 @@ var ( ] }] }`, - bytes: common.FromHex("0x000000000001000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001eb019ccd325ad53543a7e7e3b04828bdecf3cff600000000000f424121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000000000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500000000010000000900000001254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01"), - op: hook.Op{ + Bytes: common.FromHex("0x000000000001000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001eb019ccd325ad53543a7e7e3b04828bdecf3cff600000000000f424121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000000000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500000000010000000900000001254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01"), + Op: hook.Op{ ID: ids.FromStringOrPanic("ng7Dox1r8nctrF6zurhRPYWxkmE2juUhT7Qhpauyo8qSEu6jB"), Gas: 11230, GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 11230), @@ -278,8 +268,8 @@ var ( }, }, }, - atomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), - atomicRequests: &chainsatomic.Requests{ + AtomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + AtomicRequests: &chainsatomic.Requests{ PutRequests: []*chainsatomic.Element{{ Key: common.FromHex("0x38ebe8fc127b2eaeeb25c72a747e0ef27460fb04b5929568ed959d67ec3e4948"), Value: common.FromHex("0x000067b5812292324365c6e2a479b2601cd1cd1facc2fcc8c29d58b5ed96583ea17e0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500"), @@ -290,8 +280,8 @@ var ( }, }, { - name: "import_multi_input", // Included in https://subnets.avax.network/c-chain/block/132481 - old: &atomic.Tx{ + Name: "import_multi_input", // Included in https://subnets.avax.network/c-chain/block/132481 + Old: &atomic.Tx{ UnsignedAtomicTx: &atomic.UnsignedImportTx{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -301,7 +291,7 @@ var ( UTXOID: avax.UTXOID{ TxID: ids.FromStringOrPanic("DqRKjysHeiKWetgyqqM2WdnX56yg8wBdY95RhuP3eDbbVoMCH"), }, - Asset: avax.Asset{ID: avaxAssetID}, + Asset: avax.Asset{ID: AVAXAssetID}, In: &secp256k1fx.TransferInput{ Amt: 99000000, Input: secp256k1fx.Input{ @@ -313,7 +303,7 @@ var ( UTXOID: avax.UTXOID{ TxID: ids.FromStringOrPanic("25YuXY1zoYY3DgLsRbGjdNSx3jYtvqZRgFo6jpy7EMCfUn4S74"), }, - Asset: avax.Asset{ID: avaxAssetID}, + Asset: avax.Asset{ID: AVAXAssetID}, In: &secp256k1fx.TransferInput{ Amt: 399000000, Input: secp256k1fx.Input{ @@ -325,7 +315,7 @@ var ( UTXOID: avax.UTXOID{ TxID: ids.FromStringOrPanic("2DXSj1kzqWM5HWS2PXcDSD3GUNpEGinynV1qD6LxiECHmZC8fj"), }, - Asset: avax.Asset{ID: avaxAssetID}, + Asset: avax.Asset{ID: AVAXAssetID}, In: &secp256k1fx.TransferInput{ Amt: 99000000, Input: secp256k1fx.Input{ @@ -338,17 +328,17 @@ var ( { Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), Amount: 99000000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), Amount: 99000000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), Amount: 399000000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, }, }, @@ -370,7 +360,7 @@ var ( }, }, }, - new: &Tx{ + New: &Tx{ Unsigned: &Import{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -380,7 +370,7 @@ var ( UTXOID: avax.UTXOID{ TxID: ids.FromStringOrPanic("DqRKjysHeiKWetgyqqM2WdnX56yg8wBdY95RhuP3eDbbVoMCH"), }, - Asset: avax.Asset{ID: avaxAssetID}, + Asset: avax.Asset{ID: AVAXAssetID}, In: &secp256k1fx.TransferInput{ Amt: 99000000, Input: secp256k1fx.Input{ @@ -392,7 +382,7 @@ var ( UTXOID: avax.UTXOID{ TxID: ids.FromStringOrPanic("25YuXY1zoYY3DgLsRbGjdNSx3jYtvqZRgFo6jpy7EMCfUn4S74"), }, - Asset: avax.Asset{ID: avaxAssetID}, + Asset: avax.Asset{ID: AVAXAssetID}, In: &secp256k1fx.TransferInput{ Amt: 399000000, Input: secp256k1fx.Input{ @@ -404,7 +394,7 @@ var ( UTXOID: avax.UTXOID{ TxID: ids.FromStringOrPanic("2DXSj1kzqWM5HWS2PXcDSD3GUNpEGinynV1qD6LxiECHmZC8fj"), }, - Asset: avax.Asset{ID: avaxAssetID}, + Asset: avax.Asset{ID: AVAXAssetID}, In: &secp256k1fx.TransferInput{ Amt: 99000000, Input: secp256k1fx.Input{ @@ -417,17 +407,17 @@ var ( { Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), Amount: 99000000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), Amount: 99000000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"), Amount: 399000000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, }, }, @@ -449,7 +439,7 @@ var ( }, }, }, - json: `{ + JSON: `{ "unsignedTx":{ "networkID":1, "blockchainID":"2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", @@ -489,16 +479,16 @@ var ( {"signatures":["0x4e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700"]} ] }`, - bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b000000031d249d0aab138afe01e6eff9c4789018a600771d94f5396b5df7b9d05298714d0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000005e69ec000000001000000008e0713e47bfc29bef4cee6e4635da1c74a3aabade68ccad6fca3e99fd827eb1c0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000017c841c00000000100000000a022a8b069a5d5e54c7e09c5c5b0f762c6751068bef15fe951a5e4b349d642200000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000005e69ec0000000010000000000000003383c293db6be7ac246f0956ad632344dc2cd1da30000000005e69ec021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff383c293db6be7ac246f0956ad632344dc2cd1da30000000005e69ec021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff383c293db6be7ac246f0956ad632344dc2cd1da30000000017c841c021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000300000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b342570000000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b342570000000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700"), - op: hook.Op{ + Bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b000000031d249d0aab138afe01e6eff9c4789018a600771d94f5396b5df7b9d05298714d0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000005e69ec000000001000000008e0713e47bfc29bef4cee6e4635da1c74a3aabade68ccad6fca3e99fd827eb1c0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000017c841c00000000100000000a022a8b069a5d5e54c7e09c5c5b0f762c6751068bef15fe951a5e4b349d642200000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000005e69ec0000000010000000000000003383c293db6be7ac246f0956ad632344dc2cd1da30000000005e69ec021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff383c293db6be7ac246f0956ad632344dc2cd1da30000000005e69ec021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff383c293db6be7ac246f0956ad632344dc2cd1da30000000017c841c021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000300000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b342570000000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b342570000000009000000014e14b32cb790fdccc3ee4700c84d0d53986ea8f125bd69ce771d9db45f86705c48b01bbe763dddea3d27069ed12f9b3050c9dcd487830d03d6a4d90e21b3425700"), + Op: hook.Op{ ID: ids.FromStringOrPanic("2Av7bXLRwxiQhbT9EcQd8KRM3Lz6VkpTqf3Y1AT5peHZ4YAohS"), Gas: 13526, Mint: map[common.Address]uint256.Int{ common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"): *uint256.NewInt(597_000_000 * _x2cRate), }, }, - atomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), - atomicRequests: &chainsatomic.Requests{ + AtomicRequestsChainID: ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"), + AtomicRequests: &chainsatomic.Requests{ RemoveRequests: [][]byte{ common.FromHex("0x821514ed5d925142159bc2c78bc56b043200e53aab79e97ca75e7ca7f6a96d05"), common.FromHex("0xea05e5c7135613b689d9f6b9903f431067ed72a2957ca82a652de1e8fef2c630"), @@ -507,8 +497,8 @@ var ( }, }, { - name: "export_same_address_multi_asset", // Synthetic - old: &atomic.Tx{ + Name: "export_same_address_multi_asset", // Synthetic + Old: &atomic.Tx{ UnsignedAtomicTx: &atomic.UnsignedExportTx{ Ins: []atomic.EVMInput{ { @@ -517,7 +507,7 @@ var ( }, { Amount: 1_000_000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, Nonce: 5, }, }, @@ -525,7 +515,7 @@ var ( }, Creds: []verify.Verifiable{}, }, - new: &Tx{ + New: &Tx{ Unsigned: &Export{ Ins: []Input{ { @@ -534,7 +524,7 @@ var ( }, { Amount: 1_000_000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, Nonce: 5, }, }, @@ -542,7 +532,7 @@ var ( }, Creds: []Credential{}, }, - json: `{ + JSON: `{ "unsignedTx":{ "networkID":0, "blockchainID":"11111111111111111111111111111111LpoYY", @@ -555,8 +545,8 @@ var ( }, "credentials":[] }`, - bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000050000000000000000"), - op: hook.Op{ + Bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000050000000000000000"), + Op: hook.Op{ ID: ids.FromStringOrPanic("29cCETWxEUN1QCuex59j46Xtr8urBRo5M7HzwBqC3qDXWd73sX"), Gas: 12218, GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 12218), @@ -568,13 +558,13 @@ var ( }, }, }, - atomicRequests: &chainsatomic.Requests{ + AtomicRequests: &chainsatomic.Requests{ PutRequests: []*chainsatomic.Element{}, }, }, { - name: "export_multi_address_multi_asset", // Synthetic - old: &atomic.Tx{ + Name: "export_multi_address_multi_asset", // Synthetic + Old: &atomic.Tx{ UnsignedAtomicTx: &atomic.UnsignedExportTx{ Ins: []atomic.EVMInput{ { @@ -585,7 +575,7 @@ var ( { Address: common.Address{2}, Amount: 1_000_000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, Nonce: 7, }, }, @@ -593,7 +583,7 @@ var ( }, Creds: []verify.Verifiable{}, }, - new: &Tx{ + New: &Tx{ Unsigned: &Export{ Ins: []Input{ { @@ -604,7 +594,7 @@ var ( { Address: common.Address{2}, Amount: 1_000_000, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, Nonce: 7, }, }, @@ -612,7 +602,7 @@ var ( }, Creds: []Credential{}, }, - json: `{ + JSON: `{ "unsignedTx":{ "networkID":0, "blockchainID":"11111111111111111111111111111111LpoYY", @@ -625,8 +615,8 @@ var ( }, "credentials":[] }`, - bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002010000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005020000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000070000000000000000"), - op: hook.Op{ + Bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002010000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005020000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000070000000000000000"), + Op: hook.Op{ ID: ids.FromStringOrPanic("8P9XRKhxHeTv3t4Aj9cTV6dD5h78WVFH8nctLuCkeSavfKeEG"), Gas: 12218, GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 12218), @@ -641,13 +631,13 @@ var ( }, }, }, - atomicRequests: &chainsatomic.Requests{ + AtomicRequests: &chainsatomic.Requests{ PutRequests: []*chainsatomic.Element{}, }, }, { - name: "import_non_avax", // Synthetic - old: &atomic.Tx{ + Name: "import_non_avax", // Synthetic + Old: &atomic.Tx{ UnsignedAtomicTx: &atomic.UnsignedImportTx{ ImportedInputs: []*avax.TransferableInput{{ In: &secp256k1fx.TransferInput{ @@ -663,7 +653,7 @@ var ( }, Creds: []verify.Verifiable{}, }, - new: &Tx{ + New: &Tx{ Unsigned: &Import{ ImportedInputs: []*avax.TransferableInput{{ In: &secp256k1fx.TransferInput{ @@ -679,7 +669,7 @@ var ( }, Creds: []Credential{}, }, - json: `{ + JSON: `{ "unsignedTx":{ "networkID":0, "blockchainID":"11111111111111111111111111111111LpoYY", @@ -699,21 +689,21 @@ var ( }, "credentials":[] }`, - bytes: common.FromHex("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000003e70000000000000001000000000000000000000000000000000000000000000000000003e7000000000000000000000000000000000000000000000000000000000000000000000000"), - op: hook.Op{ + Bytes: common.FromHex("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000003e70000000000000001000000000000000000000000000000000000000000000000000003e7000000000000000000000000000000000000000000000000000000000000000000000000"), + Op: hook.Op{ ID: ids.FromStringOrPanic("s4xoHkf4rPQYSwjbQo78hcSP1wSeViV1Fx2PHM4AfRiDurFkf"), Gas: 10226, Mint: map[common.Address]uint256.Int{}, }, - atomicRequests: &chainsatomic.Requests{ + AtomicRequests: &chainsatomic.Requests{ RemoveRequests: [][]byte{ common.FromHex("0x2c34ce1df23b838c5abf2a7f6437cca3d3067ed509ff25f11df6b11b582b51eb"), }, }, }, } - oldTxs []*atomic.Tx - newTxs []*Tx + OldTxs []*atomic.Tx + NewTxs []*Tx // txIgnore ignores caching/context fields populated lazily on the txs. txIgnore = cmpopts.IgnoreUnexported( @@ -724,50 +714,50 @@ var ( ) func init() { - oldTxs = make([]*atomic.Tx, len(tests)) - newTxs = make([]*Tx, len(tests)) - for i, test := range tests { - oldTxs[i] = test.old - newTxs[i] = test.new + OldTxs = make([]*atomic.Tx, len(Tests)) + NewTxs = make([]*Tx, len(Tests)) + for i, test := range Tests { + OldTxs[i] = test.Old + NewTxs[i] = test.New } } func TestID(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { // We must parse the old tx to properly initialize the ID. - old, err := parseOldTx(test.bytes) + old, err := ParseOldTx(test.Bytes) require.NoError(t, err, "parseOldTx()") - assert.Equalf(t, test.op.ID, old.ID(), "%T.ID()", old) + assert.Equalf(t, test.Op.ID, old.ID(), "%T.ID()", old) }) t.Run("new", func(t *testing.T) { - assert.Equalf(t, test.op.ID, test.new.ID(), "%T.ID()", test.new) + assert.Equalf(t, test.Op.ID, test.New.ID(), "%T.ID()", test.New) }) }) } } func TestBytes(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { - got, err := atomic.Codec.Marshal(atomic.CodecVersion, test.old) - require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, test.old) - assert.Equalf(t, test.bytes, got, "%T.Marshal(, %T)", atomic.Codec, test.old) + got, err := atomic.Codec.Marshal(atomic.CodecVersion, test.Old) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, test.Old) + assert.Equalf(t, test.Bytes, got, "%T.Marshal(, %T)", atomic.Codec, test.Old) }) t.Run("new", func(t *testing.T) { - got, err := test.new.Bytes() - require.NoErrorf(t, err, "%T.Bytes()", test.new) - assert.Equalf(t, test.bytes, got, "%T.Bytes()", test.new) + got, err := test.New.Bytes() + require.NoErrorf(t, err, "%T.Bytes()", test.New) + assert.Equalf(t, test.Bytes, got, "%T.Bytes()", test.New) }) }) } } func TestMarshalSlice(t *testing.T) { - want, err := atomic.Codec.Marshal(atomic.CodecVersion, oldTxs) - require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldTxs) + want, err := atomic.Codec.Marshal(atomic.CodecVersion, OldTxs) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, OldTxs) tests := []struct { name string @@ -776,7 +766,7 @@ func TestMarshalSlice(t *testing.T) { }{ { name: "mainnet", - txs: newTxs, + txs: NewTxs, want: want, }, { @@ -793,20 +783,20 @@ func TestMarshalSlice(t *testing.T) { } func TestParse(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { got := new(atomic.Tx) - _, err := atomic.Codec.Unmarshal(test.bytes, got) + _, err := atomic.Codec.Unmarshal(test.Bytes, got) require.NoErrorf(t, err, "%T.Unmarshal(, %T)", atomic.Codec, got) - if diff := cmp.Diff(test.old, got, txIgnore); diff != "" { + if diff := cmp.Diff(test.Old, got, txIgnore); diff != "" { t.Errorf("%T.Unmarshal(, %T) diff (-want +got):\n%s", atomic.Codec, got, diff) } }) t.Run("new", func(t *testing.T) { - got, err := Parse(test.bytes) + got, err := Parse(test.Bytes) require.NoError(t, err, "Parse()") - if diff := cmp.Diff(test.new, got, txIgnore); diff != "" { + if diff := cmp.Diff(test.New, got, txIgnore); diff != "" { t.Errorf("Parse() diff (-want +got):\n%s", diff) } }) @@ -815,8 +805,8 @@ func TestParse(t *testing.T) { } func TestParseSlice(t *testing.T) { - bytes, err := atomic.Codec.Marshal(atomic.CodecVersion, oldTxs) - require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldTxs) + bytes, err := atomic.Codec.Marshal(atomic.CodecVersion, OldTxs) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, OldTxs) tests := []struct { name string @@ -827,7 +817,7 @@ func TestParseSlice(t *testing.T) { { name: "mainnet", bytes: bytes, - want: newTxs, + want: NewTxs, }, { name: "empty", @@ -856,10 +846,10 @@ func TestParseSlice(t *testing.T) { var errUnexpectedCredentialType = errors.New("unexpected credential type") -// parseOldTx parses a transaction using coreth's old parsing logic but enforces +// ParseOldTx parses a transaction using coreth's old parsing logic but enforces // additional restrictions. Coreth's parsing logic is overly permissive and // depends on later verification in [vm.VerifierBackend]. -func parseOldTx(b []byte) (*atomic.Tx, error) { +func ParseOldTx(b []byte) (*atomic.Tx, error) { tx, err := atomic.ExtractAtomicTx(b, atomic.Codec) if err != nil { return nil, err @@ -872,10 +862,10 @@ func parseOldTx(b []byte) (*atomic.Tx, error) { return tx, nil } -// parseOldTxs parses a slice of transactions using coreth's old parsing logic +// ParseOldTxs parses a slice of transactions using coreth's old parsing logic // but enforces additional restrictions. Coreth's parsing logic is overly // permissive and depends on later verification in [vm.VerifierBackend]. -func parseOldTxs(b []byte) ([]*atomic.Tx, error) { +func ParseOldTxs(b []byte) ([]*atomic.Tx, error) { txs, err := atomic.ExtractAtomicTxs(b, true, atomic.Codec) if err != nil { return nil, err @@ -890,42 +880,9 @@ func parseOldTxs(b []byte) ([]*atomic.Tx, error) { return txs, nil } -func FuzzParseCompatibility(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) - } - f.Fuzz(func(t *testing.T, data []byte) { - _, oldErr := parseOldTx(data) - oldOk := oldErr == nil - - _, newErr := Parse(data) - newOk := newErr == nil - - assert.Equal(t, oldOk, newOk, "Parse(b) == parseOldTx(b)") - }) -} - -func FuzzParseSliceCompatibility(f *testing.F) { - { - b, err := MarshalSlice(newTxs) - require.NoError(f, err, "MarshalSlice()") - f.Add(b) - } - - f.Fuzz(func(t *testing.T, data []byte) { - _, oldErr := parseOldTxs(data) - oldOk := oldErr == nil - - _, newErr := ParseSlice(data) - newOk := newErr == nil - - assert.Equal(t, oldOk, newOk, "ParseSlice(b) == parseOldTxs(b)") - }) -} - func FuzzParseRoundTrip(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) + for _, test := range Tests { + f.Add(test.Bytes) } f.Fuzz(func(t *testing.T, data []byte) { tx, err := Parse(data) @@ -941,7 +898,7 @@ func FuzzParseRoundTrip(f *testing.F) { func FuzzParseSliceRoundTrip(f *testing.F) { { - b, err := MarshalSlice(newTxs) + b, err := MarshalSlice(NewTxs) require.NoError(f, err, "MarshalSlice()") f.Add(b) } @@ -961,52 +918,39 @@ func FuzzParseSliceRoundTrip(f *testing.F) { } func TestJSONMarshal(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { - got, err := json.Marshal(test.old) - require.NoErrorf(t, err, "json.Marshal(%T)", test.old) - assert.JSONEqf(t, test.json, string(got), "json.Marshal(%T)", test.old) + got, err := json.Marshal(test.Old) + require.NoErrorf(t, err, "json.Marshal(%T)", test.Old) + assert.JSONEqf(t, test.JSON, string(got), "json.Marshal(%T)", test.Old) }) t.Run("new", func(t *testing.T) { - got, err := json.Marshal(test.new) - require.NoErrorf(t, err, "json.Marshal(%T)", test.new) - assert.JSONEqf(t, test.json, string(got), "json.Marshal(%T)", test.new) + got, err := json.Marshal(test.New) + require.NoErrorf(t, err, "json.Marshal(%T)", test.New) + assert.JSONEqf(t, test.JSON, string(got), "json.Marshal(%T)", test.New) }) }) } } -func FuzzJSONCompatibility(f *testing.F) { - fuzzTx(f, func(t *testing.T, newTx *Tx) { - oldTx := toOldTx(t, newTx) - - oldJSON, err := json.Marshal(oldTx) - require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) - - newJSON, err := json.Marshal(newTx) - require.NoErrorf(t, err, "json.Marshal(%T)", newTx) - assert.JSONEq(t, string(oldJSON), string(newJSON)) - }) -} - -func toOldTx(tb testing.TB, newTx *Tx) *atomic.Tx { +func ToOldTx(tb testing.TB, newTx *Tx) *atomic.Tx { tb.Helper() bytes, err := newTx.Bytes() require.NoErrorf(tb, err, "%T.Bytes()", newTx) - oldTx, err := parseOldTx(bytes) + oldTx, err := ParseOldTx(bytes) require.NoError(tb, err, "parseOldTx()") return oldTx } func TestAsOp(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got, err := test.new.AsOp(avaxAssetID) - require.NoErrorf(t, err, "%T.AsOp(avaxAssetID)", test.new) - assert.Equalf(t, test.op, got, "%T.AsOp(avaxAssetID)", test.new) + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { + got, err := test.New.AsOp(AVAXAssetID) + require.NoErrorf(t, err, "%T.AsOp(avaxAssetID)", test.New) + assert.Equalf(t, test.Op, got, "%T.AsOp(avaxAssetID)", test.New) }) } } @@ -1035,13 +979,13 @@ func TestAsOp_Errors(t *testing.T) { name: "import_burned_underflow", tx: &Import{ ImportedInputs: []*avax.TransferableInput{{ - Asset: avax.Asset{ID: avaxAssetID}, + Asset: avax.Asset{ID: AVAXAssetID}, In: &secp256k1fx.TransferInput{ Amt: 1, }, }}, Outs: []Output{{ - AssetID: avaxAssetID, + AssetID: AVAXAssetID, Amount: 2, }}, }, @@ -1051,11 +995,11 @@ func TestAsOp_Errors(t *testing.T) { name: "export_burned_underflow", tx: &Export{ Ins: []Input{{ - AssetID: avaxAssetID, + AssetID: AVAXAssetID, Amount: 1, }}, ExportedOutputs: []*avax.TransferableOutput{{ - Asset: avax.Asset{ID: avaxAssetID}, + Asset: avax.Asset{ID: AVAXAssetID}, Out: &secp256k1fx.TransferOutput{ Amt: 2, }, @@ -1069,129 +1013,24 @@ func TestAsOp_Errors(t *testing.T) { tx := &Tx{ Unsigned: test.tx, } - _, err := tx.AsOp(avaxAssetID) + _, err := tx.AsOp(AVAXAssetID) require.ErrorIsf(t, err, test.want, "%T.AsOp(avaxAssetID)", tx) }) } } -func FuzzAsOpCompatibility(f *testing.F) { - fuzzTx(f, func(t *testing.T, newTx *Tx) { - op, err := newTx.AsOp(avaxAssetID) - if err != nil { - t.Skip("invalid tx") - } - - oldTx := toOldTx(t, newTx) - gasUsed, err := oldTx.UnsignedAtomicTx.GasUsed(true) - require.NoErrorf(t, err, "%T.GasUsed(true)", oldTx.UnsignedAtomicTx) - - gasPrice, err := atomic.EffectiveGasPrice(oldTx.UnsignedAtomicTx, avaxAssetID, true) - require.NoErrorf(t, err, "atomic.EffectiveGasPrice(%T, avaxAssetID, true)", oldTx) - - state := newFuzzStateDB() - if export, ok := oldTx.UnsignedAtomicTx.(*atomic.UnsignedExportTx); ok { - for _, in := range export.Ins { - state.initialNonces[in.Address] = in.Nonce - } - } - - ctx := &snow.Context{AVAXAssetID: avaxAssetID} - require.NoErrorf(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, state), "%T.EVMStateTransfer()", oldTx.UnsignedAtomicTx) - - expected := hook.Op{ - ID: oldTx.ID(), - Gas: gas.Gas(gasUsed), - GasFeeCap: gasPrice, - Burn: state.op.burn, - Mint: state.op.mint, - } - if diff := cmp.Diff(expected, op, cmpopts.EquateEmpty()); diff != "" { - t.Errorf("%T.AsOp() diff (-want +got):\n%s", newTx, diff) - } - }) -} - -// fuzzStateDB is an in-memory [atomic.StateDB] for [FuzzAsOp]. It constructs an -// [op] from the mutations of [atomic.UnsignedAtomicTx.EVMStateTransfer]. -type fuzzStateDB struct { - initialNonces map[common.Address]uint64 - op op -} - -func newFuzzStateDB() *fuzzStateDB { - return &fuzzStateDB{ - initialNonces: make(map[common.Address]uint64), - op: op{ - burn: make(map[common.Address]hook.AccountDebit), - mint: make(map[common.Address]uint256.Int), - }, - } -} - -func (f *fuzzStateDB) AddBalance(addr common.Address, amount *uint256.Int) { - b := f.op.mint[addr] - b.Add(&b, amount) - f.op.mint[addr] = b -} - -func (f *fuzzStateDB) SubBalance(addr common.Address, amount *uint256.Int) { - d := f.op.burn[addr] - d.Amount.Add(&d.Amount, amount) - d.MinBalance = d.Amount - f.op.burn[addr] = d -} - -func (*fuzzStateDB) GetBalance(common.Address) *uint256.Int { - // Large enough to never underflow, but small enough to never overflow. - return new(uint256.Int).Lsh(uint256.NewInt(1), 128) -} - -func (*fuzzStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} - -func (*fuzzStateDB) SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} - -func (*fuzzStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { - // Large enough to never underflow, but small enough to never overflow. - return new(big.Int).Lsh(big.NewInt(1), 128) -} - -func (f *fuzzStateDB) SetNonce(addr common.Address, nonce uint64) { - d := f.op.burn[addr] - d.Nonce = nonce - 1 - f.op.burn[addr] = d -} - -func (f *fuzzStateDB) GetNonce(addr common.Address) uint64 { - return f.initialNonces[addr] -} - func TestAtomicRequests(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - chainID, requests, err := test.new.AtomicRequests() - require.NoErrorf(t, err, "%T.AtomicRequests()", test.new) - assert.Equalf(t, test.atomicRequestsChainID, chainID, "%T.AtomicRequests().ChainID", test.new) - assert.Equalf(t, test.atomicRequests, requests, "%T.AtomicRequests().Requests", test.new) + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { + chainID, requests, err := test.New.AtomicRequests() + require.NoErrorf(t, err, "%T.AtomicRequests()", test.New) + assert.Equalf(t, test.AtomicRequestsChainID, chainID, "%T.AtomicRequests().ChainID", test.New) + assert.Equalf(t, test.AtomicRequests, requests, "%T.AtomicRequests().Requests", test.New) }) } } -func FuzzAtomicRequestsCompatibility(f *testing.F) { - fuzzTx(f, func(t *testing.T, newTx *Tx) { - oldTx := toOldTx(t, newTx) - - oldChainID, oldRequests, err := oldTx.UnsignedAtomicTx.AtomicOps() - require.NoErrorf(t, err, "%T.AtomicOps()", oldTx.UnsignedAtomicTx) - - newChainID, newRequests, err := newTx.AtomicRequests() - require.NoErrorf(t, err, "%T.AtomicRequests()", newTx) - assert.Equal(t, oldChainID, newChainID, "chainID") - assert.Equal(t, oldRequests, newRequests, "requests") - }) -} - -func newStateDB(t testing.TB) *extstate.StateDB { +func NewStateDB(t testing.TB) *extstate.StateDB { t.Helper() db := state.NewDatabase(rawdb.NewMemoryDatabase()) @@ -1220,7 +1059,7 @@ func TestTransferNonAVAX(t *testing.T) { Outs: []Output{{ Address: alice, Amount: 1, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }}, }, }, @@ -1248,17 +1087,17 @@ func TestTransferNonAVAX(t *testing.T) { { Address: alice, Amount: 1, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: alice, Amount: 10, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: bob, Amount: 100, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: alice, @@ -1299,7 +1138,7 @@ func TestTransferNonAVAX(t *testing.T) { { Address: alice, Amount: 1, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, }, }, @@ -1343,17 +1182,17 @@ func TestTransferNonAVAX(t *testing.T) { { Address: alice, Amount: 1, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: alice, Amount: 10, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: bob, Amount: 100, - AssetID: avaxAssetID, + AssetID: AVAXAssetID, }, { Address: alice, @@ -1429,7 +1268,7 @@ func TestTransferNonAVAX(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - sdb := newStateDB(t) + sdb := NewStateDB(t) for addr, balances := range test.init { for assetID, amount := range balances { coinID := common.Hash(assetID) @@ -1437,7 +1276,7 @@ func TestTransferNonAVAX(t *testing.T) { } } - err := test.tx.TransferNonAVAX(avaxAssetID, sdb) + err := test.tx.TransferNonAVAX(AVAXAssetID, sdb) require.ErrorIs(t, err, test.wantErr) for addr, balances := range test.want { for assetID, want := range balances { @@ -1449,48 +1288,3 @@ func TestTransferNonAVAX(t *testing.T) { }) } } - -func FuzzTransferNonAVAXCompatibility(f *testing.F) { - fuzzTx(f, func(t *testing.T, newTx *Tx) { - if _, err := newTx.AsOp(avaxAssetID); err != nil { - t.Skip("invalid tx") - } - - oldSDB := newStateDB(t) - newSDB := newStateDB(t) - - hugeAVAX := new(uint256.Int).Lsh(uint256.NewInt(1), 128) - hugeBig := new(big.Int).Lsh(big.NewInt(1), 128) - for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { - if tx, ok := newTx.Unsigned.(*Export); ok { - for _, in := range tx.Ins { - sdb.AddBalance(in.Address, hugeAVAX) - sdb.SetNonce(in.Address, in.Nonce) - sdb.AddBalanceMultiCoin(in.Address, common.Hash(in.AssetID), hugeBig) - } - } - } - - var ( - oldTx = toOldTx(t, newTx) - ctx = &snow.Context{AVAXAssetID: avaxAssetID} - ) - require.NoError(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldSDB)) - require.NoError(t, newTx.TransferNonAVAX(avaxAssetID, newSDB)) - - // Materialize journaled writes into the trie so that - // [cmputils.StateDBs], which dumps the trie, reflects them. - for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { - sdb.Finalise(true) - sdb.IntermediateRoot(true) - } - - opts := []cmp.Option{ - cmpopts.IgnoreUnexported(extstate.StateDB{}), - cmputils.StateDBs(), - } - if diff := cmp.Diff(oldSDB, newSDB, opts...); diff != "" { - t.Errorf("%T.AsOp() diff (-want +got):\n%s", newTx, diff) - } - }) -} diff --git a/vms/saevm/cchain/txtest/fuzzer.go b/vms/saevm/cchain/txtest/fuzzer.go new file mode 100644 index 000000000000..748dcecb67a8 --- /dev/null +++ b/vms/saevm/cchain/txtest/fuzzer.go @@ -0,0 +1,410 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package txtest provides test helpers for using [tx.Tx]. +package txtest + +import ( + "encoding/binary" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +// F works like [testing.F], but allows for the usage of [tx.Tx]. +// +// This type should be used over [testing.F] when it is desired to consistently +// produce a parseable transaction. +// +// [F.Addresses] and [F.AssetIDs] are propagated to the [Fuzzer] used for each +// fuzz iteration. +type F struct { + *testing.F + + // Addresses, if non-empty, biases [Fuzzer.Address] toward this alphabet. + Addresses []common.Address + // AssetIDs, if non-empty, biases [Fuzzer.AssetID] toward this alphabet. + AssetIDs []ids.ID +} + +// Add works like [testing.F.Add], but expects a [tx.Tx]. +func (f *F) Add(tx *tx.Tx) { + var e encoder + e.unsigned(tx.Unsigned) + encodeSlice(&e, tx.Creds, (*encoder).credential) + f.F.Add([]byte(e)) +} + +// Fuzz works like [testing.F.Fuzz], but provides a [tx.Tx]. +func (f *F) Fuzz(test func(t *testing.T, tx *tx.Tx)) { + f.F.Fuzz(func(t *testing.T, data []byte) { + fuzz := Fuzzer{ + Data: data, + Addresses: f.Addresses, + AssetIDs: f.AssetIDs, + } + genTx := fuzz.Tx() + + // genTx isn't always ideally formatted, so we round-trip through + // parsing before providing it to the test body. + bytes, err := genTx.Bytes() + if err != nil { + t.Skipf("invalid tx: %s", err) + } + tx, err := tx.Parse(bytes) + require.NoError(t, err, "Parse()") + test(t, tx) + }) +} + +// Fuzzer turns a byte stream into structured data. +// +// The byte stream in [Fuzzer.Data] is consumed left-to-right; once exhausted, +// methods return the zero value of their result type. +type Fuzzer struct { + // Data is the byte stream backing the fuzzer. + Data []byte + // Addresses, if non-empty, biases [Fuzzer.Address] toward this alphabet. + // When empty, [Fuzzer.Address] always returns a fully random address + // consumed from [Fuzzer.Data]. + Addresses []common.Address + // AssetIDs, if non-empty, biases [Fuzzer.AssetID] toward this alphabet. + // When empty, [Fuzzer.AssetID] always returns a fully random ID consumed + // from [Fuzzer.Data]. + AssetIDs []ids.ID +} + +// Bytes returns a slice of n random bytes. +// +// Once the fuzzer is exhausted, the returned bytes are zeroes. +func (f *Fuzzer) Bytes(n int) []byte { + out := make([]byte, n) + copy(out, f.Data) + if n >= len(f.Data) { + f.Data = nil + } else { + f.Data = f.Data[n:] + } + return out +} + +// Bool returns a random bool. +// +// Once the fuzzer is exhausted, false is returned. +func (f *Fuzzer) Bool() bool { return f.Bytes(1)[0]&1 != 0 } + +// Uint32 returns a random uint32. +// +// Once the fuzzer is exhausted, 0 is returned. +func (f *Fuzzer) Uint32() uint32 { return binary.BigEndian.Uint32(f.Bytes(4)) } + +// Uint64 returns a random uint64. +// +// Once the fuzzer is exhausted, 0 is returned. +func (f *Fuzzer) Uint64() uint64 { return binary.BigEndian.Uint64(f.Bytes(8)) } + +// ID returns a random [ids.ID]. +// +// Once the fuzzer is exhausted, [ids.Empty] is returned. +func (f *Fuzzer) ID() ids.ID { return ids.ID(f.Bytes(ids.IDLen)) } + +// ShortID returns a random [ids.ShortID]. +// +// Once the fuzzer is exhausted, [ids.ShortEmpty] is returned. +func (f *Fuzzer) ShortID() ids.ShortID { return ids.ShortID(f.Bytes(ids.ShortIDLen)) } + +// Signature returns a random 65-byte array. +// +// Once the fuzzer is exhausted, the zero value is returned. +func (f *Fuzzer) Signature() [65]byte { return [65]byte(f.Bytes(65)) } + +// Intn returns a value in [0, x). +// +// Once the fuzzer is exhausted, 0 is returned. +func (f *Fuzzer) Intn(x int) int { + if x < 1 { + return 0 + } + return int(f.Uint64() % uint64(x)) +} + +// Element returns a random Element in s. It panics if s is empty. +// +// Once the fuzzer is exhausted, the first entry in s is returned. +func Element[T any](f *Fuzzer, s []T) T { + return s[f.Intn(len(s))] +} + +// SliceOf generates a random slice of generated entries. The length is random, +// but is typically small. +// +// Once the fuzzer is exhausted, an empty slice is returned. +func SliceOf[T any](f *Fuzzer, gen func(*Fuzzer) T) []T { + var out []T + // Bool defaults to false once the generation has been exausted, so this + // will eventually terminate. + for f.Bool() { + out = append(out, gen(f)) + } + return out +} + +// Address returns a random [common.Address]. When [Fuzzer.Addresses] is +// non-empty, the result is biased toward that alphabet to encourage +// repeated-address code paths. +// +// Once the fuzzer is exhausted, the zero address is returned. +func (f *Fuzzer) Address() common.Address { + if !f.Bool() || len(f.Addresses) == 0 { + return common.Address(f.Bytes(common.AddressLength)) + } + return Element(f, f.Addresses) +} + +// AssetID returns a random [ids.ID]. When [Fuzzer.AssetIDs] is non-empty, +// the result is biased toward that alphabet to encourage repeated-asset code +// paths. +// +// Once the fuzzer is exhausted, [ids.Empty] is returned. +func (f *Fuzzer) AssetID() ids.ID { + if !f.Bool() || len(f.AssetIDs) == 0 { + return f.ID() + } + return Element(f, f.AssetIDs) +} + +// TransferableInput returns a random [*avax.TransferableInput]. +// +// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no +// signature indices is returned. +func (f *Fuzzer) TransferableInput() *avax.TransferableInput { + return &avax.TransferableInput{ + UTXOID: avax.UTXOID{ + TxID: f.ID(), + OutputIndex: f.Uint32(), + }, + Asset: avax.Asset{ID: f.AssetID()}, + In: &secp256k1fx.TransferInput{ + Amt: f.Uint64(), + Input: secp256k1fx.Input{ + SigIndices: SliceOf(f, (*Fuzzer).Uint32), + }, + }, + } +} + +// TransferableOutput returns a random [*avax.TransferableOutput]. +// +// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no +// owner addresses is returned. +func (f *Fuzzer) TransferableOutput() *avax.TransferableOutput { + return &avax.TransferableOutput{ + Asset: avax.Asset{ID: f.AssetID()}, + Out: &secp256k1fx.TransferOutput{ + Amt: f.Uint64(), + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: f.Uint64(), + Threshold: f.Uint32(), + Addrs: SliceOf(f, (*Fuzzer).ShortID), + }, + }, + } +} + +// Input returns a random [tx.Input]. +// +// Once the fuzzer is exhausted, the zero value is returned. +func (f *Fuzzer) Input() tx.Input { + return tx.Input{ + Address: f.Address(), + Amount: f.Uint64(), + AssetID: f.AssetID(), + Nonce: f.Uint64(), + } +} + +// Output returns a random [tx.Output]. +// +// Once the fuzzer is exhausted, the zero value is returned. +func (f *Fuzzer) Output() tx.Output { + return tx.Output{ + Address: f.Address(), + Amount: f.Uint64(), + AssetID: f.AssetID(), + } +} + +// ImportTx returns a random [*tx.Import]. +// +// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no +// imported inputs or outputs is returned. +func (f *Fuzzer) ImportTx() *tx.Import { + return &tx.Import{ + NetworkID: f.Uint32(), + BlockchainID: f.ID(), + SourceChain: f.ID(), + ImportedInputs: SliceOf(f, (*Fuzzer).TransferableInput), + Outs: SliceOf(f, (*Fuzzer).Output), + } +} + +// ExportTx returns a random [*tx.Export]. +// +// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no +// inputs or exported outputs is returned. +func (f *Fuzzer) ExportTx() *tx.Export { + return &tx.Export{ + NetworkID: f.Uint32(), + BlockchainID: f.ID(), + DestinationChain: f.ID(), + Ins: SliceOf(f, (*Fuzzer).Input), + ExportedOutputs: SliceOf(f, (*Fuzzer).TransferableOutput), + } +} + +// Unsigned returns a random [tx.Unsigned] — either a [*tx.Import] or a +// [*tx.Export]. +// +// Once the fuzzer is exhausted, an empty [*tx.Export] is returned. +func (f *Fuzzer) Unsigned() tx.Unsigned { + if f.Bool() { + return f.ImportTx() + } else { + return f.ExportTx() + } +} + +// Credential returns a random [tx.Credential]. +// +// Once the fuzzer is exhausted, a [*secp256k1fx.Credential] with no +// signatures is returned. +func (f *Fuzzer) Credential() tx.Credential { + return &secp256k1fx.Credential{ + Sigs: SliceOf(f, (*Fuzzer).Signature), + } +} + +// Tx returns a random [*tx.Tx]. +// +// Once the fuzzer is exhausted, a non-nil pointer to a tx wrapping an empty +// [*tx.Export] with no credentials is returned. +func (f *Fuzzer) Tx() *tx.Tx { + return &tx.Tx{ + Unsigned: f.Unsigned(), + Creds: SliceOf(f, (*Fuzzer).Credential), + } +} + +// encoder writes a byte stream that [Fuzzer] will decode into the same +// structure. +type encoder []byte + +func (e *encoder) bytes(b []byte) { *e = append(*e, b...) } +func (e *encoder) uint32(v uint32) { *e = binary.BigEndian.AppendUint32(*e, v) } +func (e *encoder) uint64(v uint64) { *e = binary.BigEndian.AppendUint64(*e, v) } +func (e *encoder) id(v ids.ID) { e.bytes(v[:]) } +func (e *encoder) shortID(v ids.ShortID) { e.bytes(v[:]) } +func (e *encoder) signature(v [65]byte) { e.bytes(v[:]) } + +func (e *encoder) bool(b bool) { + if b { + *e = append(*e, 1) + } else { + *e = append(*e, 0) + } +} + +// address always picks the raw-bytes branch in [Fuzzer.Address] so the encoded +// value is independent of the alphabet. +func (e *encoder) address(v common.Address) { + e.bool(false) + e.bytes(v[:]) +} + +// assetID always picks the raw-bytes branch in [Fuzzer.AssetID] so the encoded +// value is independent of the alphabet. +func (e *encoder) assetID(v ids.ID) { + e.bool(false) + e.id(v) +} + +func encodeSlice[T any](e *encoder, items []T, gen func(*encoder, T)) { + for _, item := range items { + e.bool(true) + gen(e, item) + } + e.bool(false) +} + +func (e *encoder) transferableInput(in *avax.TransferableInput) { + e.id(in.UTXOID.TxID) + e.uint32(in.UTXOID.OutputIndex) + e.assetID(in.Asset.ID) + ti := in.In.(*secp256k1fx.TransferInput) + e.uint64(ti.Amt) + encodeSlice(e, ti.Input.SigIndices, (*encoder).uint32) +} + +func (e *encoder) transferableOutput(out *avax.TransferableOutput) { + e.assetID(out.Asset.ID) + to := out.Out.(*secp256k1fx.TransferOutput) + e.uint64(to.Amt) + e.uint64(to.OutputOwners.Locktime) + e.uint32(to.OutputOwners.Threshold) + encodeSlice(e, to.OutputOwners.Addrs, (*encoder).shortID) +} + +func (e *encoder) input(i tx.Input) { + e.address(i.Address) + e.uint64(i.Amount) + e.assetID(i.AssetID) + e.uint64(i.Nonce) +} + +func (e *encoder) output(o tx.Output) { + e.address(o.Address) + e.uint64(o.Amount) + e.assetID(o.AssetID) +} + +func (e *encoder) importTx(t *tx.Import) { + e.uint32(t.NetworkID) + e.id(t.BlockchainID) + e.id(t.SourceChain) + encodeSlice(e, t.ImportedInputs, (*encoder).transferableInput) + encodeSlice(e, t.Outs, (*encoder).output) +} + +func (e *encoder) exportTx(t *tx.Export) { + e.uint32(t.NetworkID) + e.id(t.BlockchainID) + e.id(t.DestinationChain) + encodeSlice(e, t.Ins, (*encoder).input) + encodeSlice(e, t.ExportedOutputs, (*encoder).transferableOutput) +} + +func (e *encoder) unsigned(u tx.Unsigned) { + switch u := u.(type) { + case *tx.Import: + e.bool(true) + e.importTx(u) + case *tx.Export: + e.bool(false) + e.exportTx(u) + } +} + +func (e *encoder) credential(c tx.Credential) { + encodeSlice(e, c.Self().Sigs, (*encoder).signature) +} + +func (e *encoder) tx(tx *tx.Tx) { + e.unsigned(tx.Unsigned) + encodeSlice(e, tx.Creds, (*encoder).credential) +} From 2f01ee8d22b53f792d493d63bbec6db8f57f911c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 12:53:13 -0400 Subject: [PATCH 041/120] Unblock myself for sae progress --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1b3a55afa373..ae83b616d3f7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,3 +10,4 @@ /graft/evm @ava-labs/platform-evm /graft/subnet-evm @ava-labs/platform-evm /vms/saevm @ARR4N @StephenButtolph +/vms/saevm/cchain @StephenButtolph From b28d132b788e44221f865a8cf5a65fe95181a3c4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 13:50:39 -0400 Subject: [PATCH 042/120] sae: Smarter tx fuzzing --- vms/saevm/cchain/tx/compatibility_test.go | 44 +++ vms/saevm/cchain/tx/tx_test.go | 174 ++++----- vms/saevm/cchain/tx/txtest/fuzzer.go | 407 ++++++++++++++++++++++ vms/saevm/cchain/tx/txtest/fuzzer_test.go | 33 ++ 4 files changed, 560 insertions(+), 98 deletions(-) create mode 100644 vms/saevm/cchain/tx/compatibility_test.go create mode 100644 vms/saevm/cchain/tx/txtest/fuzzer.go create mode 100644 vms/saevm/cchain/tx/txtest/fuzzer_test.go diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go new file mode 100644 index 000000000000..b47b3557d7c8 --- /dev/null +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -0,0 +1,44 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/vms/saevm/cchain/txtest" + + . "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" +) + +// newFuzzF wraps f and seeds it with [NewTxs]. +func newFuzzF(f *testing.F) *txtest.F { + fuzzF := &txtest.F{ + F: f, + } + for _, tx := range NewTxs { + fuzzF.Add(tx) + } + return fuzzF +} + +func FuzzJSONCompatibility(f *testing.F) { + newFuzzF(f).Fuzz(func(t *testing.T, newTx *Tx) { + bytes, err := newTx.Bytes() + require.NoErrorf(t, err, "%T.Bytes()", newTx) + + oldTx, err := ParseOldTx(bytes) + require.NoError(t, err, "ParseOldTx()") + + oldJSON, err := json.Marshal(oldTx) + require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) + + newJSON, err := json.Marshal(newTx) + require.NoErrorf(t, err, "json.Marshal(%T)", newTx) + assert.JSONEq(t, string(oldJSON), string(newJSON)) + }) +} diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 2e530c03e862..cbc64ea6de2e 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - // Imported for [parseOldTx] comment resolution. + // Imported for [ParseOldTx] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/vm" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" @@ -24,20 +24,20 @@ import ( "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) -// tests is defined at the package level to allow sharing between fuzz tests and +// Tests is defined at the package level to allow sharing between fuzz tests and // unit tests. var ( - tests = [...]struct { - name string - old *atomic.Tx - new *Tx - json string - id ids.ID - bytes []byte + Tests = [...]struct { + Name string + Old *atomic.Tx + New *Tx + JSON string + ID ids.ID + Bytes []byte }{ { - name: "import", // Included in https://subnets.avax.network/c-chain/block/4 - old: &atomic.Tx{ + Name: "import", // Included in https://subnets.avax.network/c-chain/block/4 + Old: &atomic.Tx{ UnsignedAtomicTx: &atomic.UnsignedImportTx{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -71,7 +71,7 @@ var ( }, }, }, - new: &Tx{ + New: &Tx{ Unsigned: &Import{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -105,7 +105,7 @@ var ( }, }, }, - json: `{ + JSON: `{ "unsignedTx":{ "networkID":1, "blockchainID":"2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", @@ -132,12 +132,12 @@ var ( ] }] }`, - id: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), - bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001c52b712aa7dce27a650bf509f799673e245edd4fa9e4e1700eb6105202fe579a0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000002faf080000000010000000000000001b8b5a87d1c05676f1f966da49151fa54dbe68c330000000002faf08021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000100000009000000013e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01"), + ID: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), + Bytes: common.FromHex("0x000000000000000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001c52b712aa7dce27a650bf509f799673e245edd4fa9e4e1700eb6105202fe579a0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000002faf080000000010000000000000001b8b5a87d1c05676f1f966da49151fa54dbe68c330000000002faf08021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000100000009000000013e6614876ee01d3b8b27480c00bdcb0ae84ee3e8346d2d5f08320f7dd3e76c4540be021fe85e91817654c9310b54e8f2e88d81db52b8693842b90f3dbd23bd5c01"), }, { - name: "export", // Included in https://subnets.avax.network/c-chain/block/48 - old: &atomic.Tx{ + Name: "export", // Included in https://subnets.avax.network/c-chain/block/48 + Old: &atomic.Tx{ UnsignedAtomicTx: &atomic.UnsignedExportTx{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -170,7 +170,7 @@ var ( }, }, }, - new: &Tx{ + New: &Tx{ Unsigned: &Export{ NetworkID: 1, BlockchainID: ids.FromStringOrPanic("2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"), @@ -203,7 +203,7 @@ var ( }, }, }, - json: `{ + JSON: `{ "unsignedTx":{ "networkID":1, "blockchainID":"2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", @@ -231,59 +231,59 @@ var ( ] }] }`, - id: ids.FromStringOrPanic("ng7Dox1r8nctrF6zurhRPYWxkmE2juUhT7Qhpauyo8qSEu6jB"), - bytes: common.FromHex("0x000000000001000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001eb019ccd325ad53543a7e7e3b04828bdecf3cff600000000000f424121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000000000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500000000010000000900000001254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01"), + ID: ids.FromStringOrPanic("ng7Dox1r8nctrF6zurhRPYWxkmE2juUhT7Qhpauyo8qSEu6jB"), + Bytes: common.FromHex("0x000000000001000000010427d4b22a2a78bcddd456742caf91b56badbff985ee19aef14573e7343fd652ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b00000001eb019ccd325ad53543a7e7e3b04828bdecf3cff600000000000f424121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000000000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000000000100000000000000000000000100000001d6ce17826dd7c12a7577af257e82d99143b72500000000010000000900000001254d11f1adbd5dfb556855d02ac236ea2dd45d1463459b73714f55ab8d34a4b74a1f18c2868b886e83a5463c422ea3ccc7e9783d5620b1f5695646b0cb1e4dfa01"), }, } - oldTxs []*atomic.Tx - newTxs []*Tx + OldTxs []*atomic.Tx + NewTxs []*Tx ) func init() { - oldTxs = make([]*atomic.Tx, len(tests)) - newTxs = make([]*Tx, len(tests)) - for i, test := range tests { - oldTxs[i] = test.old - newTxs[i] = test.new + OldTxs = make([]*atomic.Tx, len(Tests)) + NewTxs = make([]*Tx, len(Tests)) + for i, test := range Tests { + OldTxs[i] = test.Old + NewTxs[i] = test.New } } func TestID(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { // We must parse the old tx to properly initialize the ID. - old, err := parseOldTx(test.bytes) - require.NoError(t, err, "parseOldTx()") - assert.Equalf(t, test.id, old.ID(), "%T.ID()", old) + old, err := ParseOldTx(test.Bytes) + require.NoError(t, err, "ParseOldTx()") + assert.Equalf(t, test.ID, old.ID(), "%T.ID()", old) }) t.Run("new", func(t *testing.T) { - assert.Equalf(t, test.id, test.new.ID(), "%T.ID()", test.new) + assert.Equalf(t, test.ID, test.New.ID(), "%T.ID()", test.New) }) }) } } func TestBytes(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { - got, err := atomic.Codec.Marshal(atomic.CodecVersion, test.old) - require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, test.old) - assert.Equalf(t, test.bytes, got, "%T.Marshal(, %T)", atomic.Codec, test.old) + got, err := atomic.Codec.Marshal(atomic.CodecVersion, test.Old) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, test.Old) + assert.Equalf(t, test.Bytes, got, "%T.Marshal(, %T)", atomic.Codec, test.Old) }) t.Run("new", func(t *testing.T) { - got, err := test.new.Bytes() - require.NoErrorf(t, err, "%T.Bytes()", test.new) - assert.Equalf(t, test.bytes, got, "%T.Bytes()", test.new) + got, err := test.New.Bytes() + require.NoErrorf(t, err, "%T.Bytes()", test.New) + assert.Equalf(t, test.Bytes, got, "%T.Bytes()", test.New) }) }) } } func TestMarshalSlice(t *testing.T) { - want, err := atomic.Codec.Marshal(atomic.CodecVersion, oldTxs) - require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldTxs) + want, err := atomic.Codec.Marshal(atomic.CodecVersion, OldTxs) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, OldTxs) tests := []struct { name string @@ -292,7 +292,7 @@ func TestMarshalSlice(t *testing.T) { }{ { name: "mainnet", - txs: newTxs, + txs: NewTxs, want: want, }, { @@ -309,26 +309,26 @@ func TestMarshalSlice(t *testing.T) { } func TestParse(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { got := new(atomic.Tx) - _, err := atomic.Codec.Unmarshal(test.bytes, got) + _, err := atomic.Codec.Unmarshal(test.Bytes, got) require.NoErrorf(t, err, "%T.Unmarshal(, %T)", atomic.Codec, got) - assert.Equalf(t, test.old, got, "%T.Unmarshal(, %T)", atomic.Codec, got) + assert.Equalf(t, test.Old, got, "%T.Unmarshal(, %T)", atomic.Codec, got) }) t.Run("new", func(t *testing.T) { - got, err := Parse(test.bytes) + got, err := Parse(test.Bytes) require.NoError(t, err, "Parse()") - assert.Equal(t, test.new, got, "Parse()") + assert.Equal(t, test.New, got, "Parse()") }) }) } } func TestParseSlice(t *testing.T) { - bytes, err := atomic.Codec.Marshal(atomic.CodecVersion, oldTxs) - require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldTxs) + bytes, err := atomic.Codec.Marshal(atomic.CodecVersion, OldTxs) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, OldTxs) tests := []struct { name string @@ -339,7 +339,7 @@ func TestParseSlice(t *testing.T) { { name: "mainnet", bytes: bytes, - want: newTxs, + want: NewTxs, }, { name: "empty", @@ -366,10 +366,10 @@ func TestParseSlice(t *testing.T) { var errUnexpectedCredentialType = errors.New("unexpected credential type") -// parseOldTx parses a transaction using coreth's old parsing logic but enforces +// ParseOldTx parses a transaction using coreth's old parsing logic but enforces // additional restrictions. Coreth's parsing logic is overly permissive and // depends on later verification in [vm.VerifierBackend]. -func parseOldTx(b []byte) (*atomic.Tx, error) { +func ParseOldTx(b []byte) (*atomic.Tx, error) { tx, err := atomic.ExtractAtomicTx(b, atomic.Codec) if err != nil { return nil, err @@ -382,10 +382,10 @@ func parseOldTx(b []byte) (*atomic.Tx, error) { return tx, nil } -// parseOldTxs parses a slice of transactions using coreth's old parsing logic +// ParseOldTxs parses a slice of transactions using coreth's old parsing logic // but enforces additional restrictions. Coreth's parsing logic is overly // permissive and depends on later verification in [vm.VerifierBackend]. -func parseOldTxs(b []byte) ([]*atomic.Tx, error) { +func ParseOldTxs(b []byte) ([]*atomic.Tx, error) { txs, err := atomic.ExtractAtomicTxs(b, true, atomic.Codec) if err != nil { return nil, err @@ -401,41 +401,41 @@ func parseOldTxs(b []byte) ([]*atomic.Tx, error) { } func FuzzParseCompatibility(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) + for _, test := range Tests { + f.Add(test.Bytes) } f.Fuzz(func(t *testing.T, data []byte) { - _, oldErr := parseOldTx(data) + _, oldErr := ParseOldTx(data) oldOk := oldErr == nil _, newErr := Parse(data) newOk := newErr == nil - assert.Equal(t, oldOk, newOk, "Parse(b) == parseOldTx(b)") + assert.Equal(t, oldOk, newOk, "Parse(b) == ParseOldTx(b)") }) } func FuzzParseSliceCompatibility(f *testing.F) { { - b, err := MarshalSlice(newTxs) + b, err := MarshalSlice(NewTxs) require.NoError(f, err, "MarshalSlice()") f.Add(b) } f.Fuzz(func(t *testing.T, data []byte) { - _, oldErr := parseOldTxs(data) + _, oldErr := ParseOldTxs(data) oldOk := oldErr == nil _, newErr := ParseSlice(data) newOk := newErr == nil - assert.Equal(t, oldOk, newOk, "ParseSlice(b) == parseOldTxs(b)") + assert.Equal(t, oldOk, newOk, "ParseSlice(b) == ParseOldTxs(b)") }) } func FuzzParseRoundTrip(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) + for _, test := range Tests { + f.Add(test.Bytes) } f.Fuzz(func(t *testing.T, data []byte) { tx, err := Parse(data) @@ -451,7 +451,7 @@ func FuzzParseRoundTrip(f *testing.F) { func FuzzParseSliceRoundTrip(f *testing.F) { { - b, err := MarshalSlice(newTxs) + b, err := MarshalSlice(NewTxs) require.NoError(f, err, "MarshalSlice()") f.Add(b) } @@ -471,40 +471,18 @@ func FuzzParseSliceRoundTrip(f *testing.F) { } func TestJSONMarshal(t *testing.T) { - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for _, test := range Tests { + t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { - got, err := json.Marshal(test.old) - require.NoErrorf(t, err, "json.Marshal(%T)", test.old) - assert.JSONEqf(t, test.json, string(got), "json.Marshal(%T)", test.old) + got, err := json.Marshal(test.Old) + require.NoErrorf(t, err, "json.Marshal(%T)", test.Old) + assert.JSONEqf(t, test.JSON, string(got), "json.Marshal(%T)", test.Old) }) t.Run("new", func(t *testing.T) { - got, err := json.Marshal(test.new) - require.NoErrorf(t, err, "json.Marshal(%T)", test.new) - assert.JSONEqf(t, test.json, string(got), "json.Marshal(%T)", test.new) + got, err := json.Marshal(test.New) + require.NoErrorf(t, err, "json.Marshal(%T)", test.New) + assert.JSONEqf(t, test.JSON, string(got), "json.Marshal(%T)", test.New) }) }) } } - -func FuzzJSONCompatibility(f *testing.F) { - for _, test := range tests { - f.Add(test.bytes) - } - f.Fuzz(func(t *testing.T, data []byte) { - oldTx, err := parseOldTx(data) - if err != nil { - t.Skip("invalid tx") - } - - newTx, err := Parse(data) - require.NoError(t, err, "Parse()") - - oldJSON, err := json.Marshal(oldTx) - require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) - - newJSON, err := json.Marshal(newTx) - require.NoErrorf(t, err, "json.Marshal(%T)", newTx) - assert.JSONEq(t, string(oldJSON), string(newJSON)) - }) -} diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go new file mode 100644 index 000000000000..ffb658d7bf47 --- /dev/null +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -0,0 +1,407 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package txtest provides test helpers for using [tx.Tx]. +package txtest + +import ( + "encoding/binary" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +// F works like [testing.F], but allows for the usage of [tx.Tx]. +// +// This type should be used over [testing.F] when it is desired to consistently +// produce a parseable transaction. +type F struct { + *testing.F + + // Addresses, if non-empty, biases addresses toward this alphabet. + Addresses []common.Address + // AssetIDs, if non-empty, biases assetIDs toward this alphabet. + AssetIDs []ids.ID +} + +// Add works like [testing.F.Add], but expects a [tx.Tx]. +func (f *F) Add(tx *tx.Tx) { + var e encoder + e.unsigned(tx.Unsigned) + sliceTo(&e, tx.Creds, (*encoder).credential) + f.F.Add([]byte(e)) +} + +// Fuzz works like [testing.F.Fuzz], but provides a [tx.Tx]. +func (f *F) Fuzz(test func(t *testing.T, tx *tx.Tx)) { + f.F.Fuzz(func(t *testing.T, data []byte) { + fuzz := decoder{ + Data: data, + Addresses: f.Addresses, + AssetIDs: f.AssetIDs, + } + genTx := fuzz.Tx() + + // genTx isn't always ideally formatted, so we round-trip through + // parsing before providing it to the test body. + bytes, err := genTx.Bytes() + if err != nil { + t.Skipf("invalid tx: %s", err) + } + tx, err := tx.Parse(bytes) + require.NoError(t, err, "Parse()") + test(t, tx) + }) +} + +// decoder turns a byte stream into structured data. +// +// The byte stream is consumed as structured data is produced; once exhausted, +// methods return the zero value of their result type. +type decoder struct { + // Data is the byte stream backing the decoder. + Data []byte + // Addresses, if non-empty, biases [decoder.Address] toward this alphabet. + // When empty, [decoder.Address] always returns a fully random address + // consumed from data. + Addresses []common.Address + // AssetIDs, if non-empty, biases [decoder.AssetID] toward this alphabet. + // When empty, [decoder.AssetID] always returns a fully random ID consumed + // from data. + AssetIDs []ids.ID +} + +// Bytes returns a slice of n random bytes. +// +// Once the fuzzer is exhausted, the returned bytes are zeroes. +func (d *decoder) Bytes(n int) []byte { + out := make([]byte, n) + copy(out, d.Data) + if n >= len(d.Data) { + d.Data = nil + } else { + d.Data = d.Data[n:] + } + return out +} + +// Bool returns a random bool. +// +// Once the fuzzer is exhausted, false is returned. +func (d *decoder) Bool() bool { return d.Bytes(1)[0]&1 != 0 } + +// Uint32 returns a random uint32. +// +// Once the fuzzer is exhausted, 0 is returned. +func (d *decoder) Uint32() uint32 { return binary.BigEndian.Uint32(d.Bytes(4)) } + +// Uint64 returns a random uint64. +// +// Once the fuzzer is exhausted, 0 is returned. +func (d *decoder) Uint64() uint64 { return binary.BigEndian.Uint64(d.Bytes(8)) } + +// ID returns a random [ids.ID]. +// +// Once the fuzzer is exhausted, [ids.Empty] is returned. +func (d *decoder) ID() ids.ID { return ids.ID(d.Bytes(ids.IDLen)) } + +// ShortID returns a random [ids.ShortID]. +// +// Once the fuzzer is exhausted, [ids.ShortEmpty] is returned. +func (d *decoder) ShortID() ids.ShortID { return ids.ShortID(d.Bytes(ids.ShortIDLen)) } + +// Signature returns a random 65-byte array. +// +// Once the fuzzer is exhausted, the zero value is returned. +func (d *decoder) Signature() [65]byte { return [65]byte(d.Bytes(65)) } + +// Intn returns a value in [0, x). +// +// Once the fuzzer is exhausted, 0 is returned. +func (d *decoder) Intn(x int) int { + if x < 1 { + return 0 + } + return int(d.Uint64() % uint64(x)) +} + +// element returns a random element in s. It panics if s is empty. +// +// Once the fuzzer is exhausted, the first entry in s is returned. +func element[T any](f *decoder, s []T) T { + return s[f.Intn(len(s))] +} + +// sliceOf generates a random slice of generated entries. The length is random, +// but is typically small. +// +// Once the fuzzer is exhausted, an empty slice is returned. +func sliceOf[T any](f *decoder, gen func(*decoder) T) []T { + var out []T + // Bool defaults to false once the generation has been exausted, so this + // will eventually terminate. + for f.Bool() { + out = append(out, gen(f)) + } + return out +} + +// Address returns a random [common.Address]. When [fuzzer.Addresses] is +// non-empty, the result is biased toward that alphabet to encourage +// repeated-address code paths. +// +// Once the fuzzer is exhausted, the zero address is returned. +func (d *decoder) Address() common.Address { + if !d.Bool() || len(d.Addresses) == 0 { + return common.Address(d.Bytes(common.AddressLength)) + } + return element(d, d.Addresses) +} + +// AssetID returns a random [ids.ID]. When [fuzzer.AssetIDs] is non-empty, +// the result is biased toward that alphabet to encourage repeated-asset code +// paths. +// +// Once the fuzzer is exhausted, [ids.Empty] is returned. +func (d *decoder) AssetID() ids.ID { + if !d.Bool() || len(d.AssetIDs) == 0 { + return d.ID() + } + return element(d, d.AssetIDs) +} + +// TransferableInput returns a random [avax.TransferableInput]. +// +// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no +// signature indices is returned. +func (d *decoder) TransferableInput() *avax.TransferableInput { + return &avax.TransferableInput{ + UTXOID: avax.UTXOID{ + TxID: d.ID(), + OutputIndex: d.Uint32(), + }, + Asset: avax.Asset{ID: d.AssetID()}, + In: &secp256k1fx.TransferInput{ + Amt: d.Uint64(), + Input: secp256k1fx.Input{ + SigIndices: sliceOf(d, (*decoder).Uint32), + }, + }, + } +} + +// TransferableOutput returns a random [avax.TransferableOutput]. +// +// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no +// owner addresses is returned. +func (d *decoder) TransferableOutput() *avax.TransferableOutput { + return &avax.TransferableOutput{ + Asset: avax.Asset{ID: d.AssetID()}, + Out: &secp256k1fx.TransferOutput{ + Amt: d.Uint64(), + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: d.Uint64(), + Threshold: d.Uint32(), + Addrs: sliceOf(d, (*decoder).ShortID), + }, + }, + } +} + +// Input returns a random [tx.Input]. +// +// Once the fuzzer is exhausted, the zero value is returned. +func (d *decoder) Input() tx.Input { + return tx.Input{ + Address: d.Address(), + Amount: d.Uint64(), + AssetID: d.AssetID(), + Nonce: d.Uint64(), + } +} + +// Output returns a random [tx.Output]. +// +// Once the fuzzer is exhausted, the zero value is returned. +func (d *decoder) Output() tx.Output { + return tx.Output{ + Address: d.Address(), + Amount: d.Uint64(), + AssetID: d.AssetID(), + } +} + +// ImportTx returns a random [*tx.Import]. +// +// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no +// imported inputs or outputs is returned. +func (d *decoder) ImportTx() *tx.Import { + return &tx.Import{ + NetworkID: d.Uint32(), + BlockchainID: d.ID(), + SourceChain: d.ID(), + ImportedInputs: sliceOf(d, (*decoder).TransferableInput), + Outs: sliceOf(d, (*decoder).Output), + } +} + +// ExportTx returns a random [*tx.Export]. +// +// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no +// inputs or exported outputs is returned. +func (d *decoder) ExportTx() *tx.Export { + return &tx.Export{ + NetworkID: d.Uint32(), + BlockchainID: d.ID(), + DestinationChain: d.ID(), + Ins: sliceOf(d, (*decoder).Input), + ExportedOutputs: sliceOf(d, (*decoder).TransferableOutput), + } +} + +// Unsigned returns a random [tx.Unsigned] — either a [*tx.Import] or a +// [*tx.Export]. +// +// Once the fuzzer is exhausted, an empty [*tx.Export] is returned. +func (d *decoder) Unsigned() tx.Unsigned { + if d.Bool() { + return d.ImportTx() + } else { + return d.ExportTx() + } +} + +// Credential returns a random [tx.Credential]. +// +// Once the fuzzer is exhausted, a [*secp256k1fx.Credential] with no +// signatures is returned. +func (d *decoder) Credential() tx.Credential { + return &secp256k1fx.Credential{ + Sigs: sliceOf(d, (*decoder).Signature), + } +} + +// Tx returns a random [*tx.Tx]. +// +// Once the fuzzer is exhausted, a non-nil pointer to a tx wrapping an empty +// [*tx.Export] with no credentials is returned. +func (d *decoder) Tx() *tx.Tx { + return &tx.Tx{ + Unsigned: d.Unsigned(), + Creds: sliceOf(d, (*decoder).Credential), + } +} + +// encoder writes a byte stream that [fuzzer] will decode into the same +// structure. +type encoder []byte + +func (e *encoder) bytes(b []byte) { *e = append(*e, b...) } +func (e *encoder) uint32(v uint32) { *e = binary.BigEndian.AppendUint32(*e, v) } +func (e *encoder) uint64(v uint64) { *e = binary.BigEndian.AppendUint64(*e, v) } +func (e *encoder) id(v ids.ID) { e.bytes(v[:]) } +func (e *encoder) shortID(v ids.ShortID) { e.bytes(v[:]) } +func (e *encoder) signature(v [65]byte) { e.bytes(v[:]) } + +func (e *encoder) bool(b bool) { + if b { + *e = append(*e, 1) + } else { + *e = append(*e, 0) + } +} + +// address always picks the raw-bytes branch in [fuzzer.Address] so the encoded +// value is independent of the alphabet. +func (e *encoder) address(v common.Address) { + e.bool(false) + e.bytes(v[:]) +} + +// assetID always picks the raw-bytes branch in [fuzzer.AssetID] so the encoded +// value is independent of the alphabet. +func (e *encoder) assetID(v ids.ID) { + e.bool(false) + e.id(v) +} + +func sliceTo[T any](e *encoder, items []T, gen func(*encoder, T)) { + for _, item := range items { + e.bool(true) + gen(e, item) + } + e.bool(false) +} + +func (e *encoder) transferableInput(in *avax.TransferableInput) { + e.id(in.UTXOID.TxID) + e.uint32(in.UTXOID.OutputIndex) + e.assetID(in.Asset.ID) + ti := in.In.(*secp256k1fx.TransferInput) + e.uint64(ti.Amt) + sliceTo(e, ti.Input.SigIndices, (*encoder).uint32) +} + +func (e *encoder) transferableOutput(out *avax.TransferableOutput) { + e.assetID(out.Asset.ID) + to := out.Out.(*secp256k1fx.TransferOutput) + e.uint64(to.Amt) + e.uint64(to.OutputOwners.Locktime) + e.uint32(to.OutputOwners.Threshold) + sliceTo(e, to.OutputOwners.Addrs, (*encoder).shortID) +} + +func (e *encoder) input(i tx.Input) { + e.address(i.Address) + e.uint64(i.Amount) + e.assetID(i.AssetID) + e.uint64(i.Nonce) +} + +func (e *encoder) output(o tx.Output) { + e.address(o.Address) + e.uint64(o.Amount) + e.assetID(o.AssetID) +} + +func (e *encoder) importTx(t *tx.Import) { + e.uint32(t.NetworkID) + e.id(t.BlockchainID) + e.id(t.SourceChain) + sliceTo(e, t.ImportedInputs, (*encoder).transferableInput) + sliceTo(e, t.Outs, (*encoder).output) +} + +func (e *encoder) exportTx(t *tx.Export) { + e.uint32(t.NetworkID) + e.id(t.BlockchainID) + e.id(t.DestinationChain) + sliceTo(e, t.Ins, (*encoder).input) + sliceTo(e, t.ExportedOutputs, (*encoder).transferableOutput) +} + +func (e *encoder) unsigned(u tx.Unsigned) { + switch u := u.(type) { + case *tx.Import: + e.bool(true) + e.importTx(u) + case *tx.Export: + e.bool(false) + e.exportTx(u) + } +} + +func (e *encoder) credential(c tx.Credential) { + sliceTo(e, c.Self().Sigs, (*encoder).signature) +} + +func (e *encoder) tx(tx *tx.Tx) { + e.unsigned(tx.Unsigned) + sliceTo(e, tx.Creds, (*encoder).credential) +} diff --git a/vms/saevm/cchain/tx/txtest/fuzzer_test.go b/vms/saevm/cchain/tx/txtest/fuzzer_test.go new file mode 100644 index 000000000000..4b75fc360266 --- /dev/null +++ b/vms/saevm/cchain/tx/txtest/fuzzer_test.go @@ -0,0 +1,33 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txtest + +import ( + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" +) + +func FuzzRoundTrip(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + f := decoder{ + Data: data, + Addresses: []common.Address{ + {1}, + }, + AssetIDs: []ids.ID{ + {2}, + }, + } + want := f.Tx() + + var e encoder + e.tx(want) + f.Data = e + got := f.Tx() + require.Equal(t, want, got, "decode(encode(tx)) == tx") + }) +} From f12a1b4860c3560463fc7f0af229e1fdc416067e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 13:59:29 -0400 Subject: [PATCH 043/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index b47b3557d7c8..f951b2851046 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -15,19 +15,19 @@ import ( . "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" ) -// newFuzzF wraps f and seeds it with [NewTxs]. -func newFuzzF(f *testing.F) *txtest.F { - fuzzF := &txtest.F{ +// fuzz seeds f with [NewTxs] and repeatedly runs the test. +func fuzz(f *testing.F, test func(t *testing.T, newTx *Tx)) { + fuzzer := &txtest.F{ F: f, } for _, tx := range NewTxs { - fuzzF.Add(tx) + fuzzer.Add(tx) } - return fuzzF + fuzzer.Fuzz(test) } func FuzzJSONCompatibility(f *testing.F) { - newFuzzF(f).Fuzz(func(t *testing.T, newTx *Tx) { + fuzz(f, func(t *testing.T, newTx *Tx) { bytes, err := newTx.Bytes() require.NoErrorf(t, err, "%T.Bytes()", newTx) From 258df6f6e99394befd5585de8b9d7657ca9d635c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 14:55:26 -0400 Subject: [PATCH 044/120] nit --- vms/saevm/cchain/tx/txtest/fuzzer.go | 252 ++++++++-------------- vms/saevm/cchain/tx/txtest/fuzzer_test.go | 12 +- 2 files changed, 91 insertions(+), 173 deletions(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index ffb658d7bf47..1df6ff356b7e 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -33,20 +33,19 @@ type F struct { // Add works like [testing.F.Add], but expects a [tx.Tx]. func (f *F) Add(tx *tx.Tx) { var e encoder - e.unsigned(tx.Unsigned) - sliceTo(&e, tx.Creds, (*encoder).credential) + e.tx(tx) f.F.Add([]byte(e)) } // Fuzz works like [testing.F.Fuzz], but provides a [tx.Tx]. func (f *F) Fuzz(test func(t *testing.T, tx *tx.Tx)) { f.F.Fuzz(func(t *testing.T, data []byte) { - fuzz := decoder{ - Data: data, - Addresses: f.Addresses, - AssetIDs: f.AssetIDs, + d := decoder{ + data: data, + addresses: f.Addresses, + assetIDs: f.AssetIDs, } - genTx := fuzz.Tx() + genTx := d.tx() // genTx isn't always ideally formatted, so we round-trip through // parsing before providing it to the test body. @@ -65,240 +64,159 @@ func (f *F) Fuzz(test func(t *testing.T, tx *tx.Tx)) { // The byte stream is consumed as structured data is produced; once exhausted, // methods return the zero value of their result type. type decoder struct { - // Data is the byte stream backing the decoder. - Data []byte - // Addresses, if non-empty, biases [decoder.Address] toward this alphabet. - // When empty, [decoder.Address] always returns a fully random address - // consumed from data. - Addresses []common.Address - // AssetIDs, if non-empty, biases [decoder.AssetID] toward this alphabet. - // When empty, [decoder.AssetID] always returns a fully random ID consumed - // from data. - AssetIDs []ids.ID + // data is the byte stream backing the decoder. + data []byte + // addresses, if non-empty, biases [decoder.address] toward this alphabet. + addresses []common.Address + // assetIDs, if non-empty, biases [decoder.assetID] toward this alphabet. + assetIDs []ids.ID } -// Bytes returns a slice of n random bytes. -// -// Once the fuzzer is exhausted, the returned bytes are zeroes. -func (d *decoder) Bytes(n int) []byte { +func (d *decoder) bytes(n int) []byte { out := make([]byte, n) - copy(out, d.Data) - if n >= len(d.Data) { - d.Data = nil + copy(out, d.data) + if n >= len(d.data) { + d.data = nil } else { - d.Data = d.Data[n:] + d.data = d.data[n:] } return out } -// Bool returns a random bool. -// -// Once the fuzzer is exhausted, false is returned. -func (d *decoder) Bool() bool { return d.Bytes(1)[0]&1 != 0 } - -// Uint32 returns a random uint32. -// -// Once the fuzzer is exhausted, 0 is returned. -func (d *decoder) Uint32() uint32 { return binary.BigEndian.Uint32(d.Bytes(4)) } - -// Uint64 returns a random uint64. -// -// Once the fuzzer is exhausted, 0 is returned. -func (d *decoder) Uint64() uint64 { return binary.BigEndian.Uint64(d.Bytes(8)) } - -// ID returns a random [ids.ID]. -// -// Once the fuzzer is exhausted, [ids.Empty] is returned. -func (d *decoder) ID() ids.ID { return ids.ID(d.Bytes(ids.IDLen)) } - -// ShortID returns a random [ids.ShortID]. -// -// Once the fuzzer is exhausted, [ids.ShortEmpty] is returned. -func (d *decoder) ShortID() ids.ShortID { return ids.ShortID(d.Bytes(ids.ShortIDLen)) } - -// Signature returns a random 65-byte array. -// -// Once the fuzzer is exhausted, the zero value is returned. -func (d *decoder) Signature() [65]byte { return [65]byte(d.Bytes(65)) } +func (d *decoder) bool() bool { return d.bytes(1)[0]&1 != 0 } +func (d *decoder) uint32() uint32 { return binary.BigEndian.Uint32(d.bytes(4)) } +func (d *decoder) uint64() uint64 { return binary.BigEndian.Uint64(d.bytes(8)) } +func (d *decoder) id() ids.ID { return ids.ID(d.bytes(ids.IDLen)) } +func (d *decoder) shortID() ids.ShortID { return ids.ShortID(d.bytes(ids.ShortIDLen)) } +func (d *decoder) signature() [65]byte { return [65]byte(d.bytes(65)) } -// Intn returns a value in [0, x). -// -// Once the fuzzer is exhausted, 0 is returned. -func (d *decoder) Intn(x int) int { +// intn returns a value in [0, x). +func (d *decoder) intn(x int) int { if x < 1 { return 0 } - return int(d.Uint64() % uint64(x)) + return int(d.uint64() % uint64(x)) } // element returns a random element in s. It panics if s is empty. -// -// Once the fuzzer is exhausted, the first entry in s is returned. -func element[T any](f *decoder, s []T) T { - return s[f.Intn(len(s))] +func element[T any](d *decoder, s []T) T { + return s[d.intn(len(s))] } // sliceOf generates a random slice of generated entries. The length is random, // but is typically small. -// -// Once the fuzzer is exhausted, an empty slice is returned. -func sliceOf[T any](f *decoder, gen func(*decoder) T) []T { +func sliceOf[T any](d *decoder, gen func(*decoder) T) []T { var out []T - // Bool defaults to false once the generation has been exausted, so this + // [decoder.bool] returns false once the data is exhausted, so this loop // will eventually terminate. - for f.Bool() { - out = append(out, gen(f)) + for d.bool() { + out = append(out, gen(d)) } return out } -// Address returns a random [common.Address]. When [fuzzer.Addresses] is -// non-empty, the result is biased toward that alphabet to encourage -// repeated-address code paths. -// -// Once the fuzzer is exhausted, the zero address is returned. -func (d *decoder) Address() common.Address { - if !d.Bool() || len(d.Addresses) == 0 { - return common.Address(d.Bytes(common.AddressLength)) +func (d *decoder) address() common.Address { + if !d.bool() || len(d.addresses) == 0 { + return common.Address(d.bytes(common.AddressLength)) } - return element(d, d.Addresses) + return element(d, d.addresses) } -// AssetID returns a random [ids.ID]. When [fuzzer.AssetIDs] is non-empty, -// the result is biased toward that alphabet to encourage repeated-asset code -// paths. -// -// Once the fuzzer is exhausted, [ids.Empty] is returned. -func (d *decoder) AssetID() ids.ID { - if !d.Bool() || len(d.AssetIDs) == 0 { - return d.ID() +func (d *decoder) assetID() ids.ID { + if !d.bool() || len(d.assetIDs) == 0 { + return d.id() } - return element(d, d.AssetIDs) + return element(d, d.assetIDs) } -// TransferableInput returns a random [avax.TransferableInput]. -// -// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no -// signature indices is returned. -func (d *decoder) TransferableInput() *avax.TransferableInput { +func (d *decoder) transferableInput() *avax.TransferableInput { return &avax.TransferableInput{ UTXOID: avax.UTXOID{ - TxID: d.ID(), - OutputIndex: d.Uint32(), + TxID: d.id(), + OutputIndex: d.uint32(), }, - Asset: avax.Asset{ID: d.AssetID()}, + Asset: avax.Asset{ID: d.assetID()}, In: &secp256k1fx.TransferInput{ - Amt: d.Uint64(), + Amt: d.uint64(), Input: secp256k1fx.Input{ - SigIndices: sliceOf(d, (*decoder).Uint32), + SigIndices: sliceOf(d, (*decoder).uint32), }, }, } } -// TransferableOutput returns a random [avax.TransferableOutput]. -// -// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no -// owner addresses is returned. -func (d *decoder) TransferableOutput() *avax.TransferableOutput { +func (d *decoder) transferableOutput() *avax.TransferableOutput { return &avax.TransferableOutput{ - Asset: avax.Asset{ID: d.AssetID()}, + Asset: avax.Asset{ID: d.assetID()}, Out: &secp256k1fx.TransferOutput{ - Amt: d.Uint64(), + Amt: d.uint64(), OutputOwners: secp256k1fx.OutputOwners{ - Locktime: d.Uint64(), - Threshold: d.Uint32(), - Addrs: sliceOf(d, (*decoder).ShortID), + Locktime: d.uint64(), + Threshold: d.uint32(), + Addrs: sliceOf(d, (*decoder).shortID), }, }, } } -// Input returns a random [tx.Input]. -// -// Once the fuzzer is exhausted, the zero value is returned. -func (d *decoder) Input() tx.Input { +func (d *decoder) input() tx.Input { return tx.Input{ - Address: d.Address(), - Amount: d.Uint64(), - AssetID: d.AssetID(), - Nonce: d.Uint64(), + Address: d.address(), + Amount: d.uint64(), + AssetID: d.assetID(), + Nonce: d.uint64(), } } -// Output returns a random [tx.Output]. -// -// Once the fuzzer is exhausted, the zero value is returned. -func (d *decoder) Output() tx.Output { +func (d *decoder) output() tx.Output { return tx.Output{ - Address: d.Address(), - Amount: d.Uint64(), - AssetID: d.AssetID(), + Address: d.address(), + Amount: d.uint64(), + AssetID: d.assetID(), } } -// ImportTx returns a random [*tx.Import]. -// -// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no -// imported inputs or outputs is returned. -func (d *decoder) ImportTx() *tx.Import { +func (d *decoder) importTx() *tx.Import { return &tx.Import{ - NetworkID: d.Uint32(), - BlockchainID: d.ID(), - SourceChain: d.ID(), - ImportedInputs: sliceOf(d, (*decoder).TransferableInput), - Outs: sliceOf(d, (*decoder).Output), + NetworkID: d.uint32(), + BlockchainID: d.id(), + SourceChain: d.id(), + ImportedInputs: sliceOf(d, (*decoder).transferableInput), + Outs: sliceOf(d, (*decoder).output), } } -// ExportTx returns a random [*tx.Export]. -// -// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no -// inputs or exported outputs is returned. -func (d *decoder) ExportTx() *tx.Export { +func (d *decoder) exportTx() *tx.Export { return &tx.Export{ - NetworkID: d.Uint32(), - BlockchainID: d.ID(), - DestinationChain: d.ID(), - Ins: sliceOf(d, (*decoder).Input), - ExportedOutputs: sliceOf(d, (*decoder).TransferableOutput), + NetworkID: d.uint32(), + BlockchainID: d.id(), + DestinationChain: d.id(), + Ins: sliceOf(d, (*decoder).input), + ExportedOutputs: sliceOf(d, (*decoder).transferableOutput), } } -// Unsigned returns a random [tx.Unsigned] — either a [*tx.Import] or a -// [*tx.Export]. -// -// Once the fuzzer is exhausted, an empty [*tx.Export] is returned. -func (d *decoder) Unsigned() tx.Unsigned { - if d.Bool() { - return d.ImportTx() - } else { - return d.ExportTx() +func (d *decoder) unsigned() tx.Unsigned { + if d.bool() { + return d.importTx() } + return d.exportTx() } -// Credential returns a random [tx.Credential]. -// -// Once the fuzzer is exhausted, a [*secp256k1fx.Credential] with no -// signatures is returned. -func (d *decoder) Credential() tx.Credential { +func (d *decoder) credential() tx.Credential { return &secp256k1fx.Credential{ - Sigs: sliceOf(d, (*decoder).Signature), + Sigs: sliceOf(d, (*decoder).signature), } } -// Tx returns a random [*tx.Tx]. -// -// Once the fuzzer is exhausted, a non-nil pointer to a tx wrapping an empty -// [*tx.Export] with no credentials is returned. -func (d *decoder) Tx() *tx.Tx { +func (d *decoder) tx() *tx.Tx { return &tx.Tx{ - Unsigned: d.Unsigned(), - Creds: sliceOf(d, (*decoder).Credential), + Unsigned: d.unsigned(), + Creds: sliceOf(d, (*decoder).credential), } } -// encoder writes a byte stream that [fuzzer] will decode into the same +// encoder writes a byte stream that [decoder] will decode into the same // structure. type encoder []byte @@ -317,14 +235,14 @@ func (e *encoder) bool(b bool) { } } -// address always picks the raw-bytes branch in [fuzzer.Address] so the encoded +// address always picks the raw-bytes branch in [decoder.address] so the encoded // value is independent of the alphabet. func (e *encoder) address(v common.Address) { e.bool(false) e.bytes(v[:]) } -// assetID always picks the raw-bytes branch in [fuzzer.AssetID] so the encoded +// assetID always picks the raw-bytes branch in [decoder.assetID] so the encoded // value is independent of the alphabet. func (e *encoder) assetID(v ids.ID) { e.bool(false) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer_test.go b/vms/saevm/cchain/tx/txtest/fuzzer_test.go index 4b75fc360266..9e2bbb91f6e3 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer_test.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer_test.go @@ -14,20 +14,20 @@ import ( func FuzzRoundTrip(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { f := decoder{ - Data: data, - Addresses: []common.Address{ + data: data, + addresses: []common.Address{ {1}, }, - AssetIDs: []ids.ID{ + assetIDs: []ids.ID{ {2}, }, } - want := f.Tx() + want := f.tx() var e encoder e.tx(want) - f.Data = e - got := f.Tx() + f.data = e + got := f.tx() require.Equal(t, want, got, "decode(encode(tx)) == tx") }) } From 8cefe95c7750c1d38e24a9cad29c229256162a7b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 14:56:19 -0400 Subject: [PATCH 045/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index f951b2851046..2011e3ae32ee 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -15,7 +15,7 @@ import ( . "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" ) -// fuzz seeds f with [NewTxs] and repeatedly runs the test. +// fuzz seeds f with [NewTxs] and fuzzes the test. func fuzz(f *testing.F, test func(t *testing.T, newTx *Tx)) { fuzzer := &txtest.F{ F: f, From 95e72560d32d5576eb5ce286a057caff9c251c0f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 15:02:32 -0400 Subject: [PATCH 046/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 7 +------ vms/saevm/cchain/tx/tx_test.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 2011e3ae32ee..ddb6c24bc131 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -28,12 +28,7 @@ func fuzz(f *testing.F, test func(t *testing.T, newTx *Tx)) { func FuzzJSONCompatibility(f *testing.F) { fuzz(f, func(t *testing.T, newTx *Tx) { - bytes, err := newTx.Bytes() - require.NoErrorf(t, err, "%T.Bytes()", newTx) - - oldTx, err := ParseOldTx(bytes) - require.NoError(t, err, "ParseOldTx()") - + oldTx := ToOldTx(t, newTx) oldJSON, err := json.Marshal(oldTx) require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index cbc64ea6de2e..d8f28880240b 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -364,6 +364,18 @@ func TestParseSlice(t *testing.T) { } } +// ToOldTx converts a transaction from the new format into coreth's old format. +func ToOldTx(tb testing.TB, newTx *Tx) *atomic.Tx { + tb.Helper() + + bytes, err := newTx.Bytes() + require.NoErrorf(tb, err, "%T.Bytes()", newTx) + + oldTx, err := ParseOldTx(bytes) + require.NoError(tb, err, "ParseOldTx()") + return oldTx +} + var errUnexpectedCredentialType = errors.New("unexpected credential type") // ParseOldTx parses a transaction using coreth's old parsing logic but enforces From 5d2ef23ab666f71e7a99af5ea5aeb436606ce7d9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 15:03:17 -0400 Subject: [PATCH 047/120] want got --- vms/saevm/cchain/tx/compatibility_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index ddb6c24bc131..c62513c35248 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -29,11 +29,11 @@ func fuzz(f *testing.F, test func(t *testing.T, newTx *Tx)) { func FuzzJSONCompatibility(f *testing.F) { fuzz(f, func(t *testing.T, newTx *Tx) { oldTx := ToOldTx(t, newTx) - oldJSON, err := json.Marshal(oldTx) + want, err := json.Marshal(oldTx) require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) - newJSON, err := json.Marshal(newTx) + got, err := json.Marshal(newTx) require.NoErrorf(t, err, "json.Marshal(%T)", newTx) - assert.JSONEq(t, string(oldJSON), string(newJSON)) + assert.JSONEq(t, string(want), string(got)) }) } From 46814f415c61d705b51324746cadf255f0ba9c8a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 15:03:58 -0400 Subject: [PATCH 048/120] want got --- vms/saevm/cchain/tx/tx_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 2e530c03e862..7a4a05d59ad0 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -500,11 +500,11 @@ func FuzzJSONCompatibility(f *testing.F) { newTx, err := Parse(data) require.NoError(t, err, "Parse()") - oldJSON, err := json.Marshal(oldTx) + want, err := json.Marshal(oldTx) require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) - newJSON, err := json.Marshal(newTx) + got, err := json.Marshal(newTx) require.NoErrorf(t, err, "json.Marshal(%T)", newTx) - assert.JSONEq(t, string(oldJSON), string(newJSON)) + assert.JSONEq(t, string(want), string(got)) }) } From 271658d49a7144339ca0635f36955ddde93776b3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 15:06:53 -0400 Subject: [PATCH 049/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 4 ++-- vms/saevm/cchain/tx/txtest/fuzzer.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index c62513c35248..43bd6bc8d1a5 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -16,14 +16,14 @@ import ( ) // fuzz seeds f with [NewTxs] and fuzzes the test. -func fuzz(f *testing.F, test func(t *testing.T, newTx *Tx)) { +func fuzz(f *testing.F, ff func(t *testing.T, tx *Tx)) { fuzzer := &txtest.F{ F: f, } for _, tx := range NewTxs { fuzzer.Add(tx) } - fuzzer.Fuzz(test) + fuzzer.Fuzz(ff) } func FuzzJSONCompatibility(f *testing.F) { diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index 1df6ff356b7e..c26f5e97cd10 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -38,7 +38,7 @@ func (f *F) Add(tx *tx.Tx) { } // Fuzz works like [testing.F.Fuzz], but provides a [tx.Tx]. -func (f *F) Fuzz(test func(t *testing.T, tx *tx.Tx)) { +func (f *F) Fuzz(ff func(t *testing.T, tx *tx.Tx)) { f.F.Fuzz(func(t *testing.T, data []byte) { d := decoder{ data: data, @@ -55,7 +55,7 @@ func (f *F) Fuzz(test func(t *testing.T, tx *tx.Tx)) { } tx, err := tx.Parse(bytes) require.NoError(t, err, "Parse()") - test(t, tx) + ff(t, tx) }) } From 9da6af83f55f62ece1718c2668553276709846eb Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 15:07:54 -0400 Subject: [PATCH 050/120] nit --- vms/saevm/cchain/tx/txtest/fuzzer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index c26f5e97cd10..7c0bf8ff9dba 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -72,6 +72,7 @@ type decoder struct { assetIDs []ids.ID } +// bytes always returns a slice of length n, even if the decoder is exhausted. func (d *decoder) bytes(n int) []byte { out := make([]byte, n) copy(out, d.data) From 7bcb962b6945bd7b05301b6527fb0df4222e46ed Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 15:53:49 -0400 Subject: [PATCH 051/120] ci --- vms/saevm/cchain/tx/BUILD.bazel | 6 ++++- vms/saevm/cchain/tx/compatibility_test.go | 2 +- vms/saevm/cchain/tx/txtest/BUILD.bazel | 28 +++++++++++++++++++++++ vms/saevm/cchain/tx/txtest/fuzzer_test.go | 3 ++- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 vms/saevm/cchain/tx/txtest/BUILD.bazel diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index 91e98c137633..9081ffd5a57d 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -25,7 +25,10 @@ go_library( go_test( name = "tx_test", - srcs = ["tx_test.go"], + srcs = [ + "compatibility_test.go", + "tx_test.go", + ], data = glob(["testdata/**"]), embed = [":tx"], deps = [ @@ -34,6 +37,7 @@ go_test( "//ids", "//vms/components/avax", "//vms/components/verify", + "//vms/saevm/cchain/tx/txtest", "//vms/secp256k1fx", "@com_github_ava_labs_libevm//common", "@com_github_google_go_cmp//cmp", diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 43bd6bc8d1a5..a7b792c6ff49 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ava-labs/avalanchego/vms/saevm/cchain/txtest" + "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx/txtest" . "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" ) diff --git a/vms/saevm/cchain/tx/txtest/BUILD.bazel b/vms/saevm/cchain/tx/txtest/BUILD.bazel new file mode 100644 index 000000000000..3a9f394157b8 --- /dev/null +++ b/vms/saevm/cchain/tx/txtest/BUILD.bazel @@ -0,0 +1,28 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//.bazel:defs.bzl", "go_test") + +go_library( + name = "txtest", + srcs = ["fuzzer.go"], + importpath = "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx/txtest", + visibility = ["//visibility:public"], + deps = [ + "//ids", + "//vms/components/avax", + "//vms/saevm/cchain/tx", + "//vms/secp256k1fx", + "@com_github_ava_labs_libevm//common", + "@com_github_stretchr_testify//require", + ], +) + +go_test( + name = "txtest_test", + srcs = ["fuzzer_test.go"], + embed = [":txtest"], + deps = [ + "//ids", + "@com_github_ava_labs_libevm//common", + "@com_github_stretchr_testify//require", + ], +) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer_test.go b/vms/saevm/cchain/tx/txtest/fuzzer_test.go index 9e2bbb91f6e3..cd1b6194ef43 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer_test.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer_test.go @@ -6,9 +6,10 @@ package txtest import ( "testing" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/libevm/common" "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" ) func FuzzRoundTrip(f *testing.F) { From 34b1457a245ad76b7eedfc94b58fd302b65efcc3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 16:03:11 -0400 Subject: [PATCH 052/120] nosec --- vms/saevm/cchain/tx/txtest/fuzzer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index 7c0bf8ff9dba..88e927c5817f 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -96,7 +96,7 @@ func (d *decoder) intn(x int) int { if x < 1 { return 0 } - return int(d.uint64() % uint64(x)) + return int(d.uint64() % uint64(x)) //#nosec G115 -- Overflow is impossible. } // element returns a random element in s. It panics if s is empty. From eedd62eb744bfafbd9b56afeb2ae50904e15ab31 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 16:16:07 -0400 Subject: [PATCH 053/120] nit --- vms/saevm/cchain/tx/txtest/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/vms/saevm/cchain/tx/txtest/BUILD.bazel b/vms/saevm/cchain/tx/txtest/BUILD.bazel index 3a9f394157b8..9805355e8e45 100644 --- a/vms/saevm/cchain/tx/txtest/BUILD.bazel +++ b/vms/saevm/cchain/tx/txtest/BUILD.bazel @@ -3,6 +3,7 @@ load("//.bazel:defs.bzl", "go_test") go_library( name = "txtest", + testonly = True, srcs = ["fuzzer.go"], importpath = "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx/txtest", visibility = ["//visibility:public"], From 9e3bb9808ae54987c0f424aac7314d5df0a96bcf Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 16:18:52 -0400 Subject: [PATCH 054/120] ci --- vms/saevm/cchain/tx/BUILD.bazel | 2 +- vms/saevm/cchain/tx/compatibility_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index 3be97217e71a..c1fc6a5cb585 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -46,8 +46,8 @@ go_test( "//vms/components/avax", "//vms/components/gas", "//vms/components/verify", - "//vms/saevm/hook", "//vms/saevm/cchain/tx/txtest", + "//vms/saevm/hook", "//vms/secp256k1fx", "@com_github_ava_labs_libevm//common", "@com_github_google_go_cmp//cmp", diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 5d7de0a7e6a1..594702c15f36 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -8,6 +8,7 @@ import ( "math/big" "testing" + "github.com/ava-labs/libevm/common" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/holiman/uint256" @@ -19,7 +20,6 @@ import ( "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx/txtest" "github.com/ava-labs/avalanchego/vms/saevm/hook" - "github.com/ava-labs/libevm/common" . "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" ) From a4bad9b93acbca377b36c6de4b247199ee1b960b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 16:58:18 -0400 Subject: [PATCH 055/120] remove duplicate code --- vms/saevm/cchain/tx/txtest/fuzzer.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index 88e927c5817f..aa784260e026 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -99,8 +99,13 @@ func (d *decoder) intn(x int) int { return int(d.uint64() % uint64(x)) //#nosec G115 -- Overflow is impossible. } -// element returns a random element in s. It panics if s is empty. -func element[T any](d *decoder, s []T) T { +// element returns a random element biased towards values in s. +func element[T any](d *decoder, s []T, gen func(*decoder) T) T { + // [decoder.bool] must be read before checking whether or not s is empty so + // that encoding can assume that a bool will be read. + if !d.bool() || len(s) == 0 { + return gen(d) + } return s[d.intn(len(s))] } @@ -117,17 +122,11 @@ func sliceOf[T any](d *decoder, gen func(*decoder) T) []T { } func (d *decoder) address() common.Address { - if !d.bool() || len(d.addresses) == 0 { - return common.Address(d.bytes(common.AddressLength)) - } - return element(d, d.addresses) + return element(d, d.addresses, (*decoder).address) } func (d *decoder) assetID() ids.ID { - if !d.bool() || len(d.assetIDs) == 0 { - return d.id() - } - return element(d, d.assetIDs) + return element(d, d.assetIDs, (*decoder).id) } func (d *decoder) transferableInput() *avax.TransferableInput { From 56850ce148aef6bca5153141d23c88ed9346f8ec Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 16:59:00 -0400 Subject: [PATCH 056/120] nit --- vms/saevm/cchain/tx/txtest/fuzzer.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index aa784260e026..812f2d7f84f9 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -235,15 +235,15 @@ func (e *encoder) bool(b bool) { } } -// address always picks the raw-bytes branch in [decoder.address] so the encoded -// value is independent of the alphabet. +// address always picks the raw-bytes branch in [element] so the encoded value +// is independent of the alphabet. func (e *encoder) address(v common.Address) { e.bool(false) e.bytes(v[:]) } -// assetID always picks the raw-bytes branch in [decoder.assetID] so the encoded -// value is independent of the alphabet. +// assetID always picks the raw-bytes branch in [element] so the encoded value +// is independent of the alphabet. func (e *encoder) assetID(v ids.ID) { e.bool(false) e.id(v) From c74091e549cab8ba9e6cf72e26689607c40c3dc9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:01:14 -0400 Subject: [PATCH 057/120] nit --- vms/saevm/cchain/tx/txtest/fuzzer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index 812f2d7f84f9..c47754105b00 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -239,7 +239,7 @@ func (e *encoder) bool(b bool) { // is independent of the alphabet. func (e *encoder) address(v common.Address) { e.bool(false) - e.bytes(v[:]) + e.address(v) } // assetID always picks the raw-bytes branch in [element] so the encoded value From cefd09f78f326d5f5a1965a3fbb044c0e35ece35 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:01:47 -0400 Subject: [PATCH 058/120] nit --- vms/saevm/cchain/tx/txtest/fuzzer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index 812f2d7f84f9..c47754105b00 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -239,7 +239,7 @@ func (e *encoder) bool(b bool) { // is independent of the alphabet. func (e *encoder) address(v common.Address) { e.bool(false) - e.bytes(v[:]) + e.address(v) } // assetID always picks the raw-bytes branch in [element] so the encoded value From 163274ff3065773f97291ac99255097931d876d0 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:08:38 -0400 Subject: [PATCH 059/120] nit --- vms/saevm/cchain/txtest/fuzzer.go | 410 ------------------------------ 1 file changed, 410 deletions(-) delete mode 100644 vms/saevm/cchain/txtest/fuzzer.go diff --git a/vms/saevm/cchain/txtest/fuzzer.go b/vms/saevm/cchain/txtest/fuzzer.go deleted file mode 100644 index 748dcecb67a8..000000000000 --- a/vms/saevm/cchain/txtest/fuzzer.go +++ /dev/null @@ -1,410 +0,0 @@ -// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package txtest provides test helpers for using [tx.Tx]. -package txtest - -import ( - "encoding/binary" - "testing" - - "github.com/ava-labs/libevm/common" - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/vms/components/avax" - "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" - "github.com/ava-labs/avalanchego/vms/secp256k1fx" -) - -// F works like [testing.F], but allows for the usage of [tx.Tx]. -// -// This type should be used over [testing.F] when it is desired to consistently -// produce a parseable transaction. -// -// [F.Addresses] and [F.AssetIDs] are propagated to the [Fuzzer] used for each -// fuzz iteration. -type F struct { - *testing.F - - // Addresses, if non-empty, biases [Fuzzer.Address] toward this alphabet. - Addresses []common.Address - // AssetIDs, if non-empty, biases [Fuzzer.AssetID] toward this alphabet. - AssetIDs []ids.ID -} - -// Add works like [testing.F.Add], but expects a [tx.Tx]. -func (f *F) Add(tx *tx.Tx) { - var e encoder - e.unsigned(tx.Unsigned) - encodeSlice(&e, tx.Creds, (*encoder).credential) - f.F.Add([]byte(e)) -} - -// Fuzz works like [testing.F.Fuzz], but provides a [tx.Tx]. -func (f *F) Fuzz(test func(t *testing.T, tx *tx.Tx)) { - f.F.Fuzz(func(t *testing.T, data []byte) { - fuzz := Fuzzer{ - Data: data, - Addresses: f.Addresses, - AssetIDs: f.AssetIDs, - } - genTx := fuzz.Tx() - - // genTx isn't always ideally formatted, so we round-trip through - // parsing before providing it to the test body. - bytes, err := genTx.Bytes() - if err != nil { - t.Skipf("invalid tx: %s", err) - } - tx, err := tx.Parse(bytes) - require.NoError(t, err, "Parse()") - test(t, tx) - }) -} - -// Fuzzer turns a byte stream into structured data. -// -// The byte stream in [Fuzzer.Data] is consumed left-to-right; once exhausted, -// methods return the zero value of their result type. -type Fuzzer struct { - // Data is the byte stream backing the fuzzer. - Data []byte - // Addresses, if non-empty, biases [Fuzzer.Address] toward this alphabet. - // When empty, [Fuzzer.Address] always returns a fully random address - // consumed from [Fuzzer.Data]. - Addresses []common.Address - // AssetIDs, if non-empty, biases [Fuzzer.AssetID] toward this alphabet. - // When empty, [Fuzzer.AssetID] always returns a fully random ID consumed - // from [Fuzzer.Data]. - AssetIDs []ids.ID -} - -// Bytes returns a slice of n random bytes. -// -// Once the fuzzer is exhausted, the returned bytes are zeroes. -func (f *Fuzzer) Bytes(n int) []byte { - out := make([]byte, n) - copy(out, f.Data) - if n >= len(f.Data) { - f.Data = nil - } else { - f.Data = f.Data[n:] - } - return out -} - -// Bool returns a random bool. -// -// Once the fuzzer is exhausted, false is returned. -func (f *Fuzzer) Bool() bool { return f.Bytes(1)[0]&1 != 0 } - -// Uint32 returns a random uint32. -// -// Once the fuzzer is exhausted, 0 is returned. -func (f *Fuzzer) Uint32() uint32 { return binary.BigEndian.Uint32(f.Bytes(4)) } - -// Uint64 returns a random uint64. -// -// Once the fuzzer is exhausted, 0 is returned. -func (f *Fuzzer) Uint64() uint64 { return binary.BigEndian.Uint64(f.Bytes(8)) } - -// ID returns a random [ids.ID]. -// -// Once the fuzzer is exhausted, [ids.Empty] is returned. -func (f *Fuzzer) ID() ids.ID { return ids.ID(f.Bytes(ids.IDLen)) } - -// ShortID returns a random [ids.ShortID]. -// -// Once the fuzzer is exhausted, [ids.ShortEmpty] is returned. -func (f *Fuzzer) ShortID() ids.ShortID { return ids.ShortID(f.Bytes(ids.ShortIDLen)) } - -// Signature returns a random 65-byte array. -// -// Once the fuzzer is exhausted, the zero value is returned. -func (f *Fuzzer) Signature() [65]byte { return [65]byte(f.Bytes(65)) } - -// Intn returns a value in [0, x). -// -// Once the fuzzer is exhausted, 0 is returned. -func (f *Fuzzer) Intn(x int) int { - if x < 1 { - return 0 - } - return int(f.Uint64() % uint64(x)) -} - -// Element returns a random Element in s. It panics if s is empty. -// -// Once the fuzzer is exhausted, the first entry in s is returned. -func Element[T any](f *Fuzzer, s []T) T { - return s[f.Intn(len(s))] -} - -// SliceOf generates a random slice of generated entries. The length is random, -// but is typically small. -// -// Once the fuzzer is exhausted, an empty slice is returned. -func SliceOf[T any](f *Fuzzer, gen func(*Fuzzer) T) []T { - var out []T - // Bool defaults to false once the generation has been exausted, so this - // will eventually terminate. - for f.Bool() { - out = append(out, gen(f)) - } - return out -} - -// Address returns a random [common.Address]. When [Fuzzer.Addresses] is -// non-empty, the result is biased toward that alphabet to encourage -// repeated-address code paths. -// -// Once the fuzzer is exhausted, the zero address is returned. -func (f *Fuzzer) Address() common.Address { - if !f.Bool() || len(f.Addresses) == 0 { - return common.Address(f.Bytes(common.AddressLength)) - } - return Element(f, f.Addresses) -} - -// AssetID returns a random [ids.ID]. When [Fuzzer.AssetIDs] is non-empty, -// the result is biased toward that alphabet to encourage repeated-asset code -// paths. -// -// Once the fuzzer is exhausted, [ids.Empty] is returned. -func (f *Fuzzer) AssetID() ids.ID { - if !f.Bool() || len(f.AssetIDs) == 0 { - return f.ID() - } - return Element(f, f.AssetIDs) -} - -// TransferableInput returns a random [*avax.TransferableInput]. -// -// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no -// signature indices is returned. -func (f *Fuzzer) TransferableInput() *avax.TransferableInput { - return &avax.TransferableInput{ - UTXOID: avax.UTXOID{ - TxID: f.ID(), - OutputIndex: f.Uint32(), - }, - Asset: avax.Asset{ID: f.AssetID()}, - In: &secp256k1fx.TransferInput{ - Amt: f.Uint64(), - Input: secp256k1fx.Input{ - SigIndices: SliceOf(f, (*Fuzzer).Uint32), - }, - }, - } -} - -// TransferableOutput returns a random [*avax.TransferableOutput]. -// -// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no -// owner addresses is returned. -func (f *Fuzzer) TransferableOutput() *avax.TransferableOutput { - return &avax.TransferableOutput{ - Asset: avax.Asset{ID: f.AssetID()}, - Out: &secp256k1fx.TransferOutput{ - Amt: f.Uint64(), - OutputOwners: secp256k1fx.OutputOwners{ - Locktime: f.Uint64(), - Threshold: f.Uint32(), - Addrs: SliceOf(f, (*Fuzzer).ShortID), - }, - }, - } -} - -// Input returns a random [tx.Input]. -// -// Once the fuzzer is exhausted, the zero value is returned. -func (f *Fuzzer) Input() tx.Input { - return tx.Input{ - Address: f.Address(), - Amount: f.Uint64(), - AssetID: f.AssetID(), - Nonce: f.Uint64(), - } -} - -// Output returns a random [tx.Output]. -// -// Once the fuzzer is exhausted, the zero value is returned. -func (f *Fuzzer) Output() tx.Output { - return tx.Output{ - Address: f.Address(), - Amount: f.Uint64(), - AssetID: f.AssetID(), - } -} - -// ImportTx returns a random [*tx.Import]. -// -// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no -// imported inputs or outputs is returned. -func (f *Fuzzer) ImportTx() *tx.Import { - return &tx.Import{ - NetworkID: f.Uint32(), - BlockchainID: f.ID(), - SourceChain: f.ID(), - ImportedInputs: SliceOf(f, (*Fuzzer).TransferableInput), - Outs: SliceOf(f, (*Fuzzer).Output), - } -} - -// ExportTx returns a random [*tx.Export]. -// -// Once the fuzzer is exhausted, a non-nil pointer to the zero value with no -// inputs or exported outputs is returned. -func (f *Fuzzer) ExportTx() *tx.Export { - return &tx.Export{ - NetworkID: f.Uint32(), - BlockchainID: f.ID(), - DestinationChain: f.ID(), - Ins: SliceOf(f, (*Fuzzer).Input), - ExportedOutputs: SliceOf(f, (*Fuzzer).TransferableOutput), - } -} - -// Unsigned returns a random [tx.Unsigned] — either a [*tx.Import] or a -// [*tx.Export]. -// -// Once the fuzzer is exhausted, an empty [*tx.Export] is returned. -func (f *Fuzzer) Unsigned() tx.Unsigned { - if f.Bool() { - return f.ImportTx() - } else { - return f.ExportTx() - } -} - -// Credential returns a random [tx.Credential]. -// -// Once the fuzzer is exhausted, a [*secp256k1fx.Credential] with no -// signatures is returned. -func (f *Fuzzer) Credential() tx.Credential { - return &secp256k1fx.Credential{ - Sigs: SliceOf(f, (*Fuzzer).Signature), - } -} - -// Tx returns a random [*tx.Tx]. -// -// Once the fuzzer is exhausted, a non-nil pointer to a tx wrapping an empty -// [*tx.Export] with no credentials is returned. -func (f *Fuzzer) Tx() *tx.Tx { - return &tx.Tx{ - Unsigned: f.Unsigned(), - Creds: SliceOf(f, (*Fuzzer).Credential), - } -} - -// encoder writes a byte stream that [Fuzzer] will decode into the same -// structure. -type encoder []byte - -func (e *encoder) bytes(b []byte) { *e = append(*e, b...) } -func (e *encoder) uint32(v uint32) { *e = binary.BigEndian.AppendUint32(*e, v) } -func (e *encoder) uint64(v uint64) { *e = binary.BigEndian.AppendUint64(*e, v) } -func (e *encoder) id(v ids.ID) { e.bytes(v[:]) } -func (e *encoder) shortID(v ids.ShortID) { e.bytes(v[:]) } -func (e *encoder) signature(v [65]byte) { e.bytes(v[:]) } - -func (e *encoder) bool(b bool) { - if b { - *e = append(*e, 1) - } else { - *e = append(*e, 0) - } -} - -// address always picks the raw-bytes branch in [Fuzzer.Address] so the encoded -// value is independent of the alphabet. -func (e *encoder) address(v common.Address) { - e.bool(false) - e.bytes(v[:]) -} - -// assetID always picks the raw-bytes branch in [Fuzzer.AssetID] so the encoded -// value is independent of the alphabet. -func (e *encoder) assetID(v ids.ID) { - e.bool(false) - e.id(v) -} - -func encodeSlice[T any](e *encoder, items []T, gen func(*encoder, T)) { - for _, item := range items { - e.bool(true) - gen(e, item) - } - e.bool(false) -} - -func (e *encoder) transferableInput(in *avax.TransferableInput) { - e.id(in.UTXOID.TxID) - e.uint32(in.UTXOID.OutputIndex) - e.assetID(in.Asset.ID) - ti := in.In.(*secp256k1fx.TransferInput) - e.uint64(ti.Amt) - encodeSlice(e, ti.Input.SigIndices, (*encoder).uint32) -} - -func (e *encoder) transferableOutput(out *avax.TransferableOutput) { - e.assetID(out.Asset.ID) - to := out.Out.(*secp256k1fx.TransferOutput) - e.uint64(to.Amt) - e.uint64(to.OutputOwners.Locktime) - e.uint32(to.OutputOwners.Threshold) - encodeSlice(e, to.OutputOwners.Addrs, (*encoder).shortID) -} - -func (e *encoder) input(i tx.Input) { - e.address(i.Address) - e.uint64(i.Amount) - e.assetID(i.AssetID) - e.uint64(i.Nonce) -} - -func (e *encoder) output(o tx.Output) { - e.address(o.Address) - e.uint64(o.Amount) - e.assetID(o.AssetID) -} - -func (e *encoder) importTx(t *tx.Import) { - e.uint32(t.NetworkID) - e.id(t.BlockchainID) - e.id(t.SourceChain) - encodeSlice(e, t.ImportedInputs, (*encoder).transferableInput) - encodeSlice(e, t.Outs, (*encoder).output) -} - -func (e *encoder) exportTx(t *tx.Export) { - e.uint32(t.NetworkID) - e.id(t.BlockchainID) - e.id(t.DestinationChain) - encodeSlice(e, t.Ins, (*encoder).input) - encodeSlice(e, t.ExportedOutputs, (*encoder).transferableOutput) -} - -func (e *encoder) unsigned(u tx.Unsigned) { - switch u := u.(type) { - case *tx.Import: - e.bool(true) - e.importTx(u) - case *tx.Export: - e.bool(false) - e.exportTx(u) - } -} - -func (e *encoder) credential(c tx.Credential) { - encodeSlice(e, c.Self().Sigs, (*encoder).signature) -} - -func (e *encoder) tx(tx *tx.Tx) { - e.unsigned(tx.Unsigned) - encodeSlice(e, tx.Creds, (*encoder).credential) -} From 05ddba9bcf4faed1187d0d249f4aaf068a7a31b0 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:09:36 -0400 Subject: [PATCH 060/120] oops --- vms/saevm/cchain/tx/txtest/fuzzer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index c47754105b00..812f2d7f84f9 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -239,7 +239,7 @@ func (e *encoder) bool(b bool) { // is independent of the alphabet. func (e *encoder) address(v common.Address) { e.bool(false) - e.address(v) + e.bytes(v[:]) } // assetID always picks the raw-bytes branch in [element] so the encoded value From 9b0449cd09b768ec6b751990356697b6b9666fe9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:16:40 -0400 Subject: [PATCH 061/120] why --- vms/saevm/cchain/tx/compatibility_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index f63c37169902..7e5b2a7dd845 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -195,7 +195,8 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { require.NoError(t, newTx.TransferNonAVAX(AVAXAssetID, newSDB)) require.NoError(t, op.ApplyTo(newSDB.StateDB)) - // Finalize the trie structures for comparison. + // We must manually finalize the trie structures before comparison. + // Otherwise, comparing the state DBs would trivially pass. for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { sdb.Finalise(true) sdb.IntermediateRoot(true) From 7e35e325d9395bf7b9b868eaedfe6ea4e5ff62b8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:18:29 -0400 Subject: [PATCH 062/120] ci --- vms/saevm/cchain/tx/BUILD.bazel | 7 +++++++ vms/saevm/cchain/tx/tx_test.go | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index f145a3271bc6..9e98bb71ebbe 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -15,6 +15,7 @@ go_library( "//chains/atomic", "//codec", "//codec/linearcodec", + "//graft/coreth/core/extstate", "//graft/coreth/plugin/evm/atomic", "//graft/coreth/plugin/evm/upgrade/ap5", "//ids", @@ -40,8 +41,10 @@ go_test( embed = [":tx"], deps = [ "//chains/atomic", + "//graft/coreth/core/extstate", "//graft/coreth/plugin/evm/atomic", "//graft/coreth/plugin/evm/atomic/vm", + "//graft/coreth/plugin/evm/customtypes", "//ids", "//snow", "//utils/math", @@ -49,9 +52,13 @@ go_test( "//vms/components/gas", "//vms/components/verify", "//vms/saevm/cchain/tx/txtest", + "//vms/saevm/cmputils", "//vms/saevm/hook", "//vms/secp256k1fx", "@com_github_ava_labs_libevm//common", + "@com_github_ava_labs_libevm//core/rawdb", + "@com_github_ava_labs_libevm//core/state", + "@com_github_ava_labs_libevm//core/types", "@com_github_google_go_cmp//cmp", "@com_github_google_go_cmp//cmp/cmpopts", "@com_github_holiman_uint256//:uint256", diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 2a9cf5f6b4db..c4aeeec6d935 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -22,10 +22,10 @@ import ( // Imported for [ParseOldTx] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/vm" - "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" "github.com/ava-labs/avalanchego/graft/coreth/core/extstate" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" From fca7808d6be8ca4761ad90bd3e71ec1155309d03 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:21:16 -0400 Subject: [PATCH 063/120] no moar recursion pls --- vms/saevm/cchain/tx/txtest/fuzzer.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index 812f2d7f84f9..1cfea7c6c42e 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -87,6 +87,7 @@ func (d *decoder) bytes(n int) []byte { func (d *decoder) bool() bool { return d.bytes(1)[0]&1 != 0 } func (d *decoder) uint32() uint32 { return binary.BigEndian.Uint32(d.bytes(4)) } func (d *decoder) uint64() uint64 { return binary.BigEndian.Uint64(d.bytes(8)) } +func (d *decoder) addr() common.Address { return common.Address(d.bytes(common.AddressLength)) } func (d *decoder) id() ids.ID { return ids.ID(d.bytes(ids.IDLen)) } func (d *decoder) shortID() ids.ShortID { return ids.ShortID(d.bytes(ids.ShortIDLen)) } func (d *decoder) signature() [65]byte { return [65]byte(d.bytes(65)) } @@ -122,7 +123,7 @@ func sliceOf[T any](d *decoder, gen func(*decoder) T) []T { } func (d *decoder) address() common.Address { - return element(d, d.addresses, (*decoder).address) + return element(d, d.addresses, (*decoder).addr) } func (d *decoder) assetID() ids.ID { @@ -223,6 +224,7 @@ type encoder []byte func (e *encoder) bytes(b []byte) { *e = append(*e, b...) } func (e *encoder) uint32(v uint32) { *e = binary.BigEndian.AppendUint32(*e, v) } func (e *encoder) uint64(v uint64) { *e = binary.BigEndian.AppendUint64(*e, v) } +func (e *encoder) addr(v common.Address) { e.bytes(v[:]) } func (e *encoder) id(v ids.ID) { e.bytes(v[:]) } func (e *encoder) shortID(v ids.ShortID) { e.bytes(v[:]) } func (e *encoder) signature(v [65]byte) { e.bytes(v[:]) } @@ -239,7 +241,7 @@ func (e *encoder) bool(b bool) { // is independent of the alphabet. func (e *encoder) address(v common.Address) { e.bool(false) - e.bytes(v[:]) + e.addr(v) } // assetID always picks the raw-bytes branch in [element] so the encoded value From 0de4a41e361efb4420fc7aa1c051b67434e2c5ca Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:49:03 -0400 Subject: [PATCH 064/120] nit --- vms/saevm/cchain/tx/txtest/fuzzer_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer_test.go b/vms/saevm/cchain/tx/txtest/fuzzer_test.go index cd1b6194ef43..7415c2e461b1 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer_test.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer_test.go @@ -14,7 +14,7 @@ import ( func FuzzRoundTrip(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { - f := decoder{ + d := decoder{ data: data, addresses: []common.Address{ {1}, @@ -23,12 +23,12 @@ func FuzzRoundTrip(f *testing.F) { {2}, }, } - want := f.tx() + want := d.tx() var e encoder e.tx(want) - f.data = e - got := f.tx() + d.data = e + got := d.tx() require.Equal(t, want, got, "decode(encode(tx)) == tx") }) } From 87d29d20c5a3cf3c3a75c6abcc5af9e8e47519bd Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:52:25 -0400 Subject: [PATCH 065/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 594702c15f36..258f293c954c 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -101,17 +101,17 @@ func newAsOpStateDB() *asOpStateDB { } } -func (f *asOpStateDB) AddBalance(addr common.Address, amount *uint256.Int) { - b := f.op.Mint[addr] +func (s *asOpStateDB) AddBalance(addr common.Address, amount *uint256.Int) { + b := s.op.Mint[addr] b.Add(&b, amount) - f.op.Mint[addr] = b + s.op.Mint[addr] = b } -func (f *asOpStateDB) SubBalance(addr common.Address, amount *uint256.Int) { - d := f.op.Burn[addr] +func (s *asOpStateDB) SubBalance(addr common.Address, amount *uint256.Int) { + d := s.op.Burn[addr] d.Amount.Add(&d.Amount, amount) d.MinBalance = d.Amount - f.op.Burn[addr] = d + s.op.Burn[addr] = d } func (*asOpStateDB) GetBalance(common.Address) *uint256.Int { @@ -128,12 +128,12 @@ func (*asOpStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { return new(big.Int).Lsh(big.NewInt(1), 128) } -func (f *asOpStateDB) SetNonce(addr common.Address, nonce uint64) { - d := f.op.Burn[addr] +func (s *asOpStateDB) SetNonce(addr common.Address, nonce uint64) { + d := s.op.Burn[addr] d.Nonce = nonce - 1 - f.op.Burn[addr] = d + s.op.Burn[addr] = d } -func (f *asOpStateDB) GetNonce(addr common.Address) uint64 { - return f.initialNonces[addr] +func (s *asOpStateDB) GetNonce(addr common.Address) uint64 { + return s.initialNonces[addr] } From 8535be8a55c93922f1a602003066c4a95768ccbd Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:54:47 -0400 Subject: [PATCH 066/120] nit --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index f4c127f88759..4d56732ff71d 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -181,7 +181,7 @@ func gasPrice(cost uint64, gas gas.Gas) uint256.Int { return p } -// AtomicRequests returns chainID and modifications into shared memory that this +// AtomicRequests returns chainID and shared-memory modifications that this // transaction should perform during execution. func (t *Tx) AtomicRequests() (ids.ID, *atomic.Requests, error) { return t.Unsigned.atomicRequests(t.ID()) From 61c3b9091dccc0cebf4e4380fab87c5045073020 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 29 Apr 2026 17:56:43 -0400 Subject: [PATCH 067/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 17 +++++++++-------- vms/saevm/cchain/tx/tx.go | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 7e5b2a7dd845..a0a92c406f64 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -172,14 +172,15 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { oldSDB := NewStateDB(t) newSDB := NewStateDB(t) - hugeAVAX := new(uint256.Int).Lsh(uint256.NewInt(1), 128) - hugeBig := new(big.Int).Lsh(big.NewInt(1), 128) - for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { - if tx, ok := newTx.Unsigned.(*Export); ok { - for _, in := range tx.Ins { - if in.Nonce == math.MaxUint64 { - t.Skip("nonce overflow") - } + if tx, ok := newTx.Unsigned.(*Export); ok { + hugeAVAX := new(uint256.Int).Lsh(uint256.NewInt(1), 128) + hugeBig := new(big.Int).Lsh(big.NewInt(1), 128) + for _, in := range tx.Ins { + if in.Nonce == math.MaxUint64 { + t.Skip("nonce overflow") + } + + for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { sdb.AddBalance(in.Address, hugeAVAX) sdb.SetNonce(in.Address, in.Nonce) sdb.AddBalanceMultiCoin(in.Address, common.Hash(in.AssetID), hugeBig) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 3da2efe68139..12afa1ff985e 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -36,8 +36,8 @@ type Tx struct { // Unsigned is a common interface implemented by [Import] and [Export]. // -// TODO(StephenButtolph): Expand this interface to include UTXO handling, -// verification, and state execution. +// TODO(StephenButtolph): Expand this interface to include UTXO handling and +// verification. type Unsigned interface { // TransferNonAVAX transfers the non-AVAX balances requested by this // transaction. From 6e9545cfdce20f61426c81eba5d760738b32887a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 09:13:08 -0400 Subject: [PATCH 068/120] Use the identifier rather than where the identifier is used --- vms/saevm/cchain/tx/tx_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 7a4a05d59ad0..4cc0a3630e3d 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - // Imported for [parseOldTx] comment resolution. + // Imported for [vm.VerifierBackend] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/vm" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" From 47fe30d9d615381087677cc79a0f0c839149043b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 09:23:50 -0400 Subject: [PATCH 069/120] Update visability --- vms/saevm/cchain/BUILD.bazel | 11 +++++++++++ vms/saevm/cchain/tx/BUILD.bazel | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 vms/saevm/cchain/BUILD.bazel diff --git a/vms/saevm/cchain/BUILD.bazel b/vms/saevm/cchain/BUILD.bazel new file mode 100644 index 000000000000..fa721a4fadad --- /dev/null +++ b/vms/saevm/cchain/BUILD.bazel @@ -0,0 +1,11 @@ +# gazelle:default_visibility //vms/saevm/cchain:__subpackages__,//vms/saevm/cchain:external_consumers + +package(default_visibility = [ + "//vms/saevm/cchain:__subpackages__", + "//vms/saevm/cchain:external_consumers", +]) + +package_group( + name = "external_consumers", + packages = [], +) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index 91e98c137633..c7bfca07dc28 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -1,6 +1,11 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") load("//.bazel:defs.bzl", "go_test") +package(default_visibility = [ + "//vms/saevm/cchain:__subpackages__", + "//vms/saevm/cchain:external_consumers", +]) + go_library( name = "tx", srcs = [ @@ -10,7 +15,6 @@ go_library( "tx.go", ], importpath = "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx", - visibility = ["//visibility:public"], deps = [ "//codec", "//codec/linearcodec", From ba1afe1dc7d885db1dfc8b35d02713bd6c1d321a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 09:38:59 -0400 Subject: [PATCH 070/120] nit --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 93b630da97bf..6eb6e647598f 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -12,7 +12,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" - // Imported for [gasPerByte] comment resolution. + // Imported for [atomic.TxBytesGas] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap5" From e9487dc292d91ca7752737bfdef61d9f695c81cc Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 09:40:29 -0400 Subject: [PATCH 071/120] merged --- vms/saevm/cchain/tx/tx.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 87c6c384955b..79b445ab13af 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -15,7 +15,6 @@ import ( // Imported for [atomic.TxBytesGas] comment resolution. _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" - "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap5" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/hashing" @@ -23,6 +22,8 @@ import ( "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + chainsatomic "github.com/ava-labs/avalanchego/chains/atomic" ) // Tx is a signed transaction that interacts with shared memory. @@ -52,7 +53,7 @@ type Unsigned interface { // atomicRequests returns the operations that should be applied to shared // memory when this transaction is executed. - atomicRequests(txID ids.ID) (chainID ids.ID, requests *atomic.Requests, err error) + atomicRequests(txID ids.ID) (chainID ids.ID, requests *chainsatomic.Requests, err error) } type op struct { @@ -183,7 +184,7 @@ func gasPrice(cost uint64, gas gas.Gas) uint256.Int { // AtomicRequests returns chainID and shared-memory modifications that this // transaction should perform during execution. -func (t *Tx) AtomicRequests() (ids.ID, *atomic.Requests, error) { +func (t *Tx) AtomicRequests() (ids.ID, *chainsatomic.Requests, error) { return t.Unsigned.atomicRequests(t.ID()) } From 3e3e6aa686473b7ac917d8d7cc7e73a942d84cbc Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 10:08:21 -0400 Subject: [PATCH 072/120] ci --- vms/saevm/cchain/tx/txtest/BUILD.bazel | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/txtest/BUILD.bazel b/vms/saevm/cchain/tx/txtest/BUILD.bazel index 9805355e8e45..4efef60dfeeb 100644 --- a/vms/saevm/cchain/tx/txtest/BUILD.bazel +++ b/vms/saevm/cchain/tx/txtest/BUILD.bazel @@ -1,12 +1,16 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") load("//.bazel:defs.bzl", "go_test") +package(default_visibility = [ + "//vms/saevm/cchain:__subpackages__", + "//vms/saevm/cchain:external_consumers", +]) + go_library( name = "txtest", testonly = True, srcs = ["fuzzer.go"], importpath = "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx/txtest", - visibility = ["//visibility:public"], deps = [ "//ids", "//vms/components/avax", From 963c29587eadd2665db9667b27b6b13a40f1d9b8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 11:11:57 -0400 Subject: [PATCH 073/120] remove jank, use cmp --- vms/saevm/cchain/tx/compatibility_test.go | 14 +++++++ vms/saevm/cchain/tx/tx_test.go | 48 +++++++++++++++-------- vms/saevm/cchain/tx/txtest/fuzzer.go | 12 ++---- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index a7b792c6ff49..7f82f8f0215d 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -7,6 +7,7 @@ import ( "encoding/json" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,6 +27,19 @@ func fuzz(f *testing.F, ff func(t *testing.T, tx *Tx)) { fuzzer.Fuzz(ff) } +func FuzzParseRoundTrip(f *testing.F) { + fuzz(f, func(t *testing.T, want *Tx) { + bytes, err := want.Bytes() + require.NoErrorf(t, err, "%T.Bytes()", want) + + got, err := Parse(bytes) + require.NoError(t, err, "Parse()") + if diff := cmp.Diff(want, got, CmpOpt()); diff != "" { + t.Errorf("Parse() diff (-want +got):\n%s", diff) + } + }) +} + func FuzzJSONCompatibility(f *testing.F) { fuzz(f, func(t *testing.T, newTx *Tx) { oldTx := ToOldTx(t, newTx) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 5071989aabb2..baaeb2643a0b 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/saevm/cmputils" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) @@ -308,6 +309,30 @@ func TestMarshalSlice(t *testing.T) { } } +// OldCmpOpt returns a configuration for [cmp.Diff] to compare [atomic.Tx] +// instances. +func OldCmpOpt() cmp.Option { + return cmputils.IfIn[atomic.Tx](cmp.Options{ + cmpopts.IgnoreUnexported( + atomic.Metadata{}, + avax.UTXOID{}, + secp256k1fx.OutputOwners{}, + ), + cmpopts.EquateEmpty(), + }) +} + +// CmpOpt returns a configuration for [cmp.Diff] to compare [Tx] instances. +func CmpOpt() cmp.Option { + return cmputils.IfIn[Tx](cmp.Options{ + cmpopts.IgnoreUnexported( + avax.UTXOID{}, + secp256k1fx.OutputOwners{}, + ), + cmpopts.EquateEmpty(), + }) +} + func TestParse(t *testing.T) { for _, test := range Tests { t.Run(test.Name, func(t *testing.T) { @@ -315,12 +340,17 @@ func TestParse(t *testing.T) { got := new(atomic.Tx) _, err := atomic.Codec.Unmarshal(test.Bytes, got) require.NoErrorf(t, err, "%T.Unmarshal(, %T)", atomic.Codec, got) + if diff := cmp.Diff(test.Old, got, OldCmpOpt()); diff != "" { + t.Errorf("%T.Unmarshal(, %T) diff (-want +got):\n%s", atomic.Codec, got, diff) + } assert.Equalf(t, test.Old, got, "%T.Unmarshal(, %T)", atomic.Codec, got) }) t.Run("new", func(t *testing.T) { got, err := Parse(test.Bytes) require.NoError(t, err, "Parse()") - assert.Equal(t, test.New, got, "Parse()") + if diff := cmp.Diff(test.New, got, CmpOpt()); diff != "" { + t.Errorf("Parse() diff (-want +got):\n%s", diff) + } }) }) } @@ -445,22 +475,6 @@ func FuzzParseSliceCompatibility(f *testing.F) { }) } -func FuzzParseRoundTrip(f *testing.F) { - for _, test := range Tests { - f.Add(test.Bytes) - } - f.Fuzz(func(t *testing.T, data []byte) { - tx, err := Parse(data) - if err != nil { - return - } - - got, err := tx.Bytes() - require.NoErrorf(t, err, "%T.Bytes()", tx) - assert.Equal(t, data, got, "Parse(b).Bytes() == b") - }) -} - func FuzzParseSliceRoundTrip(f *testing.F) { { b, err := MarshalSlice(NewTxs) diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index 1cfea7c6c42e..47fdb5c1b709 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/ava-labs/libevm/common" - "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -45,16 +44,13 @@ func (f *F) Fuzz(ff func(t *testing.T, tx *tx.Tx)) { addresses: f.Addresses, assetIDs: f.AssetIDs, } - genTx := d.tx() + tx := d.tx() - // genTx isn't always ideally formatted, so we round-trip through - // parsing before providing it to the test body. - bytes, err := genTx.Bytes() - if err != nil { + // It's possible for the fuzzer to generate a tx that exceeds the codec + // size limits. + if _, err := tx.Bytes(); err != nil { t.Skipf("invalid tx: %s", err) } - tx, err := tx.Parse(bytes) - require.NoError(t, err, "Parse()") ff(t, tx) }) } From 8ae1a82bf862ae8f3d08ea810678adb2d347bb38 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 11:19:01 -0400 Subject: [PATCH 074/120] fix --- vms/saevm/cchain/tx/tx_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index baaeb2643a0b..109e7b44d96c 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -343,7 +343,6 @@ func TestParse(t *testing.T) { if diff := cmp.Diff(test.Old, got, OldCmpOpt()); diff != "" { t.Errorf("%T.Unmarshal(, %T) diff (-want +got):\n%s", atomic.Codec, got, diff) } - assert.Equalf(t, test.Old, got, "%T.Unmarshal(, %T)", atomic.Codec, got) }) t.Run("new", func(t *testing.T) { got, err := Parse(test.Bytes) From d39ed4d9106479517c195ce7ef2df58eadd9466f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 11:22:59 -0400 Subject: [PATCH 075/120] nit --- vms/saevm/cchain/tx/BUILD.bazel | 1 + vms/saevm/cchain/tx/txtest/BUILD.bazel | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index 36e5cedec751..ca9c1afccfae 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -42,6 +42,7 @@ go_test( "//vms/components/avax", "//vms/components/verify", "//vms/saevm/cchain/tx/txtest", + "//vms/saevm/cmputils", "//vms/secp256k1fx", "@com_github_ava_labs_libevm//common", "@com_github_google_go_cmp//cmp", diff --git a/vms/saevm/cchain/tx/txtest/BUILD.bazel b/vms/saevm/cchain/tx/txtest/BUILD.bazel index 4efef60dfeeb..70542ae3fa66 100644 --- a/vms/saevm/cchain/tx/txtest/BUILD.bazel +++ b/vms/saevm/cchain/tx/txtest/BUILD.bazel @@ -17,7 +17,6 @@ go_library( "//vms/saevm/cchain/tx", "//vms/secp256k1fx", "@com_github_ava_labs_libevm//common", - "@com_github_stretchr_testify//require", ], ) From 05930e496e2babd05f73ca3547038d5389a14ca5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 11:27:08 -0400 Subject: [PATCH 076/120] missed one --- vms/saevm/cchain/tx/tx_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 109e7b44d96c..53d67ac816c1 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -388,7 +388,9 @@ func TestParseSlice(t *testing.T) { t.Run(test.name, func(t *testing.T) { got, err := ParseSlice(test.bytes) require.ErrorIs(t, err, test.wantErr, "ParseSlice()") - assert.Equal(t, test.want, got, "ParseSlice()") + if diff := cmp.Diff(test.want, got, CmpOpt()); diff != "" { + t.Errorf("ParseSlice() diff (-want +got):\n%s", diff) + } }) } } From 3937331891b09897a52834f507210fdcec95424c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 11:32:32 -0400 Subject: [PATCH 077/120] more cmp --- vms/saevm/cchain/tx/tx_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index ffb51f42adf5..5899925e5b6f 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1325,7 +1325,9 @@ func TestTransferNonAVAX(t *testing.T) { for assetID, want := range balances { coinID := common.Hash(assetID) got := sdb.GetBalanceMultiCoin(addr, coinID) - require.Zerof(t, got.Cmp(big(want)), "addr=%s asset=%s got=%s want=%d", addr, assetID, got, want) + if diff := cmp.Diff(big(want), got, cmputils.BigInts()); diff != "" { + t.Errorf("%T.GetBalanceMultiCoin(%s, %s) diff (-want +got):\n%s", sdb, addr, coinID, diff) + } } } }) From a6aca7ceb5c16c3484f40a73ae01698cac1c36a6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 11:38:27 -0400 Subject: [PATCH 078/120] nit --- vms/saevm/cchain/tx/tx_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 5899925e5b6f..6df0d5f696bf 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1306,16 +1306,16 @@ func TestTransferNonAVAX(t *testing.T) { wantErr: errInsufficientFunds, }, } - big := func(v uint64) *big.Int { - return new(big.Int).SetUint64(v) - } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - sdb := NewStateDB(t) + var ( + sdb = NewStateDB(t) + toBig = func(v uint64) *big.Int { return new(big.Int).SetUint64(v) } + ) for addr, balances := range test.init { for assetID, amount := range balances { coinID := common.Hash(assetID) - sdb.AddBalanceMultiCoin(addr, coinID, big(amount)) + sdb.AddBalanceMultiCoin(addr, coinID, toBig(amount)) } } @@ -1325,7 +1325,7 @@ func TestTransferNonAVAX(t *testing.T) { for assetID, want := range balances { coinID := common.Hash(assetID) got := sdb.GetBalanceMultiCoin(addr, coinID) - if diff := cmp.Diff(big(want), got, cmputils.BigInts()); diff != "" { + if diff := cmp.Diff(toBig(want), got, cmputils.BigInts()); diff != "" { t.Errorf("%T.GetBalanceMultiCoin(%s, %s) diff (-want +got):\n%s", sdb, addr, coinID, diff) } } From 99f52e15a716ef6f7aa29a1b8691c0a7b521ddc5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 12:06:49 -0400 Subject: [PATCH 079/120] nit --- .../fuzz/FuzzJSONCompatibility/582528ddfad69eb5 | 2 ++ vms/saevm/cchain/tx/txtest/fuzzer.go | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 vms/saevm/cchain/tx/testdata/fuzz/FuzzJSONCompatibility/582528ddfad69eb5 diff --git a/vms/saevm/cchain/tx/testdata/fuzz/FuzzJSONCompatibility/582528ddfad69eb5 b/vms/saevm/cchain/tx/testdata/fuzz/FuzzJSONCompatibility/582528ddfad69eb5 new file mode 100644 index 000000000000..a96f5599e6b7 --- /dev/null +++ b/vms/saevm/cchain/tx/testdata/fuzz/FuzzJSONCompatibility/582528ddfad69eb5 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("0") diff --git a/vms/saevm/cchain/tx/txtest/fuzzer.go b/vms/saevm/cchain/tx/txtest/fuzzer.go index 47fdb5c1b709..d3433551c96e 100644 --- a/vms/saevm/cchain/tx/txtest/fuzzer.go +++ b/vms/saevm/cchain/tx/txtest/fuzzer.go @@ -10,6 +10,9 @@ import ( "github.com/ava-labs/libevm/common" + // Imported for [codec.Manager] comment resolution. + _ "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" @@ -106,10 +109,12 @@ func element[T any](d *decoder, s []T, gen func(*decoder) T) T { return s[d.intn(len(s))] } -// sliceOf generates a random slice of generated entries. The length is random, -// but is typically small. +// sliceOf generates a random slice of generated entries. The returned value is +// non-nil. The length is random, but is typically small. func sliceOf[T any](d *decoder, gen func(*decoder) T) []T { - var out []T + // The [codec.Manager] always generates non-nil slices. To avoid nil and + // empty comparison issues, we also always generate non-nil slices. + out := []T{} // [decoder.bool] returns false once the data is exhausted, so this loop // will eventually terminate. for d.bool() { From 8bb04f6f4766a49de6517e3bc7363d595db559ed Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 12:13:14 -0400 Subject: [PATCH 080/120] bazel --- vms/saevm/cchain/tx/txtest/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/vms/saevm/cchain/tx/txtest/BUILD.bazel b/vms/saevm/cchain/tx/txtest/BUILD.bazel index 70542ae3fa66..3561b22ac8c7 100644 --- a/vms/saevm/cchain/tx/txtest/BUILD.bazel +++ b/vms/saevm/cchain/tx/txtest/BUILD.bazel @@ -12,6 +12,7 @@ go_library( srcs = ["fuzzer.go"], importpath = "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx/txtest", deps = [ + "//codec", "//ids", "//vms/components/avax", "//vms/saevm/cchain/tx", From 23b99161839078a0b224ddaa8ef2c75194e13ace Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 12:31:15 -0400 Subject: [PATCH 081/120] Add comment --- vms/saevm/cchain/tx/compatibility_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index d92d41bb0324..874450767efd 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -143,6 +143,8 @@ func (*asOpStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { func (s *asOpStateDB) SetNonce(addr common.Address, nonce uint64) { d := s.op.Burn[addr] + // The op specifies what nonce is being consumed, not the next nonce. So we + // need to subtract 1. d.Nonce = nonce - 1 s.op.Burn[addr] = d } From c1b9fa8966a62976608b237da24e1044420d4f8e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 12:35:30 -0400 Subject: [PATCH 082/120] nit --- vms/saevm/cchain/tx/import.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 82b83c6b1091..1d92acc37217 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -86,8 +86,10 @@ func (i *Import) asOp(avaxAssetID ids.ID) (op, error) { continue } - amount := scaleAVAX(out.Amount) - total := mint[out.Address] + var ( + total = mint[out.Address] + amount = scaleAVAX(out.Amount) + ) if _, overflow := total.AddOverflow(&total, &amount); overflow { return op{}, fmt.Errorf("%w: for address %s", errOverflow, out.Address) } From b4d98b56e255918ee34c690a280291589272ab84 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 12:36:38 -0400 Subject: [PATCH 083/120] nit --- vms/saevm/cchain/tx/tx.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 6eb6e647598f..72367e938368 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -50,6 +50,7 @@ type Unsigned interface { asOp(avaxAssetID ids.ID) (op, error) } +// op contains the state changes of [hook.Op] type op struct { burn map[common.Address]hook.AccountDebit mint map[common.Address]uint256.Int From 81c3c026fa7885d595b1d4367b3a9354abca3c2b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 12:38:40 -0400 Subject: [PATCH 084/120] nit --- vms/saevm/cchain/tx/tx_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 2afabd57e251..331369399adf 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -679,7 +679,7 @@ func TestID(t *testing.T) { t.Run("old", func(t *testing.T) { // We must parse the old tx to properly initialize the ID. old, err := ParseOldTx(test.Bytes) - require.NoError(t, err, "parseOldTx()") + require.NoError(t, err, "ParseOldTx()") assert.Equalf(t, test.Op.ID, old.ID(), "%T.ID()", old) }) t.Run("new", func(t *testing.T) { From 9e366431e1d5923e4d4064ae5980ef52e82e8e99 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 12:39:02 -0400 Subject: [PATCH 085/120] nit --- vms/saevm/cchain/tx/tx_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 331369399adf..0af6f0d73273 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -942,8 +942,8 @@ func TestAsOp(t *testing.T) { for _, test := range Tests { t.Run(test.Name, func(t *testing.T) { got, err := test.New.AsOp(AVAXAssetID) - require.NoErrorf(t, err, "%T.AsOp(avaxAssetID)", test.New) - assert.Equalf(t, test.Op, got, "%T.AsOp(avaxAssetID)", test.New) + require.NoErrorf(t, err, "%T.AsOp(AVAXAssetID)", test.New) + assert.Equalf(t, test.Op, got, "%T.AsOp(AVAXAssetID)", test.New) }) } } @@ -1007,7 +1007,7 @@ func TestAsOp_Errors(t *testing.T) { Unsigned: test.tx, } _, err := tx.AsOp(AVAXAssetID) - require.ErrorIsf(t, err, test.want, "%T.AsOp(avaxAssetID)", tx) + require.ErrorIsf(t, err, test.want, "%T.AsOp(AVAXAssetID)", tx) }) } } From 40c3ba62e02fda4536a8d76253826fcaf55a221b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 14:46:02 -0400 Subject: [PATCH 086/120] cleanup --- vms/saevm/cchain/tx/tx_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 0af6f0d73273..8ac4bd188717 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -143,7 +143,7 @@ var ( ID: ids.FromStringOrPanic("h34BPNmYApCbW8buVWAtzu1KtjTFmyMhiRQQnAqPqwCqQsB7f"), Gas: 11230, Mint: map[common.Address]uint256.Int{ - common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"): *uint256.NewInt(50_000_000 * _x2cRate), + common.HexToAddress("0xb8b5a87d1c05676f1f966da49151fa54dbe68c33"): scaleAVAX(50_000_000), }, }, }, @@ -250,8 +250,8 @@ var ( GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 11230), Burn: map[common.Address]hook.AccountDebit{ common.HexToAddress("0xeb019ccd325ad53543a7e7e3b04828bdecf3cff6"): { - Amount: *uint256.NewInt(1_000_001 * _x2cRate), - MinBalance: *uint256.NewInt(1_000_001 * _x2cRate), + Amount: scaleAVAX(1_000_001), + MinBalance: scaleAVAX(1_000_001), }, }, }, @@ -461,7 +461,7 @@ var ( ID: ids.FromStringOrPanic("2Av7bXLRwxiQhbT9EcQd8KRM3Lz6VkpTqf3Y1AT5peHZ4YAohS"), Gas: 13526, Mint: map[common.Address]uint256.Int{ - common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"): *uint256.NewInt(597_000_000 * _x2cRate), + common.HexToAddress("0x383c293db6be7ac246f0956ad632344dc2cd1da3"): scaleAVAX(597_000_000), }, }, }, @@ -522,8 +522,8 @@ var ( Burn: map[common.Address]hook.AccountDebit{ {}: { Nonce: 5, - Amount: *uint256.NewInt(1_000_000 * _x2cRate), - MinBalance: *uint256.NewInt(1_000_000 * _x2cRate), + Amount: scaleAVAX(1_000_000), + MinBalance: scaleAVAX(1_000_000), }, }, }, @@ -592,8 +592,8 @@ var ( }, {2}: { Nonce: 7, - Amount: *uint256.NewInt(1_000_000 * _x2cRate), - MinBalance: *uint256.NewInt(1_000_000 * _x2cRate), + Amount: scaleAVAX(1_000_000), + MinBalance: scaleAVAX(1_000_000), }, }, }, From 3ed65476ca846836688fd2b4968cc658799172c8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 14:47:52 -0400 Subject: [PATCH 087/120] cleanup --- vms/saevm/cchain/tx/tx_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 8ac4bd188717..fa481d24946e 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -20,13 +20,12 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/saevm/cmputils" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" - - safemath "github.com/ava-labs/avalanchego/utils/math" ) // Tests is defined at the package level to allow sharing between fuzz tests and @@ -982,7 +981,7 @@ func TestAsOp_Errors(t *testing.T) { Amount: 2, }}, }, - want: safemath.ErrUnderflow, + want: math.ErrUnderflow, }, { name: "export_burned_underflow", @@ -998,7 +997,7 @@ func TestAsOp_Errors(t *testing.T) { }, }}, }, - want: safemath.ErrUnderflow, + want: math.ErrUnderflow, }, } for _, test := range tests { From f0515ec1c02c7482b8bf2832945ae2eb28c666e3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 15:21:12 -0400 Subject: [PATCH 088/120] Add more cases --- vms/saevm/cchain/tx/tx_test.go | 104 ++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index fa481d24946e..c571b4053967 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -6,6 +6,7 @@ package tx import ( "encoding/json" "errors" + "math" "testing" "github.com/ava-labs/libevm/common" @@ -20,12 +21,13 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/saevm/cmputils" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + safemath "github.com/ava-labs/avalanchego/utils/math" ) // Tests is defined at the package level to allow sharing between fuzz tests and @@ -967,6 +969,54 @@ func TestAsOp_Errors(t *testing.T) { }, want: errMultipleNonces, }, + { + name: "import_burned_overflow", + tx: &Import{ + ImportedInputs: []*avax.TransferableInput{ + { + Asset: avax.Asset{ID: AVAXAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: math.MaxUint64, + }, + }, + { + Asset: avax.Asset{ID: AVAXAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 2, + }, + }, + }, + Outs: []Output{{ + AssetID: AVAXAssetID, + Amount: 1, + }}, + }, + want: safemath.ErrOverflow, + }, + { + name: "import_burned_intermediate_overflow", + tx: &Import{ + ImportedInputs: []*avax.TransferableInput{ + { + Asset: avax.Asset{ID: AVAXAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: math.MaxUint64, + }, + }, + { + Asset: avax.Asset{ID: AVAXAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: 1, + }, + }, + }, + Outs: []Output{{ + AssetID: AVAXAssetID, + Amount: 1, + }}, + }, + want: safemath.ErrOverflow, + }, { name: "import_burned_underflow", tx: &Import{ @@ -981,7 +1031,55 @@ func TestAsOp_Errors(t *testing.T) { Amount: 2, }}, }, - want: math.ErrUnderflow, + want: safemath.ErrUnderflow, + }, + { + name: "export_burned_overflow", + tx: &Export{ + Ins: []Input{ + { + Address: common.Address{0}, + AssetID: AVAXAssetID, + Amount: math.MaxUint64, + }, + { + Address: common.Address{1}, + AssetID: AVAXAssetID, + Amount: 2, + }, + }, + ExportedOutputs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + }, + }}, + }, + want: safemath.ErrOverflow, + }, + { + name: "export_burned_intermediate_overflow", + tx: &Export{ + Ins: []Input{ + { + Address: common.Address{0}, + AssetID: AVAXAssetID, + Amount: math.MaxUint64, + }, + { + Address: common.Address{1}, + AssetID: AVAXAssetID, + Amount: 1, + }, + }, + ExportedOutputs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + }, + }}, + }, + want: safemath.ErrOverflow, }, { name: "export_burned_underflow", @@ -997,7 +1095,7 @@ func TestAsOp_Errors(t *testing.T) { }, }}, }, - want: math.ErrUnderflow, + want: safemath.ErrUnderflow, }, } for _, test := range tests { From 6dbe653614f98238a87d9c59d236db71ade75dcf Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 15:30:11 -0400 Subject: [PATCH 089/120] add comments --- vms/saevm/cchain/tx/export.go | 9 +++++++++ vms/saevm/cchain/tx/import.go | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 83e9dbeb47a5..ccad941f053e 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -9,6 +9,9 @@ import ( "github.com/ava-labs/libevm/common" + // Imported for [atomic.UnsignedExportTx.Burned] comment resolution. + _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -39,6 +42,12 @@ type Input struct { Nonce uint64 `serialize:"true" json:"nonce"` } +// Similarly to [atomic.UnsignedExportTx.Burned], burned will error if the sum +// of the inputs exceeds MaxUint64; even if the total amount burned could be +// represented as a uint64. +// +// Because the total supply of AVAX fits in a uint64, this doesn't matter in +// practice and allows for easier fuzzing. func (e *Export) burned(assetID ids.ID) (uint64, error) { var ( burned uint64 diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 1d92acc37217..62867b5d45ef 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -10,6 +10,9 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" + // Imported for [atomic.UnsignedImportTx.Burned] comment resolution. + _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -39,6 +42,12 @@ type Output struct { AssetID ids.ID `serialize:"true" json:"assetID"` } +// Similarly to [atomic.UnsignedImportTx.Burned], burned will error if the sum +// of the inputs exceeds MaxUint64; even if the total amount burned could be +// represented as a uint64. +// +// Because the total supply of AVAX fits in a uint64, this doesn't matter in +// practice and allows for easier fuzzing. func (i *Import) burned(assetID ids.ID) (uint64, error) { var ( burned uint64 From 9ae76cc1b80d9326994d5d24b5d3b256948c8e26 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 15:31:24 -0400 Subject: [PATCH 090/120] Add interface check --- vms/saevm/cchain/tx/export.go | 2 ++ vms/saevm/cchain/tx/import.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index ccad941f053e..6a666fa9609a 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -18,6 +18,8 @@ import ( "github.com/ava-labs/avalanchego/vms/saevm/hook" ) +var _ Unsigned = (*Export)(nil) + // Export is the unsigned component of a transaction that transfers assets from // the C-Chain to either the P-Chain or the X-Chain. It modifies the C-Chain // state and produces UTXOs in the shared memory between the C-Chain and the diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 62867b5d45ef..0d66725407c6 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -19,6 +19,8 @@ import ( "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) +var _ Unsigned = (*Import)(nil) + // Import is the unsigned component of a transaction that transfers assets from // either the P-Chain or the X-Chain to the C-Chain. It consumes UTXOs in the // shared memory between the C-Chain and the source chain and increases balances From 6838983558771557d51d0b3a0d323fc61b0b67ef Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 15:43:57 -0400 Subject: [PATCH 091/120] nit --- vms/saevm/cchain/tx/export.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 6a666fa9609a..398c094a9665 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -88,8 +88,7 @@ func (e *Export) asOp(avaxAssetID ids.ID) (op, error) { return op{}, fmt.Errorf("%w: address %s has nonces %d and %d", errMultipleNonces, in.Address, debit.Nonce, in.Nonce) } - // Non-AVAX inputs still record the address+nonce so SAE will increment - // the nonce, even though no AVAX is debited. + // Even if no AVAX is debited, Non-AVAX inputs MUST increment the nonce. if in.AssetID == avaxAssetID { amount := scaleAVAX(in.Amount) if _, overflow := debit.Amount.AddOverflow(&debit.Amount, &amount); overflow { From d44b9e648b97e4f54ffaa9a7f16978054e583f15 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 15:58:57 -0400 Subject: [PATCH 092/120] grammar --- vms/saevm/cchain/tx/export.go | 4 ++-- vms/saevm/cchain/tx/import.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index 398c094a9665..ee305a3ddc58 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -44,8 +44,8 @@ type Input struct { Nonce uint64 `serialize:"true" json:"nonce"` } -// Similarly to [atomic.UnsignedExportTx.Burned], burned will error if the sum -// of the inputs exceeds MaxUint64; even if the total amount burned could be +// Like [atomic.UnsignedExportTx.Burned], burned will error if the sum of the +// inputs exceeds MaxUint64, even if the total amount burned could be // represented as a uint64. // // Because the total supply of AVAX fits in a uint64, this doesn't matter in diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index 0d66725407c6..262562398bf3 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -44,8 +44,8 @@ type Output struct { AssetID ids.ID `serialize:"true" json:"assetID"` } -// Similarly to [atomic.UnsignedImportTx.Burned], burned will error if the sum -// of the inputs exceeds MaxUint64; even if the total amount burned could be +// Like [atomic.UnsignedImportTx.Burned], burned will error if the sum of the +// inputs exceeds MaxUint64, even if the total amount burned could be // represented as a uint64. // // Because the total supply of AVAX fits in a uint64, this doesn't matter in From 900d328e4ac4411a9be30ad233e2bb24710b466e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 16:47:46 -0400 Subject: [PATCH 093/120] nit --- vms/saevm/cchain/tx/tx.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index db3065eb2042..9da2183e2b7f 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -183,8 +183,8 @@ func gasPrice(cost uint64, gas gas.Gas) uint256.Int { return p } -// AtomicRequests returns chainID and shared-memory modifications that this -// transaction should perform during execution. +// AtomicRequests returns shared-memory modifications that this transaction +// should perform on the peer chainID during execution. func (t *Tx) AtomicRequests() (ids.ID, *chainsatomic.Requests, error) { return t.Unsigned.atomicRequests(t.ID()) } From bcc4037f04baca225fa8cc21e43c10c87e62a644 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 16:49:42 -0400 Subject: [PATCH 094/120] nit --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 9da2183e2b7f..6fc4c3b64873 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -186,7 +186,7 @@ func gasPrice(cost uint64, gas gas.Gas) uint256.Int { // AtomicRequests returns shared-memory modifications that this transaction // should perform on the peer chainID during execution. func (t *Tx) AtomicRequests() (ids.ID, *chainsatomic.Requests, error) { - return t.Unsigned.atomicRequests(t.ID()) + return t.atomicRequests(t.ID()) } // Parse deserializes a [Tx] from its canonical binary format. From 53703717e3d94c65622f0b8d8037a2a2e29c64ef Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 16:51:16 -0400 Subject: [PATCH 095/120] more succinct --- vms/saevm/cchain/tx/tx.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 72367e938368..3297aa8e8dce 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -100,9 +100,9 @@ func (t *Tx) AsOp(avaxAssetID ids.ID) (hook.Op, error) { return hook.Op{}, fmt.Errorf("calculating amount burned: %w", err) } - op, err := t.Unsigned.asOp(avaxAssetID) + op, err := t.asOp(avaxAssetID) if err != nil { - return hook.Op{}, fmt.Errorf("converting unsigned transaction to operation: %w", err) + return hook.Op{}, fmt.Errorf("converting to operation: %w", err) } return hook.Op{ From a5c21d1caae2d3f7c5a0eca6801e00efbfeb225e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 18:42:37 -0400 Subject: [PATCH 096/120] pedantic --- vms/saevm/cchain/tx/export.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index ee305a3ddc58..0681045b003e 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -88,7 +88,7 @@ func (e *Export) asOp(avaxAssetID ids.ID) (op, error) { return op{}, fmt.Errorf("%w: address %s has nonces %d and %d", errMultipleNonces, in.Address, debit.Nonce, in.Nonce) } - // Even if no AVAX is debited, Non-AVAX inputs MUST increment the nonce. + // Even if no AVAX is debited, non-AVAX inputs MUST increment the nonce. if in.AssetID == avaxAssetID { amount := scaleAVAX(in.Amount) if _, overflow := debit.Amount.AddOverflow(&debit.Amount, &amount); overflow { From eb701ee4fabab753d12d2f8fadb93a53894594ef Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 19:43:28 -0400 Subject: [PATCH 097/120] ok --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 3297aa8e8dce..fd370c6450c5 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -165,7 +165,7 @@ func scaleAVAX(nAVAX uint64) uint256.Int { } // gasPrice takes in the cost, in nAVAX, and the gas and returns the price per -// gas in aAVAX/gas. +// gas in aAVAX/gas. It assumes gas is non-zero. // // The result is rounded down to the nearest aAVAX/gas. func gasPrice(cost uint64, gas gas.Gas) uint256.Int { From 1ad0654767dc09c548b743d6edc709a19381f6a5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 30 Apr 2026 19:46:26 -0400 Subject: [PATCH 098/120] Document codec weirdness --- vms/saevm/cchain/tx/tx.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index fd370c6450c5..9509736401d4 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -126,6 +126,8 @@ const ( ) func gasUsed(t Unsigned) (gas.Gas, error) { + // We MUST provide a pointer to t so that the returned size includes the + // type ID. numBytes, err := c.Size(codecVersion, &t) if err != nil { return 0, err From 921d35d194483f83f53eae155fbea5a95ad4007e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 May 2026 10:07:38 -0400 Subject: [PATCH 099/120] cleanup --- vms/saevm/cchain/tx/compatibility_test.go | 4 +- vms/saevm/cchain/tx/export.go | 4 +- vms/saevm/cchain/tx/tx_test.go | 129 ++++------------------ 3 files changed, 27 insertions(+), 110 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 79ed30f689a4..c7558880a6f8 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -212,7 +212,7 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { require.NoError(t, op.ApplyTo(newSDB.StateDB)) // We must manually finalize the trie structures before comparison. - // Otherwise, comparing the state DBs would trivially pass. + // Otherwise, comparing the state DBs wouldn't include the changes. for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { sdb.Finalise(true) sdb.IntermediateRoot(true) @@ -223,7 +223,7 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { cmputils.StateDBs(), } if diff := cmp.Diff(oldSDB, newSDB, opts...); diff != "" { - t.Errorf("%T.AsOp() diff (-want +got):\n%s", newTx, diff) + t.Errorf("%T.TransferNonAVAX() diff (-want +got):\n%s", newTx, diff) } }) } diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index dd697ef846fb..c2d59c256a91 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -149,8 +149,8 @@ func (e *Export) TransferNonAVAX(avaxAssetID ids.ID, statedb *extstate.StateDB) coinID := common.Hash(in.AssetID) amount := new(big.Int).SetUint64(in.Amount) - if statedb.GetBalanceMultiCoin(in.Address, coinID).Cmp(amount) < 0 { - return errInsufficientFunds + if balance := statedb.GetBalanceMultiCoin(in.Address, coinID); balance.Cmp(amount) < 0 { + return fmt.Errorf("%w: address %s asset %s has %d want %d", errInsufficientFunds, in.Address, coinID, balance, amount) } statedb.SubBalanceMultiCoin(in.Address, coinID, amount) } diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index e9da64245e3d..2928bd3ef761 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1196,22 +1196,16 @@ func TestTransferNonAVAX(t *testing.T) { { name: "import_avax", tx: &Import{ - Outs: []Output{{ - Address: alice, - Amount: 1, - AssetID: AVAXAssetID, - }}, + Outs: []Output{ + {Address: alice, Amount: 1, AssetID: AVAXAssetID}, + }, }, }, { name: "import_non_avax", tx: &Import{ Outs: []Output{ - { - Address: alice, - Amount: 1, - AssetID: btc, - }, + {Address: alice, Amount: 1, AssetID: btc}, }, }, want: map[common.Address]map[ids.ID]uint64{ @@ -1224,41 +1218,13 @@ func TestTransferNonAVAX(t *testing.T) { name: "import_many", tx: &Import{ Outs: []Output{ - { - Address: alice, - Amount: 1, - AssetID: AVAXAssetID, - }, - { - Address: alice, - Amount: 10, - AssetID: AVAXAssetID, - }, - { - Address: bob, - Amount: 100, - AssetID: AVAXAssetID, - }, - { - Address: alice, - Amount: 1_000, - AssetID: btc, - }, - { - Address: alice, - Amount: 10_000, - AssetID: btc, - }, - { - Address: bob, - Amount: 100_000, - AssetID: btc, - }, - { - Address: bob, - Amount: 1_000_000, - AssetID: eth, - }, + {Address: alice, Amount: 1, AssetID: AVAXAssetID}, + {Address: alice, Amount: 10, AssetID: AVAXAssetID}, + {Address: bob, Amount: 100, AssetID: AVAXAssetID}, + {Address: alice, Amount: 1_000, AssetID: btc}, + {Address: alice, Amount: 10_000, AssetID: btc}, + {Address: bob, Amount: 100_000, AssetID: btc}, + {Address: bob, Amount: 1_000_000, AssetID: eth}, }, }, want: map[common.Address]map[ids.ID]uint64{ @@ -1275,11 +1241,7 @@ func TestTransferNonAVAX(t *testing.T) { name: "export_avax", tx: &Export{ Ins: []Input{ - { - Address: alice, - Amount: 1, - AssetID: AVAXAssetID, - }, + {Address: alice, Amount: 1, AssetID: AVAXAssetID}, }, }, }, @@ -1292,11 +1254,7 @@ func TestTransferNonAVAX(t *testing.T) { }, tx: &Export{ Ins: []Input{ - { - Address: alice, - Amount: 1, - AssetID: btc, - }, + {Address: alice, Amount: 1, AssetID: btc}, }, }, want: map[common.Address]map[ids.ID]uint64{ @@ -1305,7 +1263,6 @@ func TestTransferNonAVAX(t *testing.T) { }, }, }, - { name: "export_many", init: map[common.Address]map[ids.ID]uint64{ @@ -1319,41 +1276,13 @@ func TestTransferNonAVAX(t *testing.T) { }, tx: &Export{ Ins: []Input{ - { - Address: alice, - Amount: 1, - AssetID: AVAXAssetID, - }, - { - Address: alice, - Amount: 10, - AssetID: AVAXAssetID, - }, - { - Address: bob, - Amount: 100, - AssetID: AVAXAssetID, - }, - { - Address: alice, - Amount: 1_000, - AssetID: btc, - }, - { - Address: alice, - Amount: 10_000, - AssetID: btc, - }, - { - Address: bob, - Amount: 100_000, - AssetID: btc, - }, - { - Address: bob, - Amount: 1_000_000, - AssetID: eth, - }, + {Address: alice, Amount: 1, AssetID: AVAXAssetID}, + {Address: alice, Amount: 10, AssetID: AVAXAssetID}, + {Address: bob, Amount: 100, AssetID: AVAXAssetID}, + {Address: alice, Amount: 1_000, AssetID: btc}, + {Address: alice, Amount: 10_000, AssetID: btc}, + {Address: bob, Amount: 100_000, AssetID: btc}, + {Address: bob, Amount: 1_000_000, AssetID: eth}, }, }, want: map[common.Address]map[ids.ID]uint64{ @@ -1370,11 +1299,7 @@ func TestTransferNonAVAX(t *testing.T) { name: "export_non_avax_insufficient", tx: &Export{ Ins: []Input{ - { - Address: alice, - Amount: 1, - AssetID: btc, - }, + {Address: alice, Amount: 1, AssetID: btc}, }, }, wantErr: errInsufficientFunds, @@ -1388,16 +1313,8 @@ func TestTransferNonAVAX(t *testing.T) { }, tx: &Export{ Ins: []Input{ - { - Address: alice, - Amount: 1, - AssetID: btc, - }, - { - Address: alice, - Amount: 1, - AssetID: btc, - }, + {Address: alice, Amount: 1, AssetID: btc}, + {Address: alice, Amount: 1, AssetID: btc}, }, }, wantErr: errInsufficientFunds, From e340ee91a7e755e7f67b1174ce64da9683421386 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 May 2026 12:33:22 -0400 Subject: [PATCH 100/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index c7558880a6f8..3970b80128b0 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -139,8 +139,7 @@ func (s *asOpStateDB) SubBalance(addr common.Address, amount *uint256.Int) { } func (*asOpStateDB) GetBalance(common.Address) *uint256.Int { - // Large enough to never underflow, but small enough to never overflow. - return new(uint256.Int).Lsh(uint256.NewInt(1), 128) + return largeUint256() } func (*asOpStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} @@ -148,8 +147,7 @@ func (*asOpStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) { func (*asOpStateDB) SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) {} func (*asOpStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { - // Large enough to never underflow, but small enough to never overflow. - return new(big.Int).Lsh(big.NewInt(1), 128) + return largeBigInt() } func (s *asOpStateDB) SetNonce(addr common.Address, nonce uint64) { @@ -188,17 +186,15 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { newSDB := NewStateDB(t) if tx, ok := newTx.Unsigned.(*Export); ok { - hugeAVAX := new(uint256.Int).Lsh(uint256.NewInt(1), 128) - hugeBig := new(big.Int).Lsh(big.NewInt(1), 128) for _, in := range tx.Ins { if in.Nonce == math.MaxUint64 { t.Skip("nonce overflow") } for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { - sdb.AddBalance(in.Address, hugeAVAX) + sdb.AddBalance(in.Address, largeUint256()) sdb.SetNonce(in.Address, in.Nonce) - sdb.AddBalanceMultiCoin(in.Address, common.Hash(in.AssetID), hugeBig) + sdb.AddBalanceMultiCoin(in.Address, common.Hash(in.AssetID), largeBigInt()) } } } @@ -227,3 +223,8 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { } }) } + +// largeUint256 and largeBigInt return a balance large enough to never underflow +// but small enough to never overflow during test arithmetic. +func largeUint256() *uint256.Int { return new(uint256.Int).Lsh(uint256.NewInt(1), 128) } +func largeBigInt() *big.Int { return new(big.Int).Lsh(big.NewInt(1), 128) } From 38c857dde4a5ac28c1961cacff5841edbea9e2e8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 May 2026 12:35:09 -0400 Subject: [PATCH 101/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 27 ++++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 3970b80128b0..94e320775c3a 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -182,8 +182,9 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { t.Skip("invalid tx") } - oldSDB := NewStateDB(t) - newSDB := NewStateDB(t) + oldState := NewStateDB(t) + newState := NewStateDB(t) + states := []*extstate.StateDB{oldState, newState} if tx, ok := newTx.Unsigned.(*Export); ok { for _, in := range tx.Ins { @@ -191,10 +192,10 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { t.Skip("nonce overflow") } - for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { - sdb.AddBalance(in.Address, largeUint256()) - sdb.SetNonce(in.Address, in.Nonce) - sdb.AddBalanceMultiCoin(in.Address, common.Hash(in.AssetID), largeBigInt()) + for _, state := range states { + state.AddBalance(in.Address, largeUint256()) + state.SetNonce(in.Address, in.Nonce) + state.AddBalanceMultiCoin(in.Address, common.Hash(in.AssetID), largeBigInt()) } } } @@ -203,22 +204,22 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { oldTx = ToOldTx(t, newTx) ctx = &snow.Context{AVAXAssetID: AVAXAssetID} ) - require.NoError(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldSDB)) - require.NoError(t, newTx.TransferNonAVAX(AVAXAssetID, newSDB)) - require.NoError(t, op.ApplyTo(newSDB.StateDB)) + require.NoError(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldState)) + require.NoError(t, newTx.TransferNonAVAX(AVAXAssetID, newState)) + require.NoError(t, op.ApplyTo(newState.StateDB)) // We must manually finalize the trie structures before comparison. // Otherwise, comparing the state DBs wouldn't include the changes. - for _, sdb := range []*extstate.StateDB{oldSDB, newSDB} { - sdb.Finalise(true) - sdb.IntermediateRoot(true) + for _, state := range states { + state.Finalise(true) + state.IntermediateRoot(true) } opts := []cmp.Option{ cmpopts.IgnoreUnexported(extstate.StateDB{}), cmputils.StateDBs(), } - if diff := cmp.Diff(oldSDB, newSDB, opts...); diff != "" { + if diff := cmp.Diff(oldState, newState, opts...); diff != "" { t.Errorf("%T.TransferNonAVAX() diff (-want +got):\n%s", newTx, diff) } }) From 26a19a49eb2205dd40346028e4bf5e0bc62e58b2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 May 2026 12:42:16 -0400 Subject: [PATCH 102/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 4 ++-- vms/saevm/cchain/tx/tx_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 94e320775c3a..5f58a2c9bbe0 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -182,8 +182,8 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { t.Skip("invalid tx") } - oldState := NewStateDB(t) - newState := NewStateDB(t) + oldState := NewEmptyStateDB(t) + newState := NewEmptyStateDB(t) states := []*extstate.StateDB{oldState, newState} if tx, ok := newTx.Unsigned.(*Export); ok { diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 2928bd3ef761..1fb824427703 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1170,7 +1170,7 @@ func TestAtomicRequests(t *testing.T) { } } -func NewStateDB(t testing.TB) *extstate.StateDB { +func NewEmptyStateDB(t testing.TB) *extstate.StateDB { t.Helper() db := state.NewDatabase(rawdb.NewMemoryDatabase()) @@ -1323,7 +1323,7 @@ func TestTransferNonAVAX(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var ( - sdb = NewStateDB(t) + sdb = NewEmptyStateDB(t) toBig = func(v uint64) *big.Int { return new(big.Int).SetUint64(v) } ) for addr, balances := range test.init { From 0277d2266d54fef23eff943ba02c42ae6097f9ac Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 May 2026 12:42:35 -0400 Subject: [PATCH 103/120] nit --- vms/saevm/cchain/tx/tx_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 1fb824427703..8f170c858dc0 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1323,24 +1323,24 @@ func TestTransferNonAVAX(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var ( - sdb = NewEmptyStateDB(t) + state = NewEmptyStateDB(t) toBig = func(v uint64) *big.Int { return new(big.Int).SetUint64(v) } ) for addr, balances := range test.init { for assetID, amount := range balances { coinID := common.Hash(assetID) - sdb.AddBalanceMultiCoin(addr, coinID, toBig(amount)) + state.AddBalanceMultiCoin(addr, coinID, toBig(amount)) } } - err := test.tx.TransferNonAVAX(AVAXAssetID, sdb) + err := test.tx.TransferNonAVAX(AVAXAssetID, state) require.ErrorIs(t, err, test.wantErr) for addr, balances := range test.want { for assetID, want := range balances { coinID := common.Hash(assetID) - got := sdb.GetBalanceMultiCoin(addr, coinID) + got := state.GetBalanceMultiCoin(addr, coinID) if diff := cmp.Diff(toBig(want), got, cmputils.BigInts()); diff != "" { - t.Errorf("%T.GetBalanceMultiCoin(%s, %s) diff (-want +got):\n%s", sdb, addr, coinID, diff) + t.Errorf("%T.GetBalanceMultiCoin(%s, %s) diff (-want +got):\n%s", state, addr, coinID, diff) } } } From 342b458c9e6777e1b878bf68b43c4930d75cee9c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 May 2026 12:58:53 -0400 Subject: [PATCH 104/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 5f58a2c9bbe0..fa96cf4b79a0 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -188,6 +188,9 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { if tx, ok := newTx.Unsigned.(*Export); ok { for _, in := range tx.Ins { + // Coreth silently overflows the nonce, whereas SAE will leave + // the nonce unmodified. This difference doesn't matter on live + // networks. if in.Nonce == math.MaxUint64 { t.Skip("nonce overflow") } From f37215f770eaa4d0e65b04bf563f1c8e46734148 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 May 2026 15:14:54 -0400 Subject: [PATCH 105/120] nit --- vms/avm/txs/executor/executor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/avm/txs/executor/executor.go b/vms/avm/txs/executor/executor.go index 5e5397bea42b..1c644c0ea2b5 100644 --- a/vms/avm/txs/executor/executor.go +++ b/vms/avm/txs/executor/executor.go @@ -132,8 +132,8 @@ func (e *Executor) ExportTx(tx *txs.ExportTx) error { Key: utxoID[:], Value: utxoBytes, } - if out, ok := utxo.Out.(avax.Addressable); ok { - elem.Traits = out.Addresses() + if o, ok := utxo.Out.(avax.Addressable); ok { + elem.Traits = o.Addresses() } elems[i] = elem From f456eac254ccd2b86383723487494ad7686c17f6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 May 2026 15:22:25 -0400 Subject: [PATCH 106/120] address nit --- vms/saevm/cchain/tx/tx_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 8f170c858dc0..9b97028bc53b 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1335,11 +1335,12 @@ func TestTransferNonAVAX(t *testing.T) { err := test.tx.TransferNonAVAX(AVAXAssetID, state) require.ErrorIs(t, err, test.wantErr) - for addr, balances := range test.want { - for assetID, want := range balances { - coinID := common.Hash(assetID) + for _, addr := range []common.Address{alice, bob} { + for _, asset := range []ids.ID{AVAXAssetID, btc, eth} { + want := toBig(test.want[addr][asset]) + coinID := common.Hash(asset) got := state.GetBalanceMultiCoin(addr, coinID) - if diff := cmp.Diff(toBig(want), got, cmputils.BigInts()); diff != "" { + if diff := cmp.Diff(want, got, cmputils.BigInts()); diff != "" { t.Errorf("%T.GetBalanceMultiCoin(%s, %s) diff (-want +got):\n%s", state, addr, coinID, diff) } } From ac82445467c8c614be434da1fb6d3858723b34a9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 May 2026 11:21:29 -0400 Subject: [PATCH 107/120] simplify --- vms/saevm/cchain/tx/tx_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index c571b4053967..654a17dca39a 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -762,9 +762,8 @@ func TestParse(t *testing.T) { for _, test := range Tests { t.Run(test.Name, func(t *testing.T) { t.Run("old", func(t *testing.T) { - got := new(atomic.Tx) - _, err := atomic.Codec.Unmarshal(test.Bytes, got) - require.NoErrorf(t, err, "%T.Unmarshal(, %T)", atomic.Codec, got) + got, err := ParseOldTx(test.Bytes) + require.NoError(t, err, "ParseOldTx()") if diff := cmp.Diff(test.Old, got, OldCmpOpt()); diff != "" { t.Errorf("%T.Unmarshal(, %T) diff (-want +got):\n%s", atomic.Codec, got, diff) } From 51a986d485ccd4908e128a3bd937e24467fe3ecb Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 May 2026 13:35:11 -0400 Subject: [PATCH 108/120] reduce diff --- vms/avm/txs/executor/executor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/avm/txs/executor/executor.go b/vms/avm/txs/executor/executor.go index 1c644c0ea2b5..5e5397bea42b 100644 --- a/vms/avm/txs/executor/executor.go +++ b/vms/avm/txs/executor/executor.go @@ -132,8 +132,8 @@ func (e *Executor) ExportTx(tx *txs.ExportTx) error { Key: utxoID[:], Value: utxoBytes, } - if o, ok := utxo.Out.(avax.Addressable); ok { - elem.Traits = o.Addresses() + if out, ok := utxo.Out.(avax.Addressable); ok { + elem.Traits = out.Addresses() } elems[i] = elem From 86b2b6e2b794422ee9feaa09d022e90226c3719b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 May 2026 13:35:54 -0400 Subject: [PATCH 109/120] oops --- vms/saevm/cchain/tx/export.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index b7b1556b917c..236ed6e4d957 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -128,8 +128,8 @@ func (e *Export) atomicRequests(txID ids.ID) (ids.ID, *chainsatomic.Requests, er Key: utxoID[:], Value: utxoBytes, } - if out, ok := utxo.Out.(avax.Addressable); ok { - elem.Traits = out.Addresses() + if o, ok := utxo.Out.(avax.Addressable); ok { + elem.Traits = o.Addresses() } elems[i] = elem From 904a6549c48e325c17c31d7d87d644f6dd097380 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 May 2026 13:41:10 -0400 Subject: [PATCH 110/120] nit --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 6b91203d24c5..90ccc982e6b0 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -187,7 +187,7 @@ func gasPrice(cost uint64, gas gas.Gas) uint256.Int { // AtomicRequests returns shared-memory modifications that this transaction // should perform on the peer chainID during execution. -func (t *Tx) AtomicRequests() (ids.ID, *chainsatomic.Requests, error) { +func (t *Tx) AtomicRequests() (chainID ids.ID, r *chainsatomic.Requests, err error) { return t.atomicRequests(t.ID()) } From a24de254a517ed5251eeb82eadfea77d06c71003 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 May 2026 13:41:44 -0400 Subject: [PATCH 111/120] nit --- vms/saevm/cchain/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 90ccc982e6b0..95b47a1bc427 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -53,7 +53,7 @@ type Unsigned interface { // atomicRequests returns the operations that should be applied to shared // memory when this transaction is executed. - atomicRequests(txID ids.ID) (chainID ids.ID, requests *chainsatomic.Requests, err error) + atomicRequests(txID ids.ID) (chainID ids.ID, r *chainsatomic.Requests, err error) } // op contains the state changes of [hook.Op] From c4b9ef4d5ed43a391077faad77c712f61196cb0a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 May 2026 14:10:53 -0400 Subject: [PATCH 112/120] nit --- vms/saevm/cchain/tx/compatibility_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index fa96cf4b79a0..73ab7611021b 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -211,8 +211,8 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { require.NoError(t, newTx.TransferNonAVAX(AVAXAssetID, newState)) require.NoError(t, op.ApplyTo(newState.StateDB)) - // We must manually finalize the trie structures before comparison. - // Otherwise, comparing the state DBs wouldn't include the changes. + // Finalize the trie structures so that the state DB comparison includes + // the changes. for _, state := range states { state.Finalise(true) state.IntermediateRoot(true) From ba626f816ad5b1980c821077f6bfcb539932b72e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 3 May 2026 11:25:02 -0400 Subject: [PATCH 113/120] wip --- vms/saevm/cchain/tx/export.go | 1 + vms/saevm/cchain/tx/import.go | 1 + 2 files changed, 2 insertions(+) diff --git a/vms/saevm/cchain/tx/export.go b/vms/saevm/cchain/tx/export.go index f51f05772f40..8a06072682ec 100644 --- a/vms/saevm/cchain/tx/export.go +++ b/vms/saevm/cchain/tx/export.go @@ -141,6 +141,7 @@ func (e *Export) atomicRequests(txID ids.ID) (ids.ID, *chainsatomic.Requests, er var errInsufficientFunds = errors.New("insufficient funds") +// TransferNonAVAX subtracts the non-AVAX balances from the statedb. func (e *Export) TransferNonAVAX(avaxAssetID ids.ID, statedb *extstate.StateDB) error { for _, in := range e.Ins { if in.AssetID == avaxAssetID { diff --git a/vms/saevm/cchain/tx/import.go b/vms/saevm/cchain/tx/import.go index ac0056d9f925..ff1a0310465a 100644 --- a/vms/saevm/cchain/tx/import.go +++ b/vms/saevm/cchain/tx/import.go @@ -124,6 +124,7 @@ func (i *Import) atomicRequests(ids.ID) (ids.ID, *chainsatomic.Requests, error) return i.SourceChain, &chainsatomic.Requests{RemoveRequests: utxoIDs}, nil } +// TransferNonAVAX adds the non-AVAX balances to the statedb. func (i *Import) TransferNonAVAX(avaxAssetID ids.ID, statedb *extstate.StateDB) error { for _, out := range i.Outs { if out.AssetID == avaxAssetID { From 66ed39efcb3f6b8875d5cc647996d2f7383dc768 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 May 2026 10:52:18 -0400 Subject: [PATCH 114/120] nit expand test --- vms/saevm/cchain/tx/tx_test.go | 186 ++++++++++++++++++++++++++++++--- 1 file changed, 170 insertions(+), 16 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index bf148f5e966f..9999ce3fc8d7 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -508,7 +508,27 @@ var ( Nonce: 5, }, }, - ExportedOutputs: []*avax.TransferableOutput{}, + ExportedOutputs: []*avax.TransferableOutput{ + { + Out: &secp256k1fx.TransferOutput{ + Amt: 100, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{{0xaa}}, + }, + }, + }, + { + Asset: avax.Asset{ID: AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 100_000, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{{0xaa}}, + }, + }, + }, + }, }, Creds: []verify.Verifiable{}, }, @@ -525,7 +545,27 @@ var ( Nonce: 5, }, }, - ExportedOutputs: []*avax.TransferableOutput{}, + ExportedOutputs: []*avax.TransferableOutput{ + { + Out: &secp256k1fx.TransferOutput{ + Amt: 100, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{{0xaa}}, + }, + }, + }, + { + Asset: avax.Asset{ID: AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 100_000, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{{0xaa}}, + }, + }, + }, + }, }, Creds: []Credential{}, }, @@ -538,15 +578,36 @@ var ( {"address":"0x0000000000000000000000000000000000000000","amount":999,"assetID":"11111111111111111111111111111111LpoYY","nonce":5}, {"address":"0x0000000000000000000000000000000000000000","amount":1000000,"assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z","nonce":5} ], - "exportedOutputs":[] + "exportedOutputs":[ + { + "assetID":"11111111111111111111111111111111LpoYY", + "fxID":"11111111111111111111111111111111LpoYY", + "output":{ + "addresses":["GVsscSys19nXbNEJi5g1Z1y8UawXee8gj"], + "amount":100, + "locktime":0, + "threshold":1 + } + }, + { + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID":"11111111111111111111111111111111LpoYY", + "output":{ + "addresses":["GVsscSys19nXbNEJi5g1Z1y8UawXee8gj"], + "amount":100000, + "locktime":0, + "threshold":1 + } + } + ] }, "credentials":[] }`, - Bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000050000000000000000"), + Bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000000000000500000002000000000000000000000000000000000000000000000000000000000000000000000007000000000000006400000000000000000000000100000001aa0000000000000000000000000000000000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000700000000000186a000000000000000000000000100000001aa0000000000000000000000000000000000000000000000"), Op: hook.Op{ - ID: ids.FromStringOrPanic("29cCETWxEUN1QCuex59j46Xtr8urBRo5M7HzwBqC3qDXWd73sX"), - Gas: 12218, - GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 12218), + ID: ids.FromStringOrPanic("dLYGLJkvGarYPHnfRqK8zH9nu6dj6Ajf1Wjtm8X7fxr5jvvL7"), + Gas: 12378, + GasFeeCap: *uint256.NewInt(900_000 * _x2cRate / 12378), Burn: map[common.Address]hook.AccountDebit{ {}: { Nonce: 5, @@ -556,7 +617,22 @@ var ( }, }, AtomicRequests: &chainsatomic.Requests{ - PutRequests: []*chainsatomic.Element{}, + PutRequests: []*chainsatomic.Element{ + { + Key: common.FromHex("0x82c024362a71c075ac15e5e000dd66380907e3ea6af121d3d78478bb07848b75"), + Value: common.FromHex("0x00005281e076436407df7c97e5abaff7f63e11bd8bc9ce03c787f12ee0e21fe68dca00000000000000000000000000000000000000000000000000000000000000000000000000000007000000000000006400000000000000000000000100000001aa00000000000000000000000000000000000000"), + Traits: [][]byte{ + ids.ShortID{0xaa}.Bytes(), + }, + }, + { + Key: common.FromHex("0x836913bdcf743940c51675ac186dc415cbb5f2a7309916f4a48bda3df5334245"), + Value: common.FromHex("0x00005281e076436407df7c97e5abaff7f63e11bd8bc9ce03c787f12ee0e21fe68dca0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000700000000000186a000000000000000000000000100000001aa00000000000000000000000000000000000000"), + Traits: [][]byte{ + ids.ShortID{0xaa}.Bytes(), + }, + }, + }, }, }, { @@ -576,7 +652,27 @@ var ( Nonce: 7, }, }, - ExportedOutputs: []*avax.TransferableOutput{}, + ExportedOutputs: []*avax.TransferableOutput{ + { + Out: &secp256k1fx.TransferOutput{ + Amt: 500, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 2, + Addrs: []ids.ShortID{{0xbb}, {0xcc}}, + }, + }, + }, + { + Asset: avax.Asset{ID: AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 500_000, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 2, + Addrs: []ids.ShortID{{0xbb}, {0xcc}}, + }, + }, + }, + }, }, Creds: []verify.Verifiable{}, }, @@ -595,7 +691,27 @@ var ( Nonce: 7, }, }, - ExportedOutputs: []*avax.TransferableOutput{}, + ExportedOutputs: []*avax.TransferableOutput{ + { + Out: &secp256k1fx.TransferOutput{ + Amt: 500, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 2, + Addrs: []ids.ShortID{{0xbb}, {0xcc}}, + }, + }, + }, + { + Asset: avax.Asset{ID: AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 500_000, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 2, + Addrs: []ids.ShortID{{0xbb}, {0xcc}}, + }, + }, + }, + }, }, Creds: []Credential{}, }, @@ -608,15 +724,36 @@ var ( {"address":"0x0100000000000000000000000000000000000000","amount":999,"assetID":"11111111111111111111111111111111LpoYY","nonce":5}, {"address":"0x0200000000000000000000000000000000000000","amount":1000000,"assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z","nonce":7} ], - "exportedOutputs":[] + "exportedOutputs":[ + { + "assetID":"11111111111111111111111111111111LpoYY", + "fxID":"11111111111111111111111111111111LpoYY", + "output":{ + "addresses":["J3mMsbNx1AfUrQMSHBwWcDfYRYY1i7rGE","Kber8jn31BYS7SUZrJD1fRMxNW8MvZnhY"], + "amount":500, + "locktime":0, + "threshold":2 + } + }, + { + "assetID":"FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID":"11111111111111111111111111111111LpoYY", + "output":{ + "addresses":["J3mMsbNx1AfUrQMSHBwWcDfYRYY1i7rGE","Kber8jn31BYS7SUZrJD1fRMxNW8MvZnhY"], + "amount":500000, + "locktime":0, + "threshold":2 + } + } + ] }, "credentials":[] }`, - Bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002010000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005020000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000070000000000000000"), + Bytes: common.FromHex("0x000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002010000000000000000000000000000000000000000000000000003e700000000000000000000000000000000000000000000000000000000000000000000000000000005020000000000000000000000000000000000000000000000000f424021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000000000000070000000200000000000000000000000000000000000000000000000000000000000000000000000700000000000001f400000000000000000000000200000002bb00000000000000000000000000000000000000cc0000000000000000000000000000000000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000007a12000000000000000000000000200000002bb00000000000000000000000000000000000000cc0000000000000000000000000000000000000000000000"), Op: hook.Op{ - ID: ids.FromStringOrPanic("8P9XRKhxHeTv3t4Aj9cTV6dD5h78WVFH8nctLuCkeSavfKeEG"), - Gas: 12218, - GasFeeCap: *uint256.NewInt(1_000_000 * _x2cRate / 12218), + ID: ids.FromStringOrPanic("2cfgJ1XjwjNVvF4ZoW86Sc77z7TDMyGB33edRioEfuLkyKkkob"), + Gas: 12418, + GasFeeCap: *uint256.NewInt(500_000 * _x2cRate / 12418), Burn: map[common.Address]hook.AccountDebit{ {1}: { Nonce: 5, @@ -629,7 +766,24 @@ var ( }, }, AtomicRequests: &chainsatomic.Requests{ - PutRequests: []*chainsatomic.Element{}, + PutRequests: []*chainsatomic.Element{ + { + Key: common.FromHex("0x150b950ce35ef7c512b1dec725164c8ee170728976ca0c9c1202eb60cafe9230"), + Value: common.FromHex("0x0000d4ae9ab4d296ced15beba7686a2216bcfdf4a7f1bcc502b3e5c0c79a4601b2ef0000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000001f400000000000000000000000200000002bb00000000000000000000000000000000000000cc00000000000000000000000000000000000000"), + Traits: [][]byte{ + ids.ShortID{0xbb}.Bytes(), + ids.ShortID{0xcc}.Bytes(), + }, + }, + { + Key: common.FromHex("0x3080252925d9e3e399292cfecc8f95bb03da84645f44b8d0da82dc472e1f41d2"), + Value: common.FromHex("0x0000d4ae9ab4d296ced15beba7686a2216bcfdf4a7f1bcc502b3e5c0c79a4601b2ef0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000007a12000000000000000000000000200000002bb00000000000000000000000000000000000000cc00000000000000000000000000000000000000"), + Traits: [][]byte{ + ids.ShortID{0xbb}.Bytes(), + ids.ShortID{0xcc}.Bytes(), + }, + }, + }, }, }, { From fcc28ad2c08c1e11c7e55d281c27c1496b460ead Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 May 2026 12:51:37 -0400 Subject: [PATCH 115/120] reduce diff --- go.work.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.work.sum b/go.work.sum index 90ef1df24ed1..871d7cc0ec45 100644 --- a/go.work.sum +++ b/go.work.sum @@ -572,7 +572,6 @@ github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/Q github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -720,7 +719,6 @@ github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWe github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= From 6feb25b8644bdd089f9445278a9982590a43b07a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 May 2026 14:47:55 -0400 Subject: [PATCH 116/120] kiss --- vms/saevm/cchain/tx/tx_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 53b8280348d9..45b921e99b16 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -25,8 +25,8 @@ import ( _ "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/vm" "github.com/ava-labs/avalanchego/graft/coreth/core/extstate" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" - "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" @@ -39,7 +39,7 @@ import ( ) func TestMain(m *testing.M) { - customtypes.Register() + evm.RegisterAllLibEVMExtras() os.Exit(m.Run()) } From 7ec149a1e5f9a0918c7b3a14d270420f4b45fc25 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 May 2026 14:55:49 -0400 Subject: [PATCH 117/120] bazel --- vms/saevm/cchain/tx/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/saevm/cchain/tx/BUILD.bazel b/vms/saevm/cchain/tx/BUILD.bazel index c42fd4105d4c..6394885a8644 100644 --- a/vms/saevm/cchain/tx/BUILD.bazel +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -46,9 +46,9 @@ go_test( deps = [ "//chains/atomic", "//graft/coreth/core/extstate", + "//graft/coreth/plugin/evm", "//graft/coreth/plugin/evm/atomic", "//graft/coreth/plugin/evm/atomic/vm", - "//graft/coreth/plugin/evm/customtypes", "//ids", "//snow", "//utils/math", From 517e895653a35ca2d45234ccfa7a7c993c3b2233 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 May 2026 15:31:38 -0400 Subject: [PATCH 118/120] test full state --- vms/saevm/cchain/tx/compatibility_test.go | 14 +------ vms/saevm/cchain/tx/tx_test.go | 49 ++++++++++++++++------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 73ab7611021b..0d4862d85823 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -22,7 +22,6 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx/txtest" - "github.com/ava-labs/avalanchego/vms/saevm/cmputils" "github.com/ava-labs/avalanchego/vms/saevm/hook" . "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx" @@ -211,18 +210,7 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { require.NoError(t, newTx.TransferNonAVAX(AVAXAssetID, newState)) require.NoError(t, op.ApplyTo(newState.StateDB)) - // Finalize the trie structures so that the state DB comparison includes - // the changes. - for _, state := range states { - state.Finalise(true) - state.IntermediateRoot(true) - } - - opts := []cmp.Option{ - cmpopts.IgnoreUnexported(extstate.StateDB{}), - cmputils.StateDBs(), - } - if diff := cmp.Diff(oldState, newState, opts...); diff != "" { + if diff := CompareStateDBs(oldState, newState); diff != "" { t.Errorf("%T.TransferNonAVAX() diff (-want +got):\n%s", newTx, diff) } }) diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 45b921e99b16..7675723a95c8 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1332,6 +1332,21 @@ func NewEmptyStateDB(t testing.TB) *extstate.StateDB { return extstate.New(sdb) } +func CompareStateDBs(want, got *extstate.StateDB) string { + // Finalize the trie structures so that the state DB comparison includes + // any changes. + for _, v := range []*extstate.StateDB{want, got} { + v.Finalise(true) + v.IntermediateRoot(true) + } + + opts := []cmp.Option{ + cmpopts.IgnoreUnexported(extstate.StateDB{}), + cmputils.StateDBs(), + } + return cmp.Diff(want, got, opts...) +} + func TestTransferNonAVAX(t *testing.T) { var ( alice = common.Address{1} @@ -1461,13 +1476,18 @@ func TestTransferNonAVAX(t *testing.T) { name: "export_non_avax_total_insufficient", init: map[common.Address]map[ids.ID]uint64{ alice: { - btc: 1, + btc: 2, }, }, tx: &Export{ Ins: []Input{ {Address: alice, Amount: 1, AssetID: btc}, - {Address: alice, Amount: 1, AssetID: btc}, + {Address: alice, Amount: 2, AssetID: btc}, + }, + }, + want: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 1, }, }, wantErr: errInsufficientFunds, @@ -1476,27 +1496,28 @@ func TestTransferNonAVAX(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var ( - state = NewEmptyStateDB(t) + want = NewEmptyStateDB(t) toBig = func(v uint64) *big.Int { return new(big.Int).SetUint64(v) } ) + for addr, balances := range test.want { + for assetID, amount := range balances { + coinID := common.Hash(assetID) + want.AddBalanceMultiCoin(addr, coinID, toBig(amount)) + } + } + + got := NewEmptyStateDB(t) for addr, balances := range test.init { for assetID, amount := range balances { coinID := common.Hash(assetID) - state.AddBalanceMultiCoin(addr, coinID, toBig(amount)) + got.AddBalanceMultiCoin(addr, coinID, toBig(amount)) } } - err := test.tx.TransferNonAVAX(AVAXAssetID, state) + err := test.tx.TransferNonAVAX(AVAXAssetID, got) require.ErrorIs(t, err, test.wantErr) - for _, addr := range []common.Address{alice, bob} { - for _, asset := range []ids.ID{AVAXAssetID, btc, eth} { - want := toBig(test.want[addr][asset]) - coinID := common.Hash(asset) - got := state.GetBalanceMultiCoin(addr, coinID) - if diff := cmp.Diff(want, got, cmputils.BigInts()); diff != "" { - t.Errorf("%T.GetBalanceMultiCoin(%s, %s) diff (-want +got):\n%s", state, addr, coinID, diff) - } - } + if diff := CompareStateDBs(want, got); diff != "" { + t.Errorf("%T.TransferNonAVAX() diff (-want +got):\n%s", test.tx, diff) } }) } From 03c4e8fe27f755d5fd3415fa956b03a8a4937de3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 May 2026 15:47:45 -0400 Subject: [PATCH 119/120] nits --- vms/saevm/cchain/tx/compatibility_test.go | 6 +++--- vms/saevm/cchain/tx/tx.go | 2 ++ vms/saevm/cchain/tx/tx_test.go | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/vms/saevm/cchain/tx/compatibility_test.go b/vms/saevm/cchain/tx/compatibility_test.go index 0d4862d85823..72ed00dc6105 100644 --- a/vms/saevm/cchain/tx/compatibility_test.go +++ b/vms/saevm/cchain/tx/compatibility_test.go @@ -206,9 +206,9 @@ func FuzzTransferNonAVAXCompatibility(f *testing.F) { oldTx = ToOldTx(t, newTx) ctx = &snow.Context{AVAXAssetID: AVAXAssetID} ) - require.NoError(t, oldTx.UnsignedAtomicTx.EVMStateTransfer(ctx, oldState)) - require.NoError(t, newTx.TransferNonAVAX(AVAXAssetID, newState)) - require.NoError(t, op.ApplyTo(newState.StateDB)) + require.NoErrorf(t, oldTx.EVMStateTransfer(ctx, oldState), "%T.EVMStateTransfer()", oldTx) + require.NoErrorf(t, newTx.TransferNonAVAX(AVAXAssetID, newState), "%T.TransferNonAVAX()", newTx) + require.NoErrorf(t, op.ApplyTo(newState.StateDB), "%T.ApplyTo(%T)", op, newState.StateDB) if diff := CompareStateDBs(oldState, newState); diff != "" { t.Errorf("%T.TransferNonAVAX() diff (-want +got):\n%s", newTx, diff) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 4627439505cc..3c5cc70261f5 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -42,6 +42,8 @@ type Tx struct { type Unsigned interface { // TransferNonAVAX transfers the non-AVAX balances requested by this // transaction. + // + // Non-AVAX transfers were only allowed prior to the Banff upgrade. TransferNonAVAX(avaxAssetID ids.ID, statedb *extstate.StateDB) error // burned returns the amount of assetID that is consumed but not produced by diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go index 7675723a95c8..411af6ff298e 100644 --- a/vms/saevm/cchain/tx/tx_test.go +++ b/vms/saevm/cchain/tx/tx_test.go @@ -1382,6 +1382,24 @@ func TestTransferNonAVAX(t *testing.T) { }, }, }, + { + name: "import_non_avax_adds", + init: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 1, + }, + }, + tx: &Import{ + Outs: []Output{ + {Address: alice, Amount: 1, AssetID: btc}, + }, + }, + want: map[common.Address]map[ids.ID]uint64{ + alice: { + btc: 2, + }, + }, + }, { name: "import_many", tx: &Import{ From da34043818d26cfa7f748effe0a8ba6b98b6f7a5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 May 2026 15:49:42 -0400 Subject: [PATCH 120/120] nit --- vms/saevm/cchain/tx/tx.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/saevm/cchain/tx/tx.go b/vms/saevm/cchain/tx/tx.go index 3c5cc70261f5..ec228ddbb837 100644 --- a/vms/saevm/cchain/tx/tx.go +++ b/vms/saevm/cchain/tx/tx.go @@ -54,8 +54,8 @@ type Unsigned interface { // transaction. numSigs() (uint64, error) - // asOp returns the operation that this transaction performs on the EVM - // state. + // asOp returns the operation that this transaction performs on the + // EVM-native state. Ops do not include any non-AVAX balance changes. asOp(avaxAssetID ids.ID) (op, error) // atomicRequests returns the operations that should be applied to shared