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 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", ], 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/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 new file mode 100644 index 000000000000..c7bfca07dc28 --- /dev/null +++ b/vms/saevm/cchain/tx/BUILD.bazel @@ -0,0 +1,48 @@ +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 = [ + "codec.go", + "export.go", + "import.go", + "tx.go", + ], + importpath = "github.com/ava-labs/avalanchego/vms/saevm/cchain/tx", + 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..ff1debd64b50 --- /dev/null +++ b/vms/saevm/cchain/tx/codec.go @@ -0,0 +1,75 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tx + +import ( + "errors" + + "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) + } +} + +// 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/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..540438eb010d --- /dev/null +++ b/vms/saevm/cchain/tx/tx.go @@ -0,0 +1,72 @@ +// 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 ( + "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 +} diff --git a/vms/saevm/cchain/tx/tx_test.go b/vms/saevm/cchain/tx/tx_test.go new file mode 100644 index 000000000000..4cc0a3630e3d --- /dev/null +++ b/vms/saevm/cchain/tx/tx_test.go @@ -0,0 +1,510 @@ +// 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 [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" + "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"), + }}, + ExportedOutputs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ + ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + }, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + 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"), + }}, + ExportedOutputs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ + ID: ids.FromStringOrPanic("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z"), + }, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + 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"), + }, + } + 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 + } +} + +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, oldTxs) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldTxs) + + tests := []struct { + name string + txs []*Tx + want []byte + }{ + { + name: "mainnet", + txs: newTxs, + 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, oldTxs) + require.NoErrorf(t, err, "%T.Marshal(, %T)", atomic.Codec, oldTxs) + + tests := []struct { + name string + bytes []byte + want []*Tx + wantErr error + }{ + { + name: "mainnet", + bytes: bytes, + want: newTxs, + }, + { + 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(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) + } + 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) + 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()") + + want, err := json.Marshal(oldTx) + require.NoErrorf(t, err, "json.Marshal(%T)", oldTx) + + got, err := json.Marshal(newTx) + require.NoErrorf(t, err, "json.Marshal(%T)", newTx) + assert.JSONEq(t, string(want), string(got)) + }) +} 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 +}