From f8d3324cd411806c98dd7dc26f413c531e02d5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Tue, 5 May 2026 14:18:01 +0200 Subject: [PATCH 01/17] Add first-pass EVM executor foundation Add shared EVM-native types, global-state keys and stored values, chainspec configuration, and a new casper-executor-evm crate backed by revm. The executor runs against a caller-provided TrackingCopy so callers can decide whether to commit effects or discard a forked view execution. This introduces typed EVM account, bytecode, and storage records so contract state can be queried and pruned by address prefix. EVM balances are backed by deterministic Casper main purses, with balance reads and writes reconciled through existing purse balance storage. Signed Ethereum RLP decoding and sender recovery are added to casper-types for legacy, EIP-2930, and EIP-1559 transactions. Blob transactions and EIP-7702 set-code transactions return explicit unsupported errors in this first pass. The node and storage layers learn the new transaction/hash/initiator shapes without routing EVM transactions through existing Casper transaction execution yet. Chainspecs now include disabled-by-default EVM configuration with CSP namespace chain IDs. Solidity fixtures, Makefile targets, and integration tests cover deployment, calls, ERC20/ERC721 behavior, storage deletion, selfdestruct semantics, and native EVM purse-balance reconciliation. --- Cargo.lock | 1709 +++++++++++++++-- Cargo.toml | 1 + Makefile | 17 + binary_port/src/key_prefix.rs | 15 +- execution_engine/src/engine_state/mod.rs | 8 +- execution_engine/src/runtime_context/mod.rs | 5 +- .../src/transfer_request_builder.rs | 8 +- .../test_support/src/wasm_test_builder.rs | 6 +- .../test/contract_api/add_contract_version.rs | 1 + executor/evm/Cargo.toml | 15 + executor/evm/src/db.rs | 128 ++ executor/evm/src/error.rs | 69 + executor/evm/src/executor.rs | 122 ++ executor/evm/src/lib.rs | 25 + executor/evm/src/outcome.rs | 100 + executor/evm/src/request.rs | 74 + executor/evm/src/state.rs | 136 ++ executor/evm/src/tx.rs | 82 + executor/evm/tests/executor.rs | 531 +++++ node/src/components/binary_port.rs | 1 + .../transaction_acquisition.rs | 3 + .../transaction_acquisition/tests.rs | 11 +- node/src/components/block_validator.rs | 1 + node/src/components/block_validator/state.rs | 5 +- .../components/contract_runtime/operations.rs | 12 +- .../operations/wasm_v2_request.rs | 13 +- node/src/components/contract_runtime/types.rs | 5 + .../event_stream_server/sse_server.rs | 1 + node/src/components/storage.rs | 21 +- node/src/components/transaction_acceptor.rs | 21 +- .../components/transaction_acceptor/tests.rs | 1 + .../src/types/transaction/meta_transaction.rs | 10 + .../meta_transaction/transaction_header.rs | 3 + .../utils/chain_specification/parse_toml.rs | 13 +- node/src/utils/specimen.rs | 1 + resources/integration-test/chainspec.toml | 10 + resources/local/chainspec.toml.in | 10 + resources/mainnet/chainspec.toml | 10 + resources/production/chainspec.toml | 10 + resources/testnet/chainspec.toml | 10 + smart_contracts/evm_contracts/Counter.sol | 20 + .../evm_contracts/MinimalERC20.sol | 47 + .../evm_contracts/MinimalERC721.sol | 58 + .../evm_contracts/SelfDestruct.sol | 14 + .../evm_contracts/StorageDelete.sol | 14 + .../block_store/lmdb/versioned_databases.rs | 2 + storage/src/data_access_layer/balance.rs | 23 +- storage/src/data_access_layer/key_prefix.rs | 14 + storage/src/global_state/state/mod.rs | 20 +- storage/src/tracking_copy/byte_size.rs | 3 + storage/src/tracking_copy/mod.rs | 9 + types/Cargo.toml | 5 + .../test_block_v2_builder.rs | 1 + types/src/chainspec.rs | 17 +- types/src/evm.rs | 21 + types/src/evm/account.rs | 262 +++ types/src/evm/address.rs | 90 + types/src/evm/config.rs | 206 ++ types/src/evm/hash.rs | 94 + types/src/evm/transaction.rs | 493 +++++ types/src/execution/transform_kind.rs | 15 + types/src/gens.rs | 18 + types/src/key.rs | 175 +- types/src/lib.rs | 1 + types/src/stored_value.rs | 76 + types/src/transaction.rs | 89 +- types/src/transaction/deploy.rs | 2 +- types/src/transaction/error.rs | 12 +- types/src/transaction/initiator_addr.rs | 65 +- types/src/transaction/transaction_hash.rs | 32 +- types/tests/evm_transaction.rs | 113 ++ 71 files changed, 5054 insertions(+), 181 deletions(-) create mode 100644 executor/evm/Cargo.toml create mode 100644 executor/evm/src/db.rs create mode 100644 executor/evm/src/error.rs create mode 100644 executor/evm/src/executor.rs create mode 100644 executor/evm/src/lib.rs create mode 100644 executor/evm/src/outcome.rs create mode 100644 executor/evm/src/request.rs create mode 100644 executor/evm/src/state.rs create mode 100644 executor/evm/src/tx.rs create mode 100644 executor/evm/tests/executor.rs create mode 100644 smart_contracts/evm_contracts/Counter.sol create mode 100644 smart_contracts/evm_contracts/MinimalERC20.sol create mode 100644 smart_contracts/evm_contracts/MinimalERC721.sol create mode 100644 smart_contracts/evm_contracts/SelfDestruct.sol create mode 100644 smart_contracts/evm_contracts/StorageDelete.sol create mode 100644 types/src/evm.rs create mode 100644 types/src/evm/account.rs create mode 100644 types/src/evm/address.rs create mode 100644 types/src/evm/config.rs create mode 100644 types/src/evm/hash.rs create mode 100644 types/src/evm/transaction.rs create mode 100644 types/tests/evm_transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 4fdc26be77..27e2602ea2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,176 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alloy-consensus" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3b746060277f3d7f9c36903bb39b593a741cb7afcb0044164c28f0e9b673f0" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "derive_more 2.0.1", + "either", + "k256", + "once_cell", + "thiserror 2.0.12", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "k256", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "alloy-eips" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f562a81278a3ed83290e68361f2d1c75d018ae3b8589a314faf9303883e18ec9" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "c-kzg", + "derive_more 2.0.1", + "either", + "serde", + "sha2 0.10.9", +] + +[[package]] +name = "alloy-primitives" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a326d47106039f38b811057215a92139f46eef7983a4b77b10930a0ea5685b1e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if 1.0.0", + "const-hex", + "derive_more 2.0.1", + "foldhash", + "hashbrown 0.15.3", + "indexmap 2.9.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.4", + "ruint", + "rustc-hash 2.1.2", + "serde", + "sha3", + "tiny-keccak", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" +dependencies = [ + "alloy-rlp-derive", + "arrayvec 0.7.6", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36834a5c0a2fa56e171bf256c34d70fca07d0c0031583edea1c4946b7889c9e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "alloy-serde" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64600fc6c312b7e0ba76f73a381059af044f4f21f43e07f51f1fa76c868fe302" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-trie" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bada1fc392a33665de0dc50d401a3701b62583c655e3522a323490a5da016962" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec 0.7.6", + "derive_more 2.0.1", + "nybbles", + "smallvec", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1154c8187a5ff985c95a8b2daa2fedcf778b17d7668e5e50e556c4ff9c881154" +dependencies = [ + "alloy-primitives", + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "anes" version = "0.1.6" @@ -195,6 +365,296 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-r1cs-std", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-poly", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe 0.6.0", + "fnv", + "hashbrown 0.15.3", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec 0.7.6", + "digest 0.10.7", + "educe 0.6.0", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.101", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe 0.6.0", + "fnv", + "hashbrown 0.15.3", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-relations", + "ark-std 0.5.0", + "educe 0.6.0", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff 0.5.0", + "ark-std 0.5.0", + "tracing", + "tracing-subscriber 0.2.25", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.5.0", + "arrayvec 0.7.6", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -280,8 +740,29 @@ dependencies = [ name = "auction-bids" version = "0.1.0" dependencies = [ - "casper-contract", - "casper-types", + "casper-contract", + "casper-types", +] + +[[package]] +name = "aurora-engine-modexp" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518bc5745a6264b5fd7b09dffb9667e400ee9e2bbe18555fac75e1fe9afa0df9" +dependencies = [ + "hex", + "num", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -290,6 +771,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + [[package]] name = "backtrace" version = "0.3.74" @@ -365,7 +852,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.101", ] @@ -385,6 +872,22 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -396,6 +899,22 @@ name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] [[package]] name = "blake2" @@ -448,6 +967,15 @@ dependencies = [ "constant_time_eq 0.3.1", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -457,6 +985,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob 0.3.2", + "threadpool", + "zeroize", +] + [[package]] name = "bnum" version = "0.13.0" @@ -526,6 +1066,12 @@ dependencies = [ "casper-types", ] +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytecheck" version = "0.6.12" @@ -588,6 +1134,24 @@ name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "c-kzg" +version = "2.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" +dependencies = [ + "blst", + "cc", + "glob 0.3.2", + "hex", + "libc", + "once_cell", + "serde", +] [[package]] name = "call-contract" @@ -660,7 +1224,7 @@ checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.26", "serde", "serde_json", "thiserror 1.0.69", @@ -674,7 +1238,7 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.26", "serde", "serde_json", "thiserror 2.0.12", @@ -690,7 +1254,7 @@ dependencies = [ "num-derive", "num-traits", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "serde_test", @@ -744,7 +1308,7 @@ dependencies = [ "impl-trait-for-tuples", "linkme", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "thiserror 2.0.12", @@ -785,7 +1349,7 @@ dependencies = [ "num-rational", "num-traits", "once_cell", - "rand", + "rand 0.8.5", "serde", "tempfile", "toml 0.5.11", @@ -818,7 +1382,7 @@ dependencies = [ "num-rational", "num-traits", "once_cell", - "rand", + "rand 0.8.5", "regex", "serde", "serde_json", @@ -861,13 +1425,13 @@ dependencies = [ "num_cpus", "once_cell", "proptest", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "schemars", "serde", "serde_bytes", "serde_json", - "sha2", + "sha2 0.10.9", "strum 0.24.1", "tempfile", "thiserror 1.0.69", @@ -878,6 +1442,16 @@ dependencies = [ "wat", ] +[[package]] +name = "casper-executor-evm" +version = "0.1.0" +dependencies = [ + "casper-storage", + "casper-types", + "revm", + "thiserror 2.0.12", +] + [[package]] name = "casper-executor-wasm" version = "0.1.3" @@ -1030,9 +1604,9 @@ dependencies = [ "proptest", "proptest-derive", "quanta", - "rand", - "rand_chacha", - "rand_core", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", "regex", "reqwest", "rmp", @@ -1064,7 +1638,7 @@ dependencies = [ "tower", "tracing", "tracing-futures", - "tracing-subscriber", + "tracing-subscriber 0.3.20", "uint", "uuid 0.8.2", "warp", @@ -1094,8 +1668,8 @@ dependencies = [ "parking_lot", "pprof", "proptest", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_json", "tempfile", @@ -1108,6 +1682,10 @@ dependencies = [ name = "casper-types" version = "7.0.0" dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-tx-macros", "base16", "base64 0.13.1", "bincode", @@ -1120,6 +1698,7 @@ dependencies = [ "ed25519-dalek", "getrandom 0.2.16", "hex", + "hex-literal", "hex_fmt", "humantime", "itertools 0.10.5", @@ -1136,7 +1715,7 @@ dependencies = [ "proptest", "proptest-attr-macro", "proptest-derive", - "rand", + "rand 0.8.5", "rand_pcg", "schemars", "serde", @@ -1161,7 +1740,7 @@ dependencies = [ "clap 4.5.38", "once_cell", "regex", - "semver", + "semver 1.0.26", ] [[package]] @@ -1453,12 +2032,45 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" +[[package]] +name = "const-hex" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1597,6 +2209,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.4.2" @@ -1770,7 +2397,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1816,7 +2443,7 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "rustc_version", + "rustc_version 0.4.1", "subtle", "zeroize", ] @@ -1938,6 +2565,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1947,7 +2596,7 @@ dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "syn 2.0.101", ] @@ -1970,6 +2619,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.101", + "unicode-xid", ] [[package]] @@ -2043,7 +2693,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -2166,6 +2816,7 @@ dependencies = [ "elliptic-curve", "rfc6979", "signature 2.2.0", + "spki", ] [[package]] @@ -2187,7 +2838,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -2198,12 +2849,24 @@ version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" dependencies = [ - "enum-ordinalize", + "enum-ordinalize 3.1.15", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize 4.3.2", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "ee-1071-regression" version = "0.1.0" @@ -2404,7 +3067,8 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core", + "pkcs8", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -2496,6 +3160,26 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "enumset" version = "1.1.6" @@ -2607,6 +3291,28 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec 0.7.6", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec 0.7.6", + "auto_impl", + "bytes", +] + [[package]] name = "faucet" version = "0.1.0" @@ -2630,7 +3336,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2681,6 +3387,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + [[package]] name = "flate2" version = "1.1.1" @@ -2697,6 +3415,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -2737,6 +3461,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -3085,11 +3815,21 @@ dependencies = [ "clap 2.34.0", "itertools 0.10.5", "lmdb-rkv", - "rand", + "rand 0.8.5", "serde", "toml 0.5.11", ] +[[package]] +name = "gmp-mpfr-sys" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db155b537cb791b133341f99f68371d86ee7fa4c79aacfbc376d72d23c70531" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "group" version = "0.13.0" @@ -3097,7 +3837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3158,6 +3898,11 @@ name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "foldhash", + "serde", +] [[package]] name = "hashbrown" @@ -3268,6 +4013,21 @@ dependencies = [ "serde", ] +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec 0.7.6", +] + +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + [[package]] name = "hex_fmt" version = "0.3.0" @@ -3297,7 +4057,7 @@ version = "0.1.0" dependencies = [ "casper-contract", "casper-types", - "rand", + "rand 0.8.5", ] [[package]] @@ -3518,6 +4278,15 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.3" @@ -3581,6 +4350,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.3", + "serde", ] [[package]] @@ -3694,7 +4464,27 @@ dependencies = [ "cfg-if 1.0.0", "ecdsa", "elliptic-curve", - "sha2", + "once_cell", + "sha2 0.10.9", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-asm" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa468878266ad91431012b3e5ef1bf9b170eab22883503a318d46857afa4579a" +dependencies = [ + "digest 0.10.7", + "sha3-asm", ] [[package]] @@ -3713,6 +4503,21 @@ dependencies = [ "casper-types", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy_static" version = "1.5.0" @@ -3764,6 +4569,52 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libsecp256k1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" +dependencies = [ + "arrayref", + "base64 0.22.1", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", + "sha2 0.9.9", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -4216,7 +5067,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4312,6 +5163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4324,6 +5176,39 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "nybbles" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63cb50036b1ad148038105af40aaa70ff24d8a14fbc44ae5c914e1348533d12e" +dependencies = [ + "cfg-if 1.0.0", + "ruint", + "smallvec", +] + [[package]] name = "object" version = "0.32.2" @@ -4450,6 +5335,46 @@ dependencies = [ "casper-types", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec 0.7.6", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -4504,6 +5429,59 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -4731,6 +5709,26 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -4820,8 +5818,8 @@ dependencies = [ "bitflags 2.9.1", "lazy_static", "num-traits", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -4980,6 +5978,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rancor" version = "0.1.0" @@ -4996,8 +6000,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", ] [[package]] @@ -5007,7 +6022,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -5019,13 +6044,23 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.3", + "serde", +] + [[package]] name = "rand_pcg" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -5034,7 +6069,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -5280,99 +6315,289 @@ dependencies = [ name = "regression_20211110" version = "0.1.0" dependencies = [ - "casper-contract", - "casper-types", + "casper-contract", + "casper-types", +] + +[[package]] +name = "regression_20220119" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-types", +] + +[[package]] +name = "regression_20240105" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-types", +] + +[[package]] +name = "remove-associated-key" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-types", +] + +[[package]] +name = "rend" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" +dependencies = [ + "bytecheck 0.8.1", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util 0.7.15", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + +[[package]] +name = "ret-uref" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-types", +] + +[[package]] +name = "revert" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-types", +] + +[[package]] +name = "revm" +version = "27.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6bf82101a1ad8a2b637363a37aef27f88b4efc8a6e24c72bf5f64923dc5532" +dependencies = [ + "revm-bytecode", + "revm-context", + "revm-context-interface", + "revm-database", + "revm-database-interface", + "revm-handler", + "revm-inspector", + "revm-interpreter", + "revm-precompile", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-bytecode" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6922f7f4fbc15ca61ea459711ff75281cc875648c797088c34e4e064de8b8a7c" +dependencies = [ + "bitvec", + "once_cell", + "phf", + "revm-primitives", + "serde", +] + +[[package]] +name = "revm-context" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd508416a35a4d8a9feaf5ccd06ac6d6661cd31ee2dc0252f9f7316455d71f9" +dependencies = [ + "cfg-if 1.0.0", + "derive-where", + "revm-bytecode", + "revm-context-interface", + "revm-database-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-context-interface" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc90302642d21c8f93e0876e201f3c5f7913c4fcb66fb465b0fd7b707dfe1c79" +dependencies = [ + "alloy-eip2930", + "alloy-eip7702", + "auto_impl", + "either", + "revm-database-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-database" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61495e01f01c343dd90e5cb41f406c7081a360e3506acf1be0fc7880bfb04eb" +dependencies = [ + "alloy-eips", + "revm-bytecode", + "revm-database-interface", + "revm-primitives", + "revm-state", + "serde", ] [[package]] -name = "regression_20220119" -version = "0.1.0" +name = "revm-database-interface" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20628d6cd62961a05f981230746c16854f903762d01937f13244716530bf98f" dependencies = [ - "casper-contract", - "casper-types", + "auto_impl", + "either", + "revm-primitives", + "revm-state", + "serde", ] [[package]] -name = "regression_20240105" -version = "0.1.0" +name = "revm-handler" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1529c8050e663be64010e80ec92bf480315d21b1f2dbf65540028653a621b27d" dependencies = [ - "casper-contract", - "casper-types", + "auto_impl", + "derive-where", + "revm-bytecode", + "revm-context", + "revm-context-interface", + "revm-database-interface", + "revm-interpreter", + "revm-precompile", + "revm-primitives", + "revm-state", + "serde", ] [[package]] -name = "remove-associated-key" -version = "0.1.0" +name = "revm-inspector" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78db140e332489094ef314eaeb0bd1849d6d01172c113ab0eb6ea8ab9372926" dependencies = [ - "casper-contract", - "casper-types", + "auto_impl", + "either", + "revm-context", + "revm-database-interface", + "revm-handler", + "revm-interpreter", + "revm-primitives", + "revm-state", + "serde", + "serde_json", ] [[package]] -name = "rend" -version = "0.5.2" +name = "revm-interpreter" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" +checksum = "ff9d7d9d71e8a33740b277b602165b6e3d25fff091ba3d7b5a8d373bf55f28a7" dependencies = [ - "bytecheck 0.8.1", + "revm-bytecode", + "revm-context-interface", + "revm-primitives", + "serde", ] [[package]] -name = "reqwest" -version = "0.11.27" +name = "revm-precompile" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "4cee3f336b83621294b4cfe84d817e3eef6f3d0fce00951973364cc7f860424d" dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "arrayref", + "aurora-engine-modexp", + "blst", + "c-kzg", + "cfg-if 1.0.0", + "k256", + "libsecp256k1", "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tokio-util 0.7.15", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "winreg", + "p256", + "revm-primitives", + "ripemd", + "rug", + "secp256k1", + "sha2 0.10.9", ] [[package]] -name = "ret-uref" -version = "0.1.0" +name = "revm-primitives" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66145d3dc61c0d6403f27fc0d18e0363bb3b7787e67970a05c71070092896599" dependencies = [ - "casper-contract", - "casper-types", + "alloy-primitives", + "num_enum", + "serde", ] [[package]] -name = "revert" -version = "0.1.0" +name = "revm-state" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc830a0fd2600b91e371598e3d123480cd7bb473dd6def425a51213aa6c6d57" dependencies = [ - "casper-contract", - "casper-types", + "bitflags 2.9.1", + "revm-bytecode", + "revm-primitives", + "serde", ] [[package]] @@ -5408,6 +6633,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "rkyv" version = "0.8.13" @@ -5438,6 +6672,16 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "rmp" version = "0.8.14" @@ -5460,6 +6704,51 @@ dependencies = [ "serde", ] +[[package]] +name = "rug" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07a8857882aec59d27254b02481c709327c13de6fad1da60bfc4f9783eaaa61e" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", + "libm", +] + +[[package]] +name = "ruint" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.5", + "rand 0.9.4", + "rlp", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -5472,13 +6761,34 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.26", ] [[package]] @@ -5643,10 +6953,31 @@ dependencies = [ "base16ct", "der", "generic-array", + "pkcs8", "subtle", "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.4", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -5676,6 +7007,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.26" @@ -5685,12 +7025,22 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -5733,11 +7083,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -5828,6 +7187,19 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -5839,6 +7211,26 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sha3-asm" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cbb88c189d6352cc8ae96a39d19c7ecad8f7330b29461187f2587fdc2988d5" +dependencies = [ + "cc", + "cfg-if 1.0.0", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -5907,7 +7299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -5916,6 +7308,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.9" @@ -6210,6 +7608,12 @@ dependencies = [ "casper-types", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.45" @@ -6338,6 +7742,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -6429,7 +7851,7 @@ checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" dependencies = [ "bincode", "bytes", - "educe", + "educe 0.4.23", "futures-core", "futures-sink", "pin-project", @@ -6665,6 +8087,15 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" @@ -6849,7 +8280,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", "thiserror 1.0.69", "url", @@ -6872,6 +8303,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uint" version = "0.9.5" @@ -6928,6 +8365,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.7.1" @@ -7066,7 +8509,7 @@ dependencies = [ "proc-macro2", "pulldown-cmark", "regex", - "semver", + "semver 1.0.26", "syn 2.0.101", "toml 0.7.8", "url", @@ -7508,7 +8951,7 @@ dependencies = [ "indexmap 2.9.0", "more-asserts", "rkyv", - "sha2", + "sha2 0.10.9", "target-lexicon", "thiserror 1.0.69", "xxhash-rust", @@ -7556,7 +8999,7 @@ dependencies = [ "bitflags 2.9.1", "hashbrown 0.14.5", "indexmap 2.9.0", - "semver", + "semver 1.0.26", ] [[package]] @@ -7577,7 +9020,7 @@ checksum = "808198a69b5a0535583370a51d459baa14261dfab04800c4864ee9e1a14346ed" dependencies = [ "bitflags 2.9.1", "indexmap 2.9.0", - "semver", + "semver 1.0.26", ] [[package]] @@ -7690,6 +9133,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.48.0" @@ -7717,6 +9166,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -7953,6 +9411,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xattr" version = "1.5.0" @@ -8039,6 +9506,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index a1c5e42ee1..bfe0861afa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "executor/wasm_host", "executor/wasmer_backend", "executor/wasm", + "executor/evm", ] default-members = [ diff --git a/Makefile b/Makefile index 79b26f463d..236991923f 100644 --- a/Makefile +++ b/Makefile @@ -16,10 +16,12 @@ DISABLE_LOGGING = RUST_LOG=MatchesNothing VM2_CONTRACTS = $(shell find ./smart_contracts/contracts/vm2 -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) ALL_CONTRACTS = $(shell find ./smart_contracts/contracts/[!.]* -mindepth 1 -maxdepth 1 -not -path "./smart_contracts/contracts/vm2*" -type d -exec basename {} \;) CLIENT_CONTRACTS = $(shell find ./smart_contracts/contracts/client -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) +EVM_CONTRACTS = $(shell find ./smart_contracts/evm_contracts -mindepth 1 -maxdepth 1 -name '*.sol' -exec basename {} .sol \;) CARGO_HOME_REMAP = $(if $(CARGO_HOME),$(CARGO_HOME),$(HOME)/.cargo) RUSTC_FLAGS = "--remap-path-prefix=$(CARGO_HOME_REMAP)=/home/cargo --remap-path-prefix=$$PWD=/dir" CONTRACT_TARGET_DIR = target/wasm32-unknown-unknown/release +EVM_CONTRACT_TARGET_DIR = target/evm-contracts build-contract-rs/%: cd smart_contracts/contracts && RUSTFLAGS=$(RUSTC_FLAGS) $(CARGO) build --verbose --release $(filter-out --release, $(CARGO_FLAGS)) --package $* @@ -55,6 +57,21 @@ build-client-contracts: build-client-contracts-rs strip-client-contracts .PHONY: build-contracts build-contracts: build-contracts-rs +.PHONY: setup-evm +setup-evm: + @command -v solc >/dev/null || (echo "solc is required to build EVM contract fixtures" && exit 1) + +build-contract-evm/%: setup-evm + mkdir -p $(EVM_CONTRACT_TARGET_DIR) + solc --optimize --abi --bin --overwrite -o $(EVM_CONTRACT_TARGET_DIR) smart_contracts/evm_contracts/$*.sol + +.PHONY: build-contracts-evm +build-contracts-evm: $(patsubst %, build-contract-evm/%, $(EVM_CONTRACTS)) + +.PHONY: test-contracts-evm +test-contracts-evm: build-contracts-evm + $(DISABLE_LOGGING) $(CARGO) test $(CARGO_FLAGS) -p casper-executor-evm + resources/local/chainspec.toml: generate-chainspec.sh resources/local/chainspec.toml.in @./$< diff --git a/binary_port/src/key_prefix.rs b/binary_port/src/key_prefix.rs index 774e3a36b0..acf0e04bcc 100644 --- a/binary_port/src/key_prefix.rs +++ b/binary_port/src/key_prefix.rs @@ -4,6 +4,7 @@ use casper_types::{ account::AccountHash, bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, contract_messages::TopicNameHash, + evm::Address as EvmAddress, system::{auction::BidAddrTag, mint::BalanceHoldAddrTag}, EntityAddr, KeyTag, URefAddr, }; @@ -29,13 +30,15 @@ pub enum KeyPrefix { EntryPointsV1ByEntity(EntityAddr), /// Retrieves all V2 entry points for a given entity. EntryPointsV2ByEntity(EntityAddr), + /// Retrieves all EVM storage slots for a given EVM address. + EvmStorageByAddress(EvmAddress), } impl KeyPrefix { /// Returns a random `KeyPrefix`. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..8) { + match rng.gen_range(0..9) { 0 => KeyPrefix::DelegatorBidAddrsByValidator(rng.gen()), 1 => KeyPrefix::MessagesByEntity(rng.gen()), 2 => KeyPrefix::MessagesByEntityAndTopic(rng.gen(), rng.gen()), @@ -44,6 +47,7 @@ impl KeyPrefix { 5 => KeyPrefix::ProcessingBalanceHoldsByPurse(rng.gen()), 6 => KeyPrefix::EntryPointsV1ByEntity(rng.gen()), 7 => KeyPrefix::EntryPointsV2ByEntity(rng.gen()), + 8 => KeyPrefix::EvmStorageByAddress(EvmAddress::new(rng.gen())), _ => unreachable!(), } } @@ -96,6 +100,10 @@ impl ToBytes for KeyPrefix { writer.push(1); entity.write_bytes(writer)?; } + KeyPrefix::EvmStorageByAddress(address) => { + writer.push(KeyTag::EvmStorage as u8); + address.write_bytes(writer)?; + } } Ok(()) } @@ -123,6 +131,7 @@ impl ToBytes for KeyPrefix { KeyPrefix::EntryPointsV2ByEntity(entity) => { U8_SERIALIZED_LENGTH + entity.serialized_length() } + KeyPrefix::EvmStorageByAddress(address) => address.serialized_length(), } } } @@ -182,6 +191,10 @@ impl FromBytes for KeyPrefix { _ => return Err(bytesrepr::Error::Formatting), } } + tag if tag == KeyTag::EvmStorage as u8 => { + let (address, remainder) = EvmAddress::from_bytes(remainder)?; + (KeyPrefix::EvmStorageByAddress(address), remainder) + } _ => return Err(bytesrepr::Error::Formatting), }; Ok(result) diff --git a/execution_engine/src/engine_state/mod.rs b/execution_engine/src/engine_state/mod.rs index b0b201afcb..d279f2ac82 100644 --- a/execution_engine/src/engine_state/mod.rs +++ b/execution_engine/src/engine_state/mod.rs @@ -76,7 +76,9 @@ impl ExecutionEngineV1 { // A good deal of effort has been put into removing all such behaviors; please do not // come along and start adding it back. - let account_hash = initiator_addr.account_hash(); + let account_hash = initiator_addr + .account_hash() + .expect("Wasm v1 initiator must be a Casper account"); let protocol_version = self.config.protocol_version(); let state_hash = block_info.state_hash; let tc = match state_provider.tracking_copy(state_hash) { @@ -155,7 +157,9 @@ impl ExecutionEngineV1 { // A good deal of effort has been put into removing all such behaviors; please do not // come along and start adding it back. - let account_hash = initiator_addr.account_hash(); + let account_hash = initiator_addr + .account_hash() + .expect("Wasm v1 initiator must be a Casper account"); let protocol_version = self.config.protocol_version(); let tc = Rc::new(RefCell::new(tracking_copy)); let (runtime_footprint, entity_addr) = { diff --git a/execution_engine/src/runtime_context/mod.rs b/execution_engine/src/runtime_context/mod.rs index 301432debf..c1fbaa2744 100644 --- a/execution_engine/src/runtime_context/mod.rs +++ b/execution_engine/src/runtime_context/mod.rs @@ -764,7 +764,10 @@ where | StoredValue::Message(_) | StoredValue::Prepayment(_) | StoredValue::EntryPoint(_) - | StoredValue::RawBytes(_) => Ok(()), + | StoredValue::RawBytes(_) + | StoredValue::EvmAccount(_) + | StoredValue::EvmByteCode(_) + | StoredValue::EvmStorage(_) => Ok(()), } } diff --git a/execution_engine_testing/test_support/src/transfer_request_builder.rs b/execution_engine_testing/test_support/src/transfer_request_builder.rs index 1e480509e0..c48bfb7dab 100644 --- a/execution_engine_testing/test_support/src/transfer_request_builder.rs +++ b/execution_engine_testing/test_support/src/transfer_request_builder.rs @@ -127,9 +127,11 @@ impl TransferRequestBuilder { /// authorization keys. pub fn with_initiator>(mut self, initiator: T) -> Self { self.initiator = initiator.into(); - let _ = self - .authorization_keys - .insert(self.initiator.account_hash()); + let _ = self.authorization_keys.insert( + self.initiator + .account_hash() + .expect("test transfer initiator must be a Casper account"), + ); self } diff --git a/execution_engine_testing/test_support/src/wasm_test_builder.rs b/execution_engine_testing/test_support/src/wasm_test_builder.rs index 70b8db9115..97c8dda244 100644 --- a/execution_engine_testing/test_support/src/wasm_test_builder.rs +++ b/execution_engine_testing/test_support/src/wasm_test_builder.rs @@ -813,7 +813,11 @@ where .expect("builder must have a post-state hash"); let transaction_hash = TransactionHash::V1(TransactionV1Hash::default()); - let authorization_keys = BTreeSet::from_iter(iter::once(initiator.account_hash())); + let authorization_keys = BTreeSet::from_iter(iter::once( + initiator + .account_hash() + .expect("test bidding initiator must be a Casper account"), + )); let config = &self.chainspec; let fee_handling = config.core_config.fee_handling; diff --git a/execution_engine_testing/tests/src/test/contract_api/add_contract_version.rs b/execution_engine_testing/tests/src/test/contract_api/add_contract_version.rs index baf2446da7..063e4cb0ce 100644 --- a/execution_engine_testing/tests/src/test/contract_api/add_contract_version.rs +++ b/execution_engine_testing/tests/src/test/contract_api/add_contract_version.rs @@ -189,6 +189,7 @@ fn to_v1_session_input_data<'a>( }; match txn { Transaction::Deploy(_) => panic!("unexpected deploy transaction"), + Transaction::Evm(_) => panic!("unexpected EVM transaction"), Transaction::V1(transaction_v1) => { let data = SessionDataV1::new( args, diff --git a/executor/evm/Cargo.toml b/executor/evm/Cargo.toml new file mode 100644 index 0000000000..b88cf1ad6d --- /dev/null +++ b/executor/evm/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "casper-executor-evm" +version = "0.1.0" +edition = "2021" +authors = ["Michał Papierski "] +description = "Casper EVM executor package" +homepage = "https://casper.network" +repository = "https://github.com/casper-network/casper-node/tree/dev/executor/evm" +license = "Apache-2.0" + +[dependencies] +casper-storage = { version = "5.0.0", path = "../../storage" } +casper-types = { version = "7.0.0", path = "../../types", features = ["std"] } +revm = { version = "27", features = ["dev"] } +thiserror = "2" diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs new file mode 100644 index 0000000000..38c7febc05 --- /dev/null +++ b/executor/evm/src/db.rs @@ -0,0 +1,128 @@ +//! revm database adapter backed by Casper tracking copy reads. + +use casper_storage::{ + global_state::{error::Error as GlobalStateError, state::StateReader}, + TrackingCopy, +}; +use casper_types::{evm, CLValue, Key, StoredValue, U512}; +use revm::{ + database_interface::Database, + primitives::{Address, Bytes, StorageKey, StorageValue, B256, U256}, + state::{AccountInfo, Bytecode}, +}; + +use crate::{tx, DbError}; + +pub(crate) struct CasperDb<'a, R> +where + R: StateReader, +{ + tracking_copy: &'a mut TrackingCopy, +} + +impl<'a, R> CasperDb<'a, R> +where + R: StateReader, +{ + pub(crate) fn new(tracking_copy: &'a mut TrackingCopy) -> Self { + Self { tracking_copy } + } + + fn balance(&mut self, main_purse: casper_types::URef) -> Result { + let key = Key::Balance(main_purse.addr()); + match self.tracking_copy.read(&key)? { + Some(StoredValue::CLValue(cl_value)) => cl_value_to_u256(key, cl_value), + Some(stored_value) => Err(DbError::TypeMismatch { + key: Box::new(key), + expected: "StoredValue::CLValue(U512)", + found: stored_value.type_name(), + }), + None => Ok(U256::ZERO), + } + } +} + +impl Database for CasperDb<'_, R> +where + R: StateReader, +{ + type Error = DbError; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + let address = tx::from_revm_address(address); + let key = Key::EvmAccount(address); + match self.tracking_copy.read(&key)? { + Some(StoredValue::EvmAccount(account)) => { + let balance = self.balance(account.main_purse())?; + Ok(Some(AccountInfo { + balance, + nonce: account.nonce(), + code_hash: tx::to_revm_hash(account.code_hash()), + code: None, + })) + } + Some(stored_value) => Err(DbError::TypeMismatch { + key: Box::new(key), + expected: "StoredValue::EvmAccount", + found: stored_value.type_name(), + }), + None => Ok(None), + } + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + let code_hash = tx::from_revm_hash(code_hash); + let key = Key::EvmByteCode(code_hash); + match self.tracking_copy.read(&key)? { + Some(StoredValue::EvmByteCode(byte_code)) => { + Ok(Bytecode::new_raw(Bytes::from(byte_code.into_bytes()))) + } + Some(stored_value) => Err(DbError::TypeMismatch { + key: Box::new(key), + expected: "StoredValue::EvmByteCode", + found: stored_value.type_name(), + }), + None => Ok(Bytecode::default()), + } + } + + fn storage( + &mut self, + address: Address, + index: StorageKey, + ) -> Result { + let address = tx::from_revm_address(address); + let slot = tx::from_revm_u256(index); + let key = Key::EvmStorage(evm::StorageAddr::new(address, slot)); + match self.tracking_copy.read(&key)? { + Some(StoredValue::EvmStorage(value)) => Ok(tx::to_revm_u256(value.value())), + Some(stored_value) => Err(DbError::TypeMismatch { + key: Box::new(key), + expected: "StoredValue::EvmStorage", + found: stored_value.type_name(), + }), + None => Ok(U256::ZERO), + } + } + + fn block_hash(&mut self, _number: u64) -> Result { + Ok(B256::ZERO) + } +} + +fn cl_value_to_u256(key: Key, cl_value: CLValue) -> Result { + let balance = cl_value + .into_t::() + .map_err(|error| DbError::BalanceDecode { + key: Box::new(key), + error: error.to_string(), + })?; + + if balance.bits() > 256 { + return Err(DbError::BalanceOverflow { key: Box::new(key) }); + } + + let mut bytes = [0u8; 64]; + balance.to_big_endian(&mut bytes); + Ok(U256::from_be_slice(&bytes[32..])) +} diff --git a/executor/evm/src/error.rs b/executor/evm/src/error.rs new file mode 100644 index 0000000000..033e3668a0 --- /dev/null +++ b/executor/evm/src/error.rs @@ -0,0 +1,69 @@ +//! Error types returned by the Casper EVM executor. + +use casper_storage::tracking_copy::TrackingCopyError; +use casper_types::Key; + +/// Result type returned by the EVM executor. +pub type Result = core::result::Result; + +/// Errors returned by the EVM executor. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// EVM execution is disabled in the chainspec configuration. + #[error("EVM execution is disabled")] + Disabled, + /// Transaction chain id does not match the executor configuration. + #[error("EVM transaction chain id {actual} does not match configured chain id {expected}")] + ChainIdMismatch { + /// Chain id configured in the chainspec. + expected: u64, + /// Chain id recovered from the signed transaction. + actual: u64, + }, + /// Failed to read from the Casper tracking copy as a revm database. + #[error(transparent)] + Database(#[from] DbError), + /// Failed to translate revm transaction environment. + #[error("failed to build EVM transaction environment: {0}")] + Transaction(String), + /// revm rejected execution before producing state. + #[error("EVM execution failed: {0}")] + Revm(String), + /// Failed to apply EVM state changes to the tracking copy. + #[error("failed to apply EVM state changes: {0}")] + State(String), +} + +/// Errors emitted by the revm database adapter. +#[derive(Debug, thiserror::Error)] +pub enum DbError { + /// Failed while reading from the tracking copy. + #[error(transparent)] + TrackingCopy(#[from] TrackingCopyError), + /// The value stored under an EVM key has an unexpected variant. + #[error("unexpected stored value for {key}: expected {expected}, found {found}")] + TypeMismatch { + /// Global-state key that was read. + key: Box, + /// Expected stored-value shape. + expected: &'static str, + /// Actual stored-value shape. + found: String, + }, + /// A Casper balance does not fit into EVM U256. + #[error("Casper balance at {key} does not fit into EVM U256")] + BalanceOverflow { + /// Balance key that was read. + key: Box, + }, + /// A Casper CLValue failed to decode as a balance. + #[error("failed to decode Casper balance at {key}: {error}")] + BalanceDecode { + /// Balance key that was read. + key: Box, + /// Decode error text. + error: String, + }, +} + +impl revm::database_interface::DBErrorMarker for DbError {} diff --git a/executor/evm/src/executor.rs b/executor/evm/src/executor.rs new file mode 100644 index 0000000000..f4bfe1480b --- /dev/null +++ b/executor/evm/src/executor.rs @@ -0,0 +1,122 @@ +//! Public executor entry point. + +use casper_storage::{ + global_state::{error::Error as GlobalStateError, state::StateReader}, + TrackingCopy, +}; +use casper_types::{evm, Key, StoredValue}; +use revm::{ + context_interface::result::EVMError, primitives::hardfork::SpecId, Context, ExecuteEvm, + MainBuilder, MainContext, +}; + +use crate::{ + db::CasperDb, state, tx, DbError, Error, ExecuteKind, ExecuteRequest, ExecutionOutcome, Result, +}; + +/// Executes EVM transactions and calls against a Casper tracking copy. +/// +/// The executor writes only to the supplied [`TrackingCopy`]. It never commits +/// global state; callers can pass a forked tracking copy for view execution or +/// commit the resulting effects through the normal Casper storage flow. +#[derive(Clone, Debug)] +pub struct EvmExecutor { + config: evm::EvmConfig, +} + +impl EvmExecutor { + /// Creates a new executor from chainspec EVM configuration. + pub fn new(config: evm::EvmConfig) -> Self { + Self { config } + } + + /// Returns the immutable EVM configuration used by this executor. + pub fn config(&self) -> &evm::EvmConfig { + &self.config + } + + /// Executes an EVM transaction or call against the supplied tracking copy. + pub fn execute( + &self, + tracking_copy: &mut TrackingCopy, + request: ExecuteRequest, + ) -> Result + where + R: StateReader, + { + if !self.config.enabled { + return Err(Error::Disabled); + } + + if let ExecuteKind::Transaction(transaction) = &request.kind { + if let Some(actual) = transaction.chain_id() { + if actual != self.config.chain_id { + return Err(Error::ChainIdMismatch { + expected: self.config.chain_id, + actual, + }); + } + } + } + + let spec = spec_id(self.config.spec); + let tx_env = tx::build_tx_env(&self.config, &request.kind)?; + let block = request.block.to_revm_block(&self.config); + let is_call = matches!(request.kind, ExecuteKind::Call(_)); + + let result_and_state = { + let db = CasperDb::new(tracking_copy); + let mut evm = Context::mainnet() + .with_db(db) + .with_block(block) + .modify_cfg_chained(|cfg| { + cfg.spec = spec; + cfg.chain_id = self.config.chain_id; + cfg.tx_chain_id_check = !is_call; + cfg.disable_block_gas_limit = false; + cfg.disable_base_fee = is_call; + cfg.disable_balance_check = is_call; + cfg.disable_nonce_check = is_call; + }) + .build_mainnet(); + + evm.transact(tx_env).map_err(map_revm_error)? + }; + + let outcome = ExecutionOutcome::from_revm_result(&result_and_state.result); + state::apply(tracking_copy, result_and_state.state)?; + Ok(outcome) + } +} + +fn spec_id(spec: evm::EvmSpec) -> SpecId { + match spec { + evm::EvmSpec::Frontier => SpecId::FRONTIER, + evm::EvmSpec::FrontierThawing => SpecId::FRONTIER_THAWING, + evm::EvmSpec::Homestead => SpecId::HOMESTEAD, + evm::EvmSpec::DaoFork => SpecId::DAO_FORK, + evm::EvmSpec::Tangerine => SpecId::TANGERINE, + evm::EvmSpec::SpuriousDragon => SpecId::SPURIOUS_DRAGON, + evm::EvmSpec::Byzantium => SpecId::BYZANTIUM, + evm::EvmSpec::Constantinople => SpecId::CONSTANTINOPLE, + evm::EvmSpec::Petersburg => SpecId::PETERSBURG, + evm::EvmSpec::Istanbul => SpecId::ISTANBUL, + evm::EvmSpec::MuirGlacier => SpecId::MUIR_GLACIER, + evm::EvmSpec::Berlin => SpecId::BERLIN, + evm::EvmSpec::London => SpecId::LONDON, + evm::EvmSpec::ArrowGlacier => SpecId::ARROW_GLACIER, + evm::EvmSpec::GrayGlacier => SpecId::GRAY_GLACIER, + evm::EvmSpec::Merge => SpecId::MERGE, + evm::EvmSpec::Shanghai => SpecId::SHANGHAI, + evm::EvmSpec::Cancun => SpecId::CANCUN, + evm::EvmSpec::Prague => SpecId::PRAGUE, + evm::EvmSpec::Osaka => SpecId::OSAKA, + } +} + +fn map_revm_error(error: EVMError) -> Error { + match error { + EVMError::Database(error) => Error::Database(error), + other => Error::Revm(other.to_string()), + } +} diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs new file mode 100644 index 0000000000..7276fcd0d7 --- /dev/null +++ b/executor/evm/src/lib.rs @@ -0,0 +1,25 @@ +//! Casper EVM executor. +//! +//! This crate provides a small execution API over `TrackingCopy` and keeps +//! `revm` details behind internal adapter modules. + +mod db; +mod error; +mod executor; +mod outcome; +mod request; +mod state; +mod tx; + +pub use error::{DbError, Error, Result}; +pub use executor::EvmExecutor; +pub use outcome::{ExecutionOutcome, ExecutionStatus, Log}; +pub use request::{BlockContext, CallRequest, ExecuteKind, ExecuteRequest}; + +use casper_types::evm; + +/// Keccak-256 hash of empty EVM bytecode. +pub const EMPTY_CODE_HASH: evm::Hash = evm::Hash::new([ + 0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0, + 0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70, +]); diff --git a/executor/evm/src/outcome.rs b/executor/evm/src/outcome.rs new file mode 100644 index 0000000000..1abff25e9b --- /dev/null +++ b/executor/evm/src/outcome.rs @@ -0,0 +1,100 @@ +//! Public execution outcome types. + +use casper_types::evm; +use revm::context_interface::result::{ExecutionResult, Output}; + +use crate::tx; + +/// Result returned by [`crate::EvmExecutor::execute`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExecutionOutcome { + /// High-level EVM execution status. + pub status: ExecutionStatus, + /// Gas consumed by execution. + pub gas_used: u64, + /// Return or revert bytes. + pub output: Vec, + /// Logs emitted by successful execution. + pub logs: Vec, + /// Address created by a successful create transaction. + pub created_contract_address: Option, +} + +impl ExecutionOutcome { + pub(crate) fn from_revm_result(result: &ExecutionResult) -> Self { + match result { + ExecutionResult::Success { + gas_used, + logs, + output, + .. + } => { + let (output_bytes, created_contract_address) = match output { + Output::Call(bytes) => (bytes.to_vec(), None), + Output::Create(bytes, address) => { + (bytes.to_vec(), address.map(tx::from_revm_address)) + } + }; + Self { + status: ExecutionStatus::Success, + gas_used: *gas_used, + output: output_bytes, + logs: logs.iter().map(Log::from_revm_log).collect(), + created_contract_address, + } + } + ExecutionResult::Revert { gas_used, output } => Self { + status: ExecutionStatus::Revert, + gas_used: *gas_used, + output: output.to_vec(), + logs: Vec::new(), + created_contract_address: None, + }, + ExecutionResult::Halt { gas_used, .. } => Self { + status: ExecutionStatus::Halt, + gas_used: *gas_used, + output: Vec::new(), + logs: Vec::new(), + created_contract_address: None, + }, + } + } +} + +/// High-level EVM execution status. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ExecutionStatus { + /// Execution completed successfully. + Success, + /// Execution reverted and returned revert bytes. + Revert, + /// Execution halted, usually consuming all supplied gas. + Halt, +} + +/// EVM log entry emitted by a successful transaction. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Log { + /// Contract address that emitted the log. + pub address: evm::Address, + /// Indexed log topics. + pub topics: Vec, + /// Unindexed log data. + pub data: Vec, +} + +impl Log { + fn from_revm_log(log: &revm::primitives::Log) -> Self { + Self { + address: tx::from_revm_address(log.address), + topics: log + .data + .topics() + .iter() + .copied() + .map(tx::from_revm_hash) + .collect(), + data: log.data.data.to_vec(), + } + } +} diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs new file mode 100644 index 0000000000..416259732e --- /dev/null +++ b/executor/evm/src/request.rs @@ -0,0 +1,74 @@ +//! Public execution request types. + +use casper_types::evm; + +use crate::tx; + +/// Request passed to [`crate::EvmExecutor::execute`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExecuteRequest { + /// Block context available to EVM opcodes and validation. + pub block: BlockContext, + /// EVM work item to execute. + pub kind: ExecuteKind, +} + +/// EVM work item to execute. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ExecuteKind { + /// Signed Ethereum transaction decoded by `casper-types`. + Transaction(evm::Transaction), + /// Unsigned local call request. + Call(CallRequest), +} + +/// Unsigned EVM call request. +/// +/// Calls are useful for views and simulations. They still write effects into +/// the supplied tracking copy, so callers should pass a fork when they want to +/// discard the result. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CallRequest { + /// EVM address used as `msg.sender`. + pub from: evm::Address, + /// Target account, or `None` for contract creation. + pub to: Option, + /// Amount of wei to send, encoded as a big-endian 256-bit word. + pub value: evm::Hash, + /// Calldata or contract init code. + pub input: Vec, + /// Gas available for execution. + pub gas_limit: u64, + /// Gas price used by gas-price-sensitive contracts. + pub gas_price: u128, + /// Nonce presented to revm when nonce checks are enabled by future callers. + pub nonce: u64, +} + +/// Per-execution block context. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BlockContext { + /// EVM block number. + pub number: u64, + /// EVM block timestamp in seconds since the Unix epoch. + pub timestamp: u64, + /// Block beneficiary address. + pub beneficiary: evm::Address, + /// Optional gas limit override. Defaults to chainspec `[evm].block_gas_limit`. + pub gas_limit: Option, + /// Optional base-fee override. Defaults to chainspec `[evm].base_fee`. + pub base_fee: Option, +} + +impl BlockContext { + pub(crate) fn to_revm_block(&self, config: &evm::EvmConfig) -> revm::context::BlockEnv { + revm::context::BlockEnv { + number: revm::primitives::U256::from(self.number), + beneficiary: tx::to_revm_address(self.beneficiary), + timestamp: revm::primitives::U256::from(self.timestamp), + gas_limit: self.gas_limit.unwrap_or(config.block_gas_limit), + basefee: self.base_fee.unwrap_or(config.base_fee), + ..Default::default() + } + } +} diff --git a/executor/evm/src/state.rs b/executor/evm/src/state.rs new file mode 100644 index 0000000000..bdaff0799f --- /dev/null +++ b/executor/evm/src/state.rs @@ -0,0 +1,136 @@ +//! Translation from revm state changes into Casper tracking copy writes. + +use casper_storage::{ + global_state::{error::Error as GlobalStateError, state::StateReader}, + KeyPrefix, TrackingCopy, +}; +use casper_types::{evm, CLValue, Key, StoredValue, U512}; +use revm::{ + primitives::{Address, U256}, + state::{Account, EvmState}, +}; + +use crate::{tx, Error}; + +pub(crate) fn apply(tracking_copy: &mut TrackingCopy, state: EvmState) -> Result<(), Error> +where + R: StateReader, +{ + for (address, account) in state { + apply_account(tracking_copy, address, account)?; + } + Ok(()) +} + +fn apply_account( + tracking_copy: &mut TrackingCopy, + address: Address, + account: Account, +) -> Result<(), Error> +where + R: StateReader, +{ + let address = tx::from_revm_address(address); + let account_key = Key::EvmAccount(address); + + if account.is_selfdestructed() { + prune_account(tracking_copy, address, account_key)?; + return Ok(()); + } + + if let Some(code) = account.info.code.as_ref() { + let bytes = code.original_byte_slice(); + if !bytes.is_empty() { + tracking_copy.write( + Key::EvmByteCode(tx::from_revm_hash(account.info.code_hash)), + StoredValue::EvmByteCode(evm::ByteCode::new(bytes.to_vec())), + ); + } + } + + let main_purse = existing_main_purse(tracking_copy, &account_key)? + .unwrap_or_else(|| evm::deterministic_purse(address)); + let code_hash = tx::from_revm_hash(account.info.code_hash); + + tracking_copy.write( + account_key, + StoredValue::EvmAccount(evm::Account::new(account.info.nonce, code_hash, main_purse)), + ); + write_balance(tracking_copy, main_purse, account.info.balance)?; + + for (slot, value) in account.changed_storage_slots() { + let key = Key::EvmStorage(evm::StorageAddr::new(address, tx::from_revm_u256(*slot))); + if value.present_value.is_zero() { + tracking_copy.prune(key); + } else { + tracking_copy.write( + key, + StoredValue::EvmStorage(evm::StorageValue::new(tx::from_revm_u256( + value.present_value, + ))), + ); + } + } + + Ok(()) +} + +fn prune_account( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + account_key: Key, +) -> Result<(), Error> +where + R: StateReader, +{ + let storage_keys = tracking_copy + .get_keys_by_prefix(&KeyPrefix::EvmStorageByAddress(address)) + .map_err(|error| Error::State(error.to_string()))?; + for key in storage_keys { + tracking_copy.prune(key); + } + tracking_copy.prune(account_key); + Ok(()) +} + +fn existing_main_purse( + tracking_copy: &mut TrackingCopy, + account_key: &Key, +) -> Result, Error> +where + R: StateReader, +{ + match tracking_copy + .read(account_key) + .map_err(|error| Error::State(error.to_string()))? + { + Some(StoredValue::EvmAccount(account)) => Ok(Some(account.main_purse())), + Some(stored_value) => Err(Error::State(format!( + "unexpected stored value for {account_key}: expected StoredValue::EvmAccount, found {}", + stored_value.type_name() + ))), + None => Ok(None), + } +} + +fn write_balance( + tracking_copy: &mut TrackingCopy, + main_purse: casper_types::URef, + balance: U256, +) -> Result<(), Error> +where + R: StateReader, +{ + let balance = u256_to_u512(balance); + let cl_value = CLValue::from_t(balance).map_err(|error| Error::State(error.to_string()))?; + tracking_copy.write( + Key::Balance(main_purse.addr()), + StoredValue::CLValue(cl_value), + ); + Ok(()) +} + +fn u256_to_u512(value: U256) -> U512 { + let bytes = value.to_be_bytes::<32>(); + U512::from_big_endian(&bytes) +} diff --git a/executor/evm/src/tx.rs b/executor/evm/src/tx.rs new file mode 100644 index 0000000000..ec3556763f --- /dev/null +++ b/executor/evm/src/tx.rs @@ -0,0 +1,82 @@ +//! Translation from Casper-owned EVM requests into revm transaction environments. + +use casper_types::evm; +use revm::{ + context::TxEnv, + primitives::{Address, Bytes, TxKind, B256, U256}, +}; + +use crate::{Error, ExecuteKind}; + +pub(crate) fn build_tx_env(config: &evm::EvmConfig, kind: &ExecuteKind) -> Result { + let tx_env = match kind { + ExecuteKind::Transaction(transaction) => { + let mut builder = TxEnv::builder() + .caller(to_revm_address(transaction.from())) + .gas_limit(transaction.gas_limit()) + .value(to_revm_u256(transaction.value())) + .data(Bytes::from(transaction.input().to_vec())) + .nonce(transaction.nonce()) + .chain_id(transaction.chain_id().or(Some(config.chain_id))); + + builder = match transaction.kind() { + evm::TransactionKind::Legacy | evm::TransactionKind::Eip2930 => builder.gas_price( + transaction + .gas_price() + .unwrap_or_else(|| transaction.max_fee_per_gas()), + ), + evm::TransactionKind::Eip1559 => builder + .max_fee_per_gas(transaction.max_fee_per_gas()) + .gas_priority_fee(transaction.max_priority_fee_per_gas()), + }; + + builder = match transaction.to() { + Some(address) => builder.kind(TxKind::Call(to_revm_address(address))), + None => builder.kind(TxKind::Create), + }; + + builder + .build() + .map_err(|error| Error::Transaction(format!("{error:?}")))? + } + ExecuteKind::Call(call) => TxEnv::builder() + .caller(to_revm_address(call.from)) + .gas_limit(call.gas_limit) + .gas_price(call.gas_price) + .kind(match call.to { + Some(address) => TxKind::Call(to_revm_address(address)), + None => TxKind::Create, + }) + .value(to_revm_u256(call.value)) + .data(Bytes::from(call.input.clone())) + .nonce(call.nonce) + .chain_id(Some(config.chain_id)) + .build() + .map_err(|error| Error::Transaction(format!("{error:?}")))?, + }; + Ok(tx_env) +} + +pub(crate) fn to_revm_address(address: evm::Address) -> Address { + Address::from(address.value()) +} + +pub(crate) fn from_revm_address(address: Address) -> evm::Address { + evm::Address::new(address.into_array()) +} + +pub(crate) fn to_revm_hash(hash: evm::Hash) -> B256 { + B256::from(hash.value()) +} + +pub(crate) fn from_revm_hash(hash: B256) -> evm::Hash { + evm::Hash::new(hash.0) +} + +pub(crate) fn to_revm_u256(value: evm::Hash) -> U256 { + U256::from_be_slice(value.as_bytes()) +} + +pub(crate) fn from_revm_u256(value: U256) -> evm::Hash { + evm::Hash::new(value.to_be_bytes()) +} diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs new file mode 100644 index 0000000000..8239601325 --- /dev/null +++ b/executor/evm/tests/executor.rs @@ -0,0 +1,531 @@ +use std::path::PathBuf; + +use casper_executor_evm::{ + BlockContext, CallRequest, EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, + EMPTY_CODE_HASH, +}; +use casper_storage::{ + data_access_layer::{GenesisRequest, GenesisResult}, + global_state::{ + self, + error::Error as GlobalStateError, + state::{lmdb::LmdbGlobalStateView, CommitProvider, StateProvider, StateReader}, + }, + TrackingCopy, +}; +use casper_types::{ + evm, CLValue, ChainspecRegistry, Digest, GenesisAccount, GenesisConfig, HoldBalanceHandling, + Key, Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, StoredValue, SystemConfig, + Timestamp, WasmConfig, U512, +}; + +fn tracking_copy() -> (TrackingCopy, impl Send) { + let accounts = (1u8..=3) + .map(|seed| { + let secret_key = + SecretKey::ed25519_from_bytes([seed; SecretKey::ED25519_LENGTH]).unwrap(); + GenesisAccount::Account { + public_key: PublicKey::from(&secret_key), + balance: Motes::new(U512::from(1_000_000_000_000u64)), + validator: None, + } + }) + .collect(); + + let (global_state, _state_root_hash, tempdir) = + global_state::state::lmdb::make_temporary_global_state([]); + let genesis_config = GenesisConfig::new( + accounts, + WasmConfig::default(), + SystemConfig::default(), + 10, + 10, + 0, + Default::default(), + 14, + Timestamp::now().millis(), + HoldBalanceHandling::Accrued, + 0, + true, + None, + StorageCosts::default(), + 0, + ); + let genesis_request = GenesisRequest::new( + Digest::hash("evm-executor-test-genesis"), + ProtocolVersion::V2_0_0, + genesis_config, + ChainspecRegistry::new_with_genesis(b"", b""), + ); + let post_state_hash = match global_state.genesis(genesis_request) { + GenesisResult::Failure(failure) => panic!("failed to run genesis: {failure:?}"), + GenesisResult::Fatal(fatal) => panic!("fatal error while running genesis: {fatal}"), + GenesisResult::Success { + post_state_hash, .. + } => post_state_hash, + }; + let reader = global_state + .checkout(post_state_hash) + .expect("checkout should not fail") + .expect("post-genesis root should exist"); + (TrackingCopy::new(reader, 5, false), tempdir) +} + +fn executor(spec: evm::EvmSpec) -> EvmExecutor { + EvmExecutor::new(evm::EvmConfig { + enabled: true, + chain_id: 7, + spec, + block_gas_limit: 30_000_000, + base_fee: 0, + }) +} + +fn block() -> BlockContext { + BlockContext { + number: 1, + timestamp: 1_714_000_000, + beneficiary: evm::Address::ZERO, + gas_limit: None, + base_fee: None, + } +} + +fn call_request( + from: evm::Address, + to: Option, + input: Vec, + value: evm::Hash, +) -> ExecuteRequest { + ExecuteRequest { + block: block(), + kind: ExecuteKind::Call(CallRequest { + from, + to, + value, + input, + gas_limit: 5_000_000, + gas_price: 0, + nonce: 0, + }), + } +} + +fn execute_call>( + executor: &EvmExecutor, + tracking_copy: &mut TrackingCopy, + from: evm::Address, + to: Option, + input: Vec, +) -> casper_executor_evm::ExecutionOutcome { + let outcome = executor + .execute( + tracking_copy, + call_request(from, to, input, evm::Hash::ZERO), + ) + .expect("EVM execution should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + outcome +} + +fn deploy>( + executor: &EvmExecutor, + tracking_copy: &mut TrackingCopy, + from: evm::Address, + name: &str, +) -> evm::Address { + execute_call(executor, tracking_copy, from, None, contract_bin(name)) + .created_contract_address + .expect("deploy should return a contract address") +} + +fn contract_bin(name: &str) -> Vec { + let path = artifact_path(format!("{name}.bin")); + let hex = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + decode_hex(hex.trim()) +} + +fn artifact_path(file_name: String) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("target") + .join("evm-contracts") + .join(file_name) +} + +fn selector(signature: &str) -> Vec { + revm::primitives::keccak256(signature.as_bytes())[..4].to_vec() +} + +fn calldata(signature: &str, args: &[evm::Hash]) -> Vec { + let mut bytes = selector(signature); + for arg in args { + bytes.extend_from_slice(arg.as_bytes()); + } + bytes +} + +fn word(value: u64) -> evm::Hash { + let mut bytes = [0u8; 32]; + bytes[24..].copy_from_slice(&value.to_be_bytes()); + evm::Hash::new(bytes) +} + +fn address_word(address: evm::Address) -> evm::Hash { + let mut bytes = [0u8; 32]; + bytes[12..].copy_from_slice(address.as_bytes()); + evm::Hash::new(bytes) +} + +fn decode_word(output: &[u8]) -> u64 { + assert_eq!(output.len(), 32); + u64::from_be_bytes(output[24..].try_into().unwrap()) +} + +fn decode_address(output: &[u8]) -> evm::Address { + assert_eq!(output.len(), 32); + let mut bytes = [0u8; 20]; + bytes.copy_from_slice(&output[12..]); + evm::Address::new(bytes) +} + +fn decode_hex(hex: &str) -> Vec { + assert!(hex.len() % 2 == 0, "hex input must have an even length"); + (0..hex.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).unwrap()) + .collect() +} + +fn read_storage>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + slot: evm::Hash, +) -> Option { + match tracking_copy + .read(&Key::EvmStorage(evm::StorageAddr::new(address, slot))) + .expect("storage read should not fail") + { + Some(StoredValue::EvmStorage(value)) => Some(value.value()), + Some(other) => panic!("unexpected storage value: {other:?}"), + None => None, + } +} + +fn read_balance>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> U512 { + let purse = match tracking_copy + .read(&Key::EvmAccount(address)) + .expect("account read should not fail") + { + Some(StoredValue::EvmAccount(account)) => account.main_purse(), + Some(other) => panic!("unexpected account value: {other:?}"), + None => return U512::zero(), + }; + match tracking_copy + .read(&Key::Balance(purse.addr())) + .expect("balance read should not fail") + { + Some(StoredValue::CLValue(value)) => value.into_t::().unwrap(), + Some(other) => panic!("unexpected balance value: {other:?}"), + None => U512::zero(), + } +} + +fn seed_evm_balance>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + balance: U512, +) { + let main_purse = evm::deterministic_purse(address); + tracking_copy.write( + Key::EvmAccount(address), + StoredValue::EvmAccount(evm::Account::new(0, EMPTY_CODE_HASH, main_purse)), + ); + tracking_copy.write( + Key::Balance(main_purse.addr()), + StoredValue::CLValue(CLValue::from_t(balance).unwrap()), + ); +} + +#[test] +fn counter_supports_committed_and_discarded_execution() { + let executor = executor(evm::EvmSpec::Prague); + let from = evm::Address::new([1; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let counter = deploy(&executor, &mut tracking_copy, from, "Counter"); + + let increment = execute_call( + &executor, + &mut tracking_copy, + from, + Some(counter), + selector("increment()"), + ); + assert_eq!(decode_word(&increment.output), 1); + + let mut view = tracking_copy.fork(); + let view_increment = execute_call( + &executor, + &mut view, + from, + Some(counter), + selector("increment()"), + ); + assert_eq!(decode_word(&view_increment.output), 2); + + let get = execute_call( + &executor, + &mut tracking_copy, + from, + Some(counter), + selector("get()"), + ); + assert_eq!(decode_word(&get.output), 1); +} + +#[test] +fn erc20_and_native_purse_balances_update() { + let executor = executor(evm::EvmSpec::Prague); + let owner = evm::Address::new([1; 20]); + let recipient = evm::Address::new([2; 20]); + let spender = evm::Address::new([3; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_balance(&mut tracking_copy, owner, U512::from(1_000u64)); + let transfer_value = word(250); + let outcome = executor + .execute( + &mut tracking_copy, + call_request(owner, Some(recipient), Vec::new(), transfer_value), + ) + .expect("native EVM transfer should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(read_balance(&mut tracking_copy, owner), U512::from(750u64)); + assert_eq!( + read_balance(&mut tracking_copy, recipient), + U512::from(250u64) + ); + + let token = deploy(&executor, &mut tracking_copy, owner, "MinimalERC20"); + execute_call( + &executor, + &mut tracking_copy, + owner, + Some(token), + calldata("mint(address,uint256)", &[address_word(owner), word(1_000)]), + ); + execute_call( + &executor, + &mut tracking_copy, + owner, + Some(token), + calldata( + "transfer(address,uint256)", + &[address_word(recipient), word(150)], + ), + ); + execute_call( + &executor, + &mut tracking_copy, + owner, + Some(token), + calldata( + "approve(address,uint256)", + &[address_word(spender), word(100)], + ), + ); + execute_call( + &executor, + &mut tracking_copy, + spender, + Some(token), + calldata( + "transferFrom(address,address,uint256)", + &[address_word(owner), address_word(recipient), word(40)], + ), + ); + + let owner_balance = execute_call( + &executor, + &mut tracking_copy, + owner, + Some(token), + calldata("balanceOf(address)", &[address_word(owner)]), + ); + let recipient_balance = execute_call( + &executor, + &mut tracking_copy, + owner, + Some(token), + calldata("balanceOf(address)", &[address_word(recipient)]), + ); + let remaining_allowance = execute_call( + &executor, + &mut tracking_copy, + owner, + Some(token), + calldata( + "allowance(address,address)", + &[address_word(owner), address_word(spender)], + ), + ); + + assert_eq!(decode_word(&owner_balance.output), 810); + assert_eq!(decode_word(&recipient_balance.output), 190); + assert_eq!(decode_word(&remaining_allowance.output), 60); +} + +#[test] +fn erc721_mint_approve_and_transfer() { + let executor = executor(evm::EvmSpec::Prague); + let owner = evm::Address::new([1; 20]); + let recipient = evm::Address::new([2; 20]); + let approved = evm::Address::new([3; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let nft = deploy(&executor, &mut tracking_copy, owner, "MinimalERC721"); + + execute_call( + &executor, + &mut tracking_copy, + owner, + Some(nft), + calldata("mint(address,uint256)", &[address_word(owner), word(42)]), + ); + let initial_owner = execute_call( + &executor, + &mut tracking_copy, + owner, + Some(nft), + calldata("ownerOf(uint256)", &[word(42)]), + ); + assert_eq!(decode_address(&initial_owner.output), owner); + + execute_call( + &executor, + &mut tracking_copy, + owner, + Some(nft), + calldata( + "approve(address,uint256)", + &[address_word(approved), word(42)], + ), + ); + execute_call( + &executor, + &mut tracking_copy, + approved, + Some(nft), + calldata( + "transferFrom(address,address,uint256)", + &[address_word(owner), address_word(recipient), word(42)], + ), + ); + let final_owner = execute_call( + &executor, + &mut tracking_copy, + owner, + Some(nft), + calldata("ownerOf(uint256)", &[word(42)]), + ); + assert_eq!(decode_address(&final_owner.output), recipient); +} + +#[test] +fn storage_zeroes_are_pruned() { + let executor = executor(evm::EvmSpec::Prague); + let from = evm::Address::new([1; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let contract = deploy(&executor, &mut tracking_copy, from, "StorageDelete"); + + execute_call( + &executor, + &mut tracking_copy, + from, + Some(contract), + calldata("set(uint256)", &[word(123)]), + ); + assert_eq!( + read_storage(&mut tracking_copy, contract, evm::Hash::ZERO), + Some(word(123)) + ); + + execute_call( + &executor, + &mut tracking_copy, + from, + Some(contract), + selector("clear()"), + ); + assert_eq!( + read_storage(&mut tracking_copy, contract, evm::Hash::ZERO), + None + ); +} + +#[test] +fn selfdestruct_cleanup_follows_selected_fork() { + let from = evm::Address::new([1; 20]); + let beneficiary = evm::Address::new([2; 20]); + + let shanghai_executor = executor(evm::EvmSpec::Shanghai); + let (mut shanghai_tracking_copy, _shanghai_tempdir) = tracking_copy(); + let shanghai_contract = deploy( + &shanghai_executor, + &mut shanghai_tracking_copy, + from, + "SelfDestruct", + ); + assert_eq!( + read_storage( + &mut shanghai_tracking_copy, + shanghai_contract, + evm::Hash::ZERO + ), + Some(word(7)) + ); + execute_call( + &shanghai_executor, + &mut shanghai_tracking_copy, + from, + Some(shanghai_contract), + calldata("destroy(address)", &[address_word(beneficiary)]), + ); + assert_eq!( + shanghai_tracking_copy + .read(&Key::EvmAccount(shanghai_contract)) + .unwrap(), + None + ); + assert_eq!( + read_storage( + &mut shanghai_tracking_copy, + shanghai_contract, + evm::Hash::ZERO + ), + None + ); + + let prague_executor = executor(evm::EvmSpec::Prague); + let (mut prague_tracking_copy, _prague_tempdir) = tracking_copy(); + let prague_contract = deploy( + &prague_executor, + &mut prague_tracking_copy, + from, + "SelfDestruct", + ); + execute_call( + &prague_executor, + &mut prague_tracking_copy, + from, + Some(prague_contract), + calldata("destroy(address)", &[address_word(beneficiary)]), + ); + assert!(prague_tracking_copy + .read(&Key::EvmAccount(prague_contract)) + .unwrap() + .is_some()); +} diff --git a/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index 45f1eca4fa..c576f89b41 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -347,6 +347,7 @@ where } KeyPrefix::EntryPointsV1ByEntity(addr) => StorageKeyPrefix::EntryPointsV1ByEntity(addr), KeyPrefix::EntryPointsV2ByEntity(addr) => StorageKeyPrefix::EntryPointsV2ByEntity(addr), + KeyPrefix::EvmStorageByAddress(addr) => StorageKeyPrefix::EvmStorageByAddress(addr), }; let request = PrefixedValuesRequest::new(state_root_hash, storage_key_prefix); match effect_builder.get_prefixed_values(request).await { diff --git a/node/src/components/block_synchronizer/transaction_acquisition.rs b/node/src/components/block_synchronizer/transaction_acquisition.rs index 6dacebd01a..63e4224b43 100644 --- a/node/src/components/block_synchronizer/transaction_acquisition.rs +++ b/node/src/components/block_synchronizer/transaction_acquisition.rs @@ -87,6 +87,9 @@ impl TransactionAcquisition { (TransactionHash::V1(transaction_v1_hash), txn_v1_approvals_hash) => { TransactionId::new(transaction_v1_hash.into(), txn_v1_approvals_hash) } + (TransactionHash::Evm(transaction_hash), approvals_hash) => { + TransactionId::new(transaction_hash.into(), approvals_hash) + } }; new_txn_ids.push((txn_id, TransactionState::Vacant)); } diff --git a/node/src/components/block_synchronizer/transaction_acquisition/tests.rs b/node/src/components/block_synchronizer/transaction_acquisition/tests.rs index 7e541f0a98..3b351d7987 100644 --- a/node/src/components/block_synchronizer/transaction_acquisition/tests.rs +++ b/node/src/components/block_synchronizer/transaction_acquisition/tests.rs @@ -46,16 +46,7 @@ fn gen_approvals_hashes<'a, I: Iterator + Clone>( } fn get_transaction_id(transaction: &Transaction) -> TransactionId { - match transaction { - Transaction::Deploy(deploy) => TransactionId::new( - TransactionHash::Deploy(*deploy.hash()), - deploy.compute_approvals_hash().unwrap(), - ), - Transaction::V1(transaction_v1) => TransactionId::new( - TransactionHash::V1(*transaction_v1.hash()), - transaction_v1.compute_approvals_hash().unwrap(), - ), - } + transaction.compute_id() } #[test] diff --git a/node/src/components/block_validator.rs b/node/src/components/block_validator.rs index 289f0c7609..40add0d13b 100644 --- a/node/src/components/block_validator.rs +++ b/node/src/components/block_validator.rs @@ -833,6 +833,7 @@ where TransactionId::new(deploy_hash.into(), approvals_hash) } TransactionHash::V1(v1_hash) => TransactionId::new(v1_hash.into(), approvals_hash), + TransactionHash::Evm(evm_hash) => TransactionId::new(evm_hash.into(), approvals_hash), }; effects.extend( effect_builder diff --git a/node/src/components/block_validator/state.rs b/node/src/components/block_validator/state.rs index 207c7538b9..e0d12ade48 100644 --- a/node/src/components/block_validator/state.rs +++ b/node/src/components/block_validator/state.rs @@ -1276,10 +1276,7 @@ mod tests { // Create a new, random transaction. let transaction = new_standard(fixture.rng, 1500.into(), TimeDiff::from_seconds(1)); - let transaction_hash = match &transaction { - Transaction::Deploy(deploy) => TransactionHash::Deploy(*deploy.hash()), - Transaction::V1(v1) => TransactionHash::V1(*v1.hash()), - }; + let transaction_hash = transaction.hash(); let chainspec = Chainspec::default(); let footprint = TransactionFootprint::new(&chainspec, &transaction).unwrap(); diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 4dfb6a2d2e..9f6f3237bf 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -370,9 +370,10 @@ pub fn execute_finalized_block( artifact_builder .with_state_result_error(err) .map_err(|_| BlockExecutionError::RootNotFound(state_root_hash))?; - BalanceIdentifier::PenalizedAccount( - initiator_addr.clone().account_hash(), - ) + let account_hash = initiator_addr + .account_hash() + .expect("contract runtime initiator must be a Casper account"); + BalanceIdentifier::PenalizedAccount(account_hash) } } } else { @@ -473,7 +474,10 @@ pub fn execute_finalized_block( BalanceIdentifier::Payment } } else { - BalanceIdentifier::PenalizedAccount(initiator_addr.clone().account_hash()) + let account_hash = initiator_addr + .account_hash() + .expect("contract runtime initiator must be a Casper account"); + BalanceIdentifier::PenalizedAccount(account_hash) } }; diff --git a/node/src/components/contract_runtime/operations/wasm_v2_request.rs b/node/src/components/contract_runtime/operations/wasm_v2_request.rs index 0fae7680d3..8314b14c91 100644 --- a/node/src/components/contract_runtime/operations/wasm_v2_request.rs +++ b/node/src/components/contract_runtime/operations/wasm_v2_request.rs @@ -213,8 +213,11 @@ impl WasmV2Request { // different API. debug_assert_eq!(transferred_value, value); + let initiator_account_hash = initiator_addr + .account_hash() + .expect("Wasm v2 initiator must be a Casper account"); let install_request = builder - .with_initiator(initiator_addr.account_hash()) + .with_initiator(initiator_account_hash) .with_gas_limit(gas_limit) .with_transaction_hash(transaction_hash) .with_wasm_bytes(module_bytes) @@ -233,15 +236,17 @@ impl WasmV2Request { Target::Session { .. } | Target::Stored { .. } => { let mut builder = ExecuteRequestBuilder::default(); - let initiator_account_hash = &initiator_addr.account_hash(); + let initiator_account_hash = initiator_addr + .account_hash() + .expect("Wasm v2 initiator must be a Casper account"); - let initiator_key = Key::Account(*initiator_account_hash); + let initiator_key = Key::Account(initiator_account_hash); builder = builder .with_address_generator(address_generator) .with_gas_limit(gas_limit) .with_transaction_hash(transaction_hash) - .with_initiator(*initiator_account_hash) + .with_initiator(initiator_account_hash) .with_caller_key(initiator_key) .with_chain_name(network_name) .with_transferred_value(value) diff --git a/node/src/components/contract_runtime/types.rs b/node/src/components/contract_runtime/types.rs index 5af037dc8f..78b11725ce 100644 --- a/node/src/components/contract_runtime/types.rs +++ b/node/src/components/contract_runtime/types.rs @@ -577,6 +577,11 @@ impl SpeculativeExecutionResult { Transaction::V1(_) => SpeculativeExecutionResult::InvalidTransaction( InvalidTransaction::V1(InvalidTransactionV1::UnableToCalculateGasLimit), ), + Transaction::Evm(_) => SpeculativeExecutionResult::InvalidTransaction( + InvalidTransaction::Evm(casper_types::evm::TransactionError::Decode( + "EVM transactions are not routed through contract runtime".to_string(), + )), + ), } } diff --git a/node/src/components/event_stream_server/sse_server.rs b/node/src/components/event_stream_server/sse_server.rs index 051d0fd762..03b9b0cd84 100644 --- a/node/src/components/event_stream_server/sse_server.rs +++ b/node/src/components/event_stream_server/sse_server.rs @@ -124,6 +124,7 @@ impl SseData { let (timestamp, ttl) = match &txn { Transaction::Deploy(deploy) => (deploy.timestamp(), deploy.ttl()), Transaction::V1(txn) => (txn.timestamp(), txn.ttl()), + Transaction::Evm(txn) => (txn.timestamp(), txn.ttl()), }; let message_count = rng.gen_range(0..6); let messages = std::iter::repeat_with(|| rng.gen()) diff --git a/node/src/components/storage.rs b/node/src/components/storage.rs index 4e073b6008..17010c9e14 100644 --- a/node/src/components/storage.rs +++ b/node/src/components/storage.rs @@ -1614,7 +1614,7 @@ impl Storage { .all_transactions() .filter_map(|transaction_hash| match transaction_hash { TransactionHash::Deploy(deploy_hash) => Some(*deploy_hash), - TransactionHash::V1(_) => None, + TransactionHash::V1(_) | TransactionHash::Evm(_) => None, }) .collect(), }; @@ -1660,7 +1660,7 @@ impl Storage { match transaction { Transaction::Deploy(deploy) => Ok(Some(LegacyDeploy::from(deploy))), - transaction @ Transaction::V1(_) => { + transaction @ (Transaction::V1(_) | Transaction::Evm(_)) => { let mismatch = VariantMismatch(Box::new((transaction_hash, transaction))); error!(%mismatch, "failed getting legacy deploy"); Err(FatalStorageError::from(mismatch)) @@ -1721,6 +1721,18 @@ impl Storage { } } } + (approvals_hash, finalized_approvals, transaction @ Transaction::Evm(_)) => { + match ApprovalsHash::compute(&finalized_approvals) { + Ok(computed_approvals_hash) if computed_approvals_hash == approvals_hash => { + Ok(Some(transaction)) + } + Ok(_computed_approvals_hash) => Ok(None), + Err(error) => { + error!(%error, "failed to calculate finalized EVM transaction approvals hash"); + Err(FatalStorageError::UnexpectedSerializationFailure(error)) + } + } + } } } @@ -2035,6 +2047,11 @@ impl Storage { Some(Transaction::V1(transaction_v1)) => { ret.push((transaction_hash, (&transaction_v1).into(), execution_result)) } + Some(transaction @ Transaction::Evm(_)) => { + let mismatch = VariantMismatch(Box::new((transaction_hash, transaction))); + error!(%mismatch, "failed getting transaction header"); + return Err(FatalStorageError::from(mismatch)); + } }; } Ok(Some(ret)) diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index 1fc6ad6010..ef3c49cb3a 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -18,9 +18,9 @@ use casper_types::{ account::AccountHash, addressable_entity::AddressableEntity, system::auction::ARG_AMOUNT, AddressableEntityHash, AddressableEntityIdentifier, BlockHeader, Chainspec, EntityAddr, EntityKind, EntityVersion, EntityVersionKey, ExecutableDeployItem, - ExecutableDeployItemIdentifier, InitiatorAddr, Package, PackageAddr, PackageHash, - PackageIdentifier, Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, - TransactionTarget, DEFAULT_ENTRY_POINT_NAME, U512, + ExecutableDeployItemIdentifier, Package, PackageAddr, PackageHash, PackageIdentifier, + Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, TransactionTarget, + DEFAULT_ENTRY_POINT_NAME, U512, }; use crate::{ @@ -212,9 +212,18 @@ impl TransactionAcceptor { }; if event_metadata.source.is_client() { - let account_hash = match event_metadata.transaction.initiator_addr() { - InitiatorAddr::PublicKey(public_key) => public_key.to_account_hash(), - InitiatorAddr::AccountHash(account_hash) => account_hash, + let initiator_addr = event_metadata.transaction.initiator_addr(); + let Some(account_hash) = initiator_addr.account_hash() else { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Decode( + "EVM transactions are not routed through transaction acceptor" + .to_string(), + ), + )), + ); }; let entity_addr = EntityAddr::Account(account_hash.value()); effect_builder diff --git a/node/src/components/transaction_acceptor/tests.rs b/node/src/components/transaction_acceptor/tests.rs index ee77a15fd6..35aac95691 100644 --- a/node/src/components/transaction_acceptor/tests.rs +++ b/node/src/components/transaction_acceptor/tests.rs @@ -1125,6 +1125,7 @@ impl reactor::Reactor for Reactor { | BalanceIdentifier::PenalizedAccount(account_hash) => { Key::Account(*account_hash) } + BalanceIdentifier::Evm(address) => Key::EvmAccount(*address), BalanceIdentifier::Entity(entity_addr) => { Key::AddressableEntity(*entity_addr) } diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index 0cc81cbaa6..c9e90b3410 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -256,6 +256,11 @@ impl MetaTransaction { &transaction_config.transaction_v1_config, ) .map(MetaTransaction::V1), + Transaction::Evm(_) => Err(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Decode( + "EVM transactions are not routed through node transaction metadata".to_string(), + ), + )), } } @@ -468,6 +473,11 @@ pub(crate) fn calculate_transaction_lane_for_transaction( ) .map_err(InvalidTransaction::V1) } + Transaction::Evm(_) => Err(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Decode( + "EVM transactions do not use Casper transaction lanes".to_string(), + ), + )), } } diff --git a/node/src/types/transaction/meta_transaction/transaction_header.rs b/node/src/types/transaction/meta_transaction/transaction_header.rs index fa0c6b0108..c4b6820638 100644 --- a/node/src/types/transaction/meta_transaction/transaction_header.rs +++ b/node/src/types/transaction/meta_transaction/transaction_header.rs @@ -63,6 +63,9 @@ impl From<&Transaction> for TransactionHeader { match transaction { Transaction::Deploy(deploy) => deploy.header().clone().into(), Transaction::V1(v1) => v1.into(), + Transaction::Evm(_) => { + panic!("EVM transactions are not routed through node transaction metadata") + } } } } diff --git a/node/src/utils/chain_specification/parse_toml.rs b/node/src/utils/chain_specification/parse_toml.rs index 1a3ce947a3..696d414f3e 100644 --- a/node/src/utils/chain_specification/parse_toml.rs +++ b/node/src/utils/chain_specification/parse_toml.rs @@ -32,10 +32,10 @@ use std::{convert::TryFrom, path::Path}; use serde::{Deserialize, Serialize}; use casper_types::{ - bytesrepr::Bytes, file_utils, AccountsConfig, ActivationPoint, Chainspec, ChainspecRawBytes, - CoreConfig, GlobalStateUpdate, GlobalStateUpdateConfig, HighwayConfig, NetworkConfig, - ProtocolConfig, ProtocolVersion, StorageCosts, SystemConfig, TransactionConfig, VacancyConfig, - WasmConfig, + bytesrepr::Bytes, evm::EvmConfig, file_utils, AccountsConfig, ActivationPoint, Chainspec, + ChainspecRawBytes, CoreConfig, GlobalStateUpdate, GlobalStateUpdateConfig, HighwayConfig, + NetworkConfig, ProtocolConfig, ProtocolVersion, StorageCosts, SystemConfig, TransactionConfig, + VacancyConfig, WasmConfig, }; use crate::utils::{ @@ -77,6 +77,8 @@ pub(super) struct TomlChainspec { network: TomlNetwork, core: CoreConfig, transactions: TransactionConfig, + #[serde(default)] + evm: EvmConfig, highway: HighwayConfig, wasm: WasmConfig, system_costs: SystemConfig, @@ -97,6 +99,7 @@ impl From<&Chainspec> for TomlChainspec { }; let core = chainspec.core_config.clone(); let transactions = chainspec.transaction_config.clone(); + let evm = chainspec.evm_config; let highway = chainspec.highway_config; let wasm = chainspec.wasm_config; let system_costs = chainspec.system_costs_config; @@ -108,6 +111,7 @@ impl From<&Chainspec> for TomlChainspec { network, core, transactions, + evm, highway, wasm, system_costs, @@ -163,6 +167,7 @@ pub(super) fn parse_toml>( network_config, core_config: toml_chainspec.core, transaction_config: toml_chainspec.transactions, + evm_config: toml_chainspec.evm, highway_config: toml_chainspec.highway, wasm_config: toml_chainspec.wasm, system_costs_config: toml_chainspec.system_costs, diff --git a/node/src/utils/specimen.rs b/node/src/utils/specimen.rs index f9277a04f5..5f33a5a61f 100644 --- a/node/src/utils/specimen.rs +++ b/node/src/utils/specimen.rs @@ -842,6 +842,7 @@ impl LargestSpecimen for BlockPayload { cache, ))) } + Transaction::Evm(transaction) => Transaction::Evm(transaction), }; let large_txn_hash_with_approvals = (large_txn.hash(), large_txn.approvals()); diff --git a/resources/integration-test/chainspec.toml b/resources/integration-test/chainspec.toml index 0ec491a465..9d2bebfbb6 100644 --- a/resources/integration-test/chainspec.toml +++ b/resources/integration-test/chainspec.toml @@ -508,3 +508,13 @@ upper_threshold = 90 lower_threshold = 50 max_gas_price = 1 min_gas_price = 1 + +[evm] +enabled = false +# EVM chain IDs use 0x435350NN ("CSP" + network namespace) as EIP-155 replay domains. +# Namespaces: mainnet=0x01, testnet=0x02, integration=0x04, devnet=0x05, local=0xFF. +# This chainspec uses namespace 0x04 (integration); decimal 1129533444. +chain_id = 1_129_533_444 +spec = 'prague' +block_gas_limit = 30_000_000 +base_fee = 0 diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index b9b34c1328..681e2b71d7 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -500,3 +500,13 @@ upper_threshold = 90 lower_threshold = 50 max_gas_price = 3 min_gas_price = 1 + +[evm] +enabled = false +# EVM chain IDs use 0x435350NN ("CSP" + network namespace) as EIP-155 replay domains. +# Namespaces: mainnet=0x01, testnet=0x02, integration=0x04, devnet=0x05, local=0xFF. +# All local chainspecs use namespace 0xFF; decimal 1129533695. +chain_id = 1_129_533_695 +spec = 'prague' +block_gas_limit = 30_000_000 +base_fee = 0 diff --git a/resources/mainnet/chainspec.toml b/resources/mainnet/chainspec.toml index 3448dc9935..7e22a6f1f0 100644 --- a/resources/mainnet/chainspec.toml +++ b/resources/mainnet/chainspec.toml @@ -508,3 +508,13 @@ upper_threshold = 90 lower_threshold = 50 max_gas_price = 1 min_gas_price = 1 + +[evm] +enabled = false +# EVM chain IDs use 0x435350NN ("CSP" + network namespace) as EIP-155 replay domains. +# Namespaces: mainnet=0x01, testnet=0x02, integration=0x04, devnet=0x05, local=0xFF. +# This chainspec uses namespace 0x01 (mainnet); decimal 1129533441. +chain_id = 1_129_533_441 +spec = 'prague' +block_gas_limit = 30_000_000 +base_fee = 0 diff --git a/resources/production/chainspec.toml b/resources/production/chainspec.toml index 58d0008057..fe98a0ea34 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -507,3 +507,13 @@ upper_threshold = 90 lower_threshold = 50 max_gas_price = 1 min_gas_price = 1 + +[evm] +enabled = false +# EVM chain IDs use 0x435350NN ("CSP" + network namespace) as EIP-155 replay domains. +# Namespaces: mainnet=0x01, testnet=0x02, integration=0x04, devnet=0x05, local=0xFF. +# This chainspec uses namespace 0x01 (mainnet); decimal 1129533441. +chain_id = 1_129_533_441 +spec = 'prague' +block_gas_limit = 30_000_000 +base_fee = 0 diff --git a/resources/testnet/chainspec.toml b/resources/testnet/chainspec.toml index 2158da03cb..01148de81a 100644 --- a/resources/testnet/chainspec.toml +++ b/resources/testnet/chainspec.toml @@ -510,3 +510,13 @@ upper_threshold = 90 lower_threshold = 50 max_gas_price = 1 min_gas_price = 1 + +[evm] +enabled = false +# EVM chain IDs use 0x435350NN ("CSP" + network namespace) as EIP-155 replay domains. +# Namespaces: mainnet=0x01, testnet=0x02, integration=0x04, devnet=0x05, local=0xFF. +# This chainspec uses namespace 0x02 (testnet); decimal 1129533442. +chain_id = 1_129_533_442 +spec = 'prague' +block_gas_limit = 30_000_000 +base_fee = 0 diff --git a/smart_contracts/evm_contracts/Counter.sol b/smart_contracts/evm_contracts/Counter.sol new file mode 100644 index 0000000000..2006062208 --- /dev/null +++ b/smart_contracts/evm_contracts/Counter.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Counter { + uint256 private counter; + + function increment() external payable returns (uint256) { + counter += 1; + return counter; + } + + function decrement() external payable returns (uint256) { + counter -= 1; + return counter; + } + + function get() external view returns (uint256) { + return counter; + } +} diff --git a/smart_contracts/evm_contracts/MinimalERC20.sol b/smart_contracts/evm_contracts/MinimalERC20.sol new file mode 100644 index 0000000000..7c288df4d4 --- /dev/null +++ b/smart_contracts/evm_contracts/MinimalERC20.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract MinimalERC20 { + string public name = "Casper EVM Test Token"; + string public symbol = "CET"; + uint8 public decimals = 18; + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + totalSupply += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(address to, uint256 amount) external returns (bool) { + _transfer(msg.sender, to, amount); + return true; + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + uint256 currentAllowance = allowance[from][msg.sender]; + require(currentAllowance >= amount, "allowance"); + allowance[from][msg.sender] = currentAllowance - amount; + _transfer(from, to, amount); + return true; + } + + function _transfer(address from, address to, uint256 amount) internal { + require(balanceOf[from] >= amount, "balance"); + balanceOf[from] -= amount; + balanceOf[to] += amount; + emit Transfer(from, to, amount); + } +} diff --git a/smart_contracts/evm_contracts/MinimalERC721.sol b/smart_contracts/evm_contracts/MinimalERC721.sol new file mode 100644 index 0000000000..eb2488cd76 --- /dev/null +++ b/smart_contracts/evm_contracts/MinimalERC721.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract MinimalERC721 { + string public name = "Casper EVM Test NFT"; + string public symbol = "CEN"; + + mapping(uint256 => address) private owners; + mapping(address => uint256) private balances; + mapping(uint256 => address) private tokenApprovals; + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + function mint(address to, uint256 tokenId) external { + require(to != address(0), "zero"); + require(owners[tokenId] == address(0), "minted"); + owners[tokenId] = to; + balances[to] += 1; + emit Transfer(address(0), to, tokenId); + } + + function balanceOf(address owner) external view returns (uint256) { + require(owner != address(0), "zero"); + return balances[owner]; + } + + function ownerOf(uint256 tokenId) public view returns (address) { + address owner = owners[tokenId]; + require(owner != address(0), "missing"); + return owner; + } + + function approve(address to, uint256 tokenId) external { + address owner = ownerOf(tokenId); + require(msg.sender == owner, "owner"); + tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + function getApproved(uint256 tokenId) external view returns (address) { + require(owners[tokenId] != address(0), "missing"); + return tokenApprovals[tokenId]; + } + + function transferFrom(address from, address to, uint256 tokenId) public { + address owner = ownerOf(tokenId); + require(owner == from, "from"); + require(to != address(0), "zero"); + require(msg.sender == owner || msg.sender == tokenApprovals[tokenId], "auth"); + + tokenApprovals[tokenId] = address(0); + balances[from] -= 1; + balances[to] += 1; + owners[tokenId] = to; + emit Transfer(from, to, tokenId); + } +} diff --git a/smart_contracts/evm_contracts/SelfDestruct.sol b/smart_contracts/evm_contracts/SelfDestruct.sol new file mode 100644 index 0000000000..58a58a79c9 --- /dev/null +++ b/smart_contracts/evm_contracts/SelfDestruct.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract SelfDestruct { + uint256 public value; + + constructor() payable { + value = 7; + } + + function destroy(address payable beneficiary) external { + selfdestruct(beneficiary); + } +} diff --git a/smart_contracts/evm_contracts/StorageDelete.sol b/smart_contracts/evm_contracts/StorageDelete.sol new file mode 100644 index 0000000000..8b947e5bae --- /dev/null +++ b/smart_contracts/evm_contracts/StorageDelete.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract StorageDelete { + uint256 public value; + + function set(uint256 newValue) external { + value = newValue; + } + + function clear() external { + delete value; + } +} diff --git a/storage/src/block_store/lmdb/versioned_databases.rs b/storage/src/block_store/lmdb/versioned_databases.rs index aecd297915..c2930fa6b6 100644 --- a/storage/src/block_store/lmdb/versioned_databases.rs +++ b/storage/src/block_store/lmdb/versioned_databases.rs @@ -42,6 +42,7 @@ impl VersionedKey for TransactionHash { match self { TransactionHash::Deploy(deploy_hash) => Some(deploy_hash), TransactionHash::V1(_) => None, + TransactionHash::Evm(_) => None, } } } @@ -566,6 +567,7 @@ mod tests { let _ = visited.insert(*deploy.hash(), deploy); } Transaction::V1(_) => unreachable!(), + Transaction::Evm(_) => unreachable!(), } Ok(()) }; diff --git a/storage/src/data_access_layer/balance.rs b/storage/src/data_access_layer/balance.rs index c2b0760ec1..c131855fd2 100644 --- a/storage/src/data_access_layer/balance.rs +++ b/storage/src/data_access_layer/balance.rs @@ -1,6 +1,7 @@ //! Types for balance queries. use casper_types::{ account::AccountHash, + evm, global_state::TrieMerkleProof, system::{ handle_payment::{ACCUMULATION_PURSE_KEY, PAYMENT_PURSE_KEY, REFUND_PURSE_KEY}, @@ -8,7 +9,8 @@ use casper_types::{ HANDLE_PAYMENT, }, AccessRights, BlockTime, Digest, EntityAddr, HoldBalanceHandling, InitiatorAddr, Key, - ProtocolVersion, PublicKey, StoredValue, TimeDiff, URef, URefAddr, U512, + ProtocolVersion, PublicKey, StoredValue, StoredValueTypeMismatch, TimeDiff, URef, URefAddr, + U512, }; use itertools::Itertools; use num_rational::Ratio; @@ -60,6 +62,8 @@ pub enum BalanceIdentifier { Public(PublicKey), /// Use main purse of entity from account hash. Account(AccountHash), + /// Use main purse backing an EVM account. + Evm(evm::Address), /// Use main purse of entity. Entity(EntityAddr), /// Use purse at Key::Purse(URefAddr). @@ -78,6 +82,7 @@ impl BalanceIdentifier { BalanceIdentifier::Purse(uref) => Some(uref.addr()), BalanceIdentifier::Public(_) | BalanceIdentifier::Account(_) + | BalanceIdentifier::Evm(_) | BalanceIdentifier::PenalizedAccount(_) | BalanceIdentifier::PenalizedPayment | BalanceIdentifier::Entity(_) @@ -117,6 +122,21 @@ impl BalanceIdentifier { Err(tce) => return Err(tce), } } + BalanceIdentifier::Evm(address) => { + let key = Key::EvmAccount(*address); + match tc.read(&key)? { + Some(StoredValue::EvmAccount(account)) => account.main_purse(), + Some(stored_value) => { + return Err(TrackingCopyError::TypeMismatch( + StoredValueTypeMismatch::new( + "StoredValue::EvmAccount".to_string(), + stored_value.type_name(), + ), + )); + } + None => return Err(TrackingCopyError::KeyNotFound(key)), + } + } BalanceIdentifier::Entity(entity_addr) => { match tc.runtime_footprint_by_entity_addr(*entity_addr) { Ok(entity) => entity @@ -192,6 +212,7 @@ impl From for BalanceIdentifier { match value { InitiatorAddr::PublicKey(public_key) => BalanceIdentifier::Public(public_key), InitiatorAddr::AccountHash(account_hash) => BalanceIdentifier::Account(account_hash), + InitiatorAddr::EvmAddress(address) => BalanceIdentifier::Evm(address), } } } diff --git a/storage/src/data_access_layer/key_prefix.rs b/storage/src/data_access_layer/key_prefix.rs index 6e3fe12e06..498b1def40 100644 --- a/storage/src/data_access_layer/key_prefix.rs +++ b/storage/src/data_access_layer/key_prefix.rs @@ -2,6 +2,7 @@ use casper_types::{ account::AccountHash, bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, contract_messages::TopicNameHash, + evm::Address as EvmAddress, system::{auction::BidAddrTag, mint::BalanceHoldAddrTag}, EntityAddr, KeyTag, URefAddr, }; @@ -25,6 +26,8 @@ pub enum KeyPrefix { EntryPointsV1ByEntity(EntityAddr), /// Retrieves all V2 entry points for a given entity. EntryPointsV2ByEntity(EntityAddr), + /// Retrieves all EVM storage slots for a given EVM address. + EvmStorageByAddress(EvmAddress), } impl ToBytes for KeyPrefix { @@ -57,6 +60,7 @@ impl ToBytes for KeyPrefix { KeyPrefix::EntryPointsV2ByEntity(entity) => { U8_SERIALIZED_LENGTH + entity.serialized_length() } + KeyPrefix::EvmStorageByAddress(address) => address.serialized_length(), } } @@ -100,6 +104,10 @@ impl ToBytes for KeyPrefix { writer.push(1); entity.write_bytes(writer)?; } + KeyPrefix::EvmStorageByAddress(address) => { + writer.push(KeyTag::EvmStorage as u8); + address.write_bytes(writer)?; + } } Ok(()) } @@ -160,6 +168,10 @@ impl FromBytes for KeyPrefix { _ => return Err(bytesrepr::Error::Formatting), } } + tag if tag == KeyTag::EvmStorage as u8 => { + let (address, remainder) = EvmAddress::from_bytes(remainder)?; + (KeyPrefix::EvmStorageByAddress(address), remainder) + } _ => return Err(bytesrepr::Error::Formatting), }; Ok(result) @@ -194,6 +206,8 @@ mod tests { u8_slice_32().prop_map(KeyPrefix::ProcessingBalanceHoldsByPurse), entity_addr_arb().prop_map(KeyPrefix::EntryPointsV1ByEntity), entity_addr_arb().prop_map(KeyPrefix::EntryPointsV2ByEntity), + prop::array::uniform20(any::()) + .prop_map(|bytes| { KeyPrefix::EvmStorageByAddress(EvmAddress::new(bytes)) }), ] } diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index a4fb2870fe..d1ee10aa9a 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -1170,7 +1170,9 @@ pub trait StateProvider: Send + Sync + Sized { Err(err) => return BiddingResult::Failure(TrackingCopyError::Storage(err)), }; - let source_account_hash = initiator.account_hash(); + let source_account_hash = initiator + .account_hash() + .expect("bidding initiator must be a Casper account"); let (entity_addr, mut footprint, mut entity_access_rights) = match tc .borrow_mut() .authorized_runtime_footprint_with_access_rights( @@ -1489,7 +1491,7 @@ pub trait StateProvider: Send + Sync + Sized { // pay amount from source to target match runtime .transfer( - Some(initiator_addr.account_hash()), + initiator_addr.account_hash(), source_purse, target_purse, refund_amount, @@ -1559,7 +1561,7 @@ pub trait StateProvider: Send + Sync + Sized { }; match runtime .transfer( - Some(initiator_addr.account_hash()), + initiator_addr.account_hash(), source_purse, target_purse, refund_amount, @@ -1701,7 +1703,7 @@ pub trait StateProvider: Send + Sync + Sized { }; runtime .transfer( - Some(initiator_addr.account_hash()), + initiator_addr.account_hash(), source_purse, target_purse, amount, @@ -2078,7 +2080,10 @@ pub trait StateProvider: Send + Sync + Sized { } }; - let source_account_hash = request.initiator().account_hash(); + let source_account_hash = request + .initiator() + .account_hash() + .expect("transfer initiator must be a Casper account"); let protocol_version = request.protocol_version(); if let Err(tce) = tc .borrow_mut() @@ -2277,7 +2282,10 @@ pub trait StateProvider: Send + Sync + Sized { } }; - let source_account_hash = request.initiator().account_hash(); + let source_account_hash = request + .initiator() + .account_hash() + .expect("burn initiator must be a Casper account"); let protocol_version = request.protocol_version(); if let Err(tce) = tc .borrow_mut() diff --git a/storage/src/tracking_copy/byte_size.rs b/storage/src/tracking_copy/byte_size.rs index d71443e6c9..c1e99e2db3 100644 --- a/storage/src/tracking_copy/byte_size.rs +++ b/storage/src/tracking_copy/byte_size.rs @@ -48,6 +48,9 @@ impl ByteSize for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.serialized_length(), StoredValue::EntryPoint(entry_point) => entry_point.serialized_length(), StoredValue::RawBytes(raw_bytes) => raw_bytes.serialized_length(), + StoredValue::EvmAccount(account) => account.serialized_length(), + StoredValue::EvmByteCode(byte_code) => byte_code.serialized_length(), + StoredValue::EvmStorage(value) => value.serialized_length(), } } } diff --git a/storage/src/tracking_copy/mod.rs b/storage/src/tracking_copy/mod.rs index d32747b5a8..850bb81027 100644 --- a/storage/src/tracking_copy/mod.rs +++ b/storage/src/tracking_copy/mod.rs @@ -891,6 +891,15 @@ where StoredValue::RawBytes(_) => { return Ok(query.into_not_found_result("RawBytes value found.")); } + StoredValue::EvmAccount(_) => { + return Ok(query.into_not_found_result("EvmAccount value found.")); + } + StoredValue::EvmByteCode(_) => { + return Ok(query.into_not_found_result("EvmByteCode value found.")); + } + StoredValue::EvmStorage(_) => { + return Ok(query.into_not_found_result("EvmStorage value found.")); + } } } } diff --git a/types/Cargo.toml b/types/Cargo.toml index 0c72a916c5..47b5563a53 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -49,6 +49,10 @@ uint = { version = "0.9.0", default-features = false } untrusted = { version = "0.7.1", optional = true } derive_more = "0.99.17" version-sync = { version = "0.9", optional = true } +alloy-consensus = { version = "=1.0.22", default-features = false, features = ["k256"] } +alloy-primitives = { version = "=1.2.0", default-features = false, features = ["rlp", "sha3-keccak"] } +alloy-eips = { version = "=1.0.22", default-features = false, features = ["k256"] } +alloy-tx-macros = { version = "=1.0.22", default-features = false } [dev-dependencies] base16 = { version = "0.2.1", features = ["std"] } @@ -73,6 +77,7 @@ thiserror = "1" untrusted = "0.7.1" # add explicit dependency to resolve RUSTSEC-2024-0421 url = "2.5.4" +hex-literal = "1.1.0" [features] json-schema = ["once_cell", "schemars", "serde-map-to-array/json-schema"] diff --git a/types/src/block/test_block_builder/test_block_v2_builder.rs b/types/src/block/test_block_builder/test_block_v2_builder.rs index a0b35ee5d4..fed1c9fb04 100644 --- a/types/src/block/test_block_builder/test_block_v2_builder.rs +++ b/types/src/block/test_block_builder/test_block_v2_builder.rs @@ -193,6 +193,7 @@ impl TestBlockV2Builder { let target = transaction_v1.get_transaction_target().unwrap(); simplified_calculate_transaction_lane_from_values(&entry_point, &target) } + Transaction::Evm(_) => LARGE_WASM_LANE_ID, }; match lane_id { MINT_LANE_ID => mint_hashes.push(txn_hash), diff --git a/types/src/chainspec.rs b/types/src/chainspec.rs index f29bcfcdba..a1fde64a45 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -35,6 +35,7 @@ use tracing::error; use crate::testing::TestRng; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, + evm::EvmConfig, ChainNameDigest, Digest, EraId, ProtocolVersion, Timestamp, }; pub use accounts_config::{ @@ -43,14 +44,14 @@ pub use accounts_config::{ }; pub use activation_point::ActivationPoint; pub use chainspec_raw_bytes::ChainspecRawBytes; +#[cfg(any(all(feature = "std", feature = "testing"), test))] +pub use core_config::DEFAULT_FEE_HANDLING; pub use core_config::{ ConsensusProtocolName, CoreConfig, LegacyRequiredFinality, DEFAULT_GAS_HOLD_INTERVAL, DEFAULT_MINIMUM_BID_AMOUNT, }; #[cfg(any(feature = "std", test))] -pub use core_config::{ - DEFAULT_BASELINE_MOTES_AMOUNT, DEFAULT_FEE_HANDLING, DEFAULT_REFUND_HANDLING, -}; +pub use core_config::{DEFAULT_BASELINE_MOTES_AMOUNT, DEFAULT_REFUND_HANDLING}; pub use fee_handling::FeeHandling; #[cfg(any(feature = "std", test))] pub use genesis_config::GenesisConfig; @@ -121,6 +122,10 @@ pub struct Chainspec { #[serde(rename = "transactions")] pub transaction_config: TransactionConfig, + /// EVM config. + #[serde(rename = "evm")] + pub evm_config: EvmConfig, + /// Wasm config. #[serde(rename = "wasm")] pub wasm_config: WasmConfig, @@ -276,6 +281,7 @@ impl Chainspec { let core_config = CoreConfig::random(rng); let highway_config = HighwayConfig::random(rng); let transaction_config = TransactionConfig::random(rng); + let evm_config = EvmConfig::default(); let wasm_config = rng.gen(); let system_costs_config = SystemConfig::random(rng); let vacancy_config = VacancyConfig::random(rng); @@ -286,6 +292,7 @@ impl Chainspec { core_config, highway_config, transaction_config, + evm_config, wasm_config, system_costs_config, vacancy_config, @@ -337,6 +344,7 @@ impl ToBytes for Chainspec { self.core_config.write_bytes(writer)?; self.highway_config.write_bytes(writer)?; self.transaction_config.write_bytes(writer)?; + self.evm_config.write_bytes(writer)?; self.wasm_config.write_bytes(writer)?; self.system_costs_config.write_bytes(writer)?; self.vacancy_config.write_bytes(writer)?; @@ -355,6 +363,7 @@ impl ToBytes for Chainspec { + self.core_config.serialized_length() + self.highway_config.serialized_length() + self.transaction_config.serialized_length() + + self.evm_config.serialized_length() + self.wasm_config.serialized_length() + self.system_costs_config.serialized_length() + self.vacancy_config.serialized_length() @@ -369,6 +378,7 @@ impl FromBytes for Chainspec { let (core_config, remainder) = CoreConfig::from_bytes(remainder)?; let (highway_config, remainder) = HighwayConfig::from_bytes(remainder)?; let (transaction_config, remainder) = TransactionConfig::from_bytes(remainder)?; + let (evm_config, remainder) = EvmConfig::from_bytes(remainder)?; let (wasm_config, remainder) = WasmConfig::from_bytes(remainder)?; let (system_costs_config, remainder) = SystemConfig::from_bytes(remainder)?; let (vacancy_config, remainder) = VacancyConfig::from_bytes(remainder)?; @@ -379,6 +389,7 @@ impl FromBytes for Chainspec { core_config, highway_config, transaction_config, + evm_config, wasm_config, system_costs_config, vacancy_config, diff --git a/types/src/evm.rs b/types/src/evm.rs new file mode 100644 index 0000000000..0cd0af2ae8 --- /dev/null +++ b/types/src/evm.rs @@ -0,0 +1,21 @@ +//! EVM-native types shared by Casper components. +//! +//! This module intentionally exposes Casper-owned wrapper types instead of +//! executor or `revm` types. Ethereum transaction decoding and secp256k1 sender +//! recovery are performed here so downstream crates can validate signed RLP +//! without depending on the executor implementation. + +mod account; +mod address; +mod config; +mod hash; +mod transaction; + +pub use account::{deterministic_purse, Account, ByteCode, StorageAddr, StorageValue}; +pub use address::{Address, ADDRESS_LENGTH}; +pub use config::{EvmConfig, EvmSpec}; +pub use hash::{Hash, HASH_LENGTH}; +pub use transaction::{ + Transaction, TransactionError, TransactionHash, TransactionKind, EIP4844_TRANSACTION_TYPE_ID, + EIP7702_TRANSACTION_TYPE_ID, +}; diff --git a/types/src/evm/account.rs b/types/src/evm/account.rs new file mode 100644 index 0000000000..51e8be1564 --- /dev/null +++ b/types/src/evm/account.rs @@ -0,0 +1,262 @@ +use alloc::vec::Vec; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::{Address, Hash, ADDRESS_LENGTH}; +use crate::{ + bytesrepr::{self, Bytes, FromBytes, ToBytes}, + Digest, URef, +}; + +/// EVM account metadata stored in global state. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct Account { + nonce: u64, + code_hash: Hash, + main_purse: URef, +} + +impl Account { + /// Creates EVM account metadata. + pub const fn new(nonce: u64, code_hash: Hash, main_purse: URef) -> Self { + Account { + nonce, + code_hash, + main_purse, + } + } + + /// Returns the EVM account nonce. + pub const fn nonce(self) -> u64 { + self.nonce + } + + /// Returns the hash of the bytecode associated with this account. + pub const fn code_hash(self) -> Hash { + self.code_hash + } + + /// Returns the Casper main purse backing this EVM account balance. + pub const fn main_purse(self) -> URef { + self.main_purse + } +} + +impl ToBytes for Account { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut bytes = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut bytes)?; + Ok(bytes) + } + + fn serialized_length(&self) -> usize { + self.nonce.serialized_length() + + self.code_hash.serialized_length() + + self.main_purse.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.nonce.write_bytes(writer)?; + self.code_hash.write_bytes(writer)?; + self.main_purse.write_bytes(writer) + } +} + +impl FromBytes for Account { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (nonce, remainder) = u64::from_bytes(bytes)?; + let (code_hash, remainder) = Hash::from_bytes(remainder)?; + let (main_purse, remainder) = URef::from_bytes(remainder)?; + Ok((Account::new(nonce, code_hash, main_purse), remainder)) + } +} + +/// EVM contract bytecode stored in global state. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct ByteCode(Vec); + +impl ByteCode { + /// Creates EVM bytecode from raw bytes. + pub fn new(bytes: Vec) -> Self { + ByteCode(bytes) + } + + /// Returns the bytecode bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Consumes the wrapper and returns the bytecode bytes. + pub fn into_bytes(self) -> Vec { + self.0 + } +} + +impl AsRef<[u8]> for ByteCode { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl From> for ByteCode { + fn from(bytes: Vec) -> Self { + ByteCode::new(bytes) + } +} + +impl From for ByteCode { + fn from(bytes: Bytes) -> Self { + ByteCode::new(bytes.into()) + } +} + +impl ToBytes for ByteCode { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + self.0.to_bytes() + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.0.write_bytes(writer) + } +} + +impl FromBytes for ByteCode { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + Bytes::from_bytes(bytes).map(|(bytes, remainder)| (ByteCode::from(bytes), remainder)) + } +} + +/// EVM storage value stored in global state. +#[derive( + Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct StorageValue(Hash); + +impl StorageValue { + /// Creates an EVM storage value from a 32-byte word. + pub const fn new(value: Hash) -> Self { + StorageValue(value) + } + + /// Returns the 32-byte word stored in this slot. + pub const fn value(self) -> Hash { + self.0 + } + + /// Returns `true` when all bytes are zero. + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + +impl From for StorageValue { + fn from(value: Hash) -> Self { + StorageValue::new(value) + } +} + +impl From for Hash { + fn from(value: StorageValue) -> Self { + value.value() + } +} + +impl ToBytes for StorageValue { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + self.0.to_bytes() + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.0.write_bytes(writer) + } +} + +impl FromBytes for StorageValue { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + Hash::from_bytes(bytes).map(|(hash, remainder)| (StorageValue::new(hash), remainder)) + } +} + +/// Global-state address for one EVM account storage slot. +#[derive( + Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct StorageAddr { + address: Address, + slot: Hash, +} + +impl StorageAddr { + /// Creates an EVM storage address from a contract address and storage slot. + pub const fn new(address: Address, slot: Hash) -> Self { + StorageAddr { address, slot } + } + + /// Returns the EVM account or contract address owning the storage slot. + pub const fn address(self) -> Address { + self.address + } + + /// Returns the EVM storage slot key. + pub const fn slot(self) -> Hash { + self.slot + } +} + +impl ToBytes for StorageAddr { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut bytes = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut bytes)?; + Ok(bytes) + } + + fn serialized_length(&self) -> usize { + self.address.serialized_length() + self.slot.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.address.write_bytes(writer)?; + self.slot.write_bytes(writer) + } +} + +impl FromBytes for StorageAddr { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (address, remainder) = Address::from_bytes(bytes)?; + let (slot, remainder) = Hash::from_bytes(remainder)?; + Ok((StorageAddr::new(address, slot), remainder)) + } +} + +/// Returns the deterministic main purse backing an EVM address. +pub fn deterministic_purse(address: Address) -> URef { + let mut preimage = Vec::with_capacity(b"evm-purse-v1".len() + ADDRESS_LENGTH); + preimage.extend_from_slice(b"evm-purse-v1"); + preimage.extend_from_slice(address.as_ref()); + URef::new( + Digest::hash(preimage).value(), + crate::AccessRights::READ_ADD_WRITE, + ) +} diff --git a/types/src/evm/address.rs b/types/src/evm/address.rs new file mode 100644 index 0000000000..96cf507059 --- /dev/null +++ b/types/src/evm/address.rs @@ -0,0 +1,90 @@ +use alloc::{string::String, vec::Vec}; +use core::{ + convert::TryFrom, + fmt::{self, Display, Formatter}, +}; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::bytesrepr::{self, FromBytes, ToBytes}; + +/// The number of bytes in an EVM address. +pub const ADDRESS_LENGTH: usize = 20; + +const ADDRESS_SERIALIZED_LENGTH: usize = ADDRESS_LENGTH; + +/// A 20-byte Ethereum account or contract address. +#[derive( + Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct Address([u8; ADDRESS_LENGTH]); + +impl Address { + /// The zero EVM address. + pub const ZERO: Address = Address([0; ADDRESS_LENGTH]); + + /// Creates an address from raw bytes. + pub const fn new(bytes: [u8; ADDRESS_LENGTH]) -> Self { + Address(bytes) + } + + /// Returns the raw bytes backing this address. + pub const fn value(self) -> [u8; ADDRESS_LENGTH] { + self.0 + } + + /// Returns the raw bytes backing this address by reference. + pub const fn as_bytes(&self) -> &[u8; ADDRESS_LENGTH] { + &self.0 + } + + /// Returns a lower-case hexadecimal string without a `0x` prefix. + pub fn to_hex_string(self) -> String { + base16::encode_lower(&self.0) + } +} + +impl AsRef<[u8]> for Address { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Display for Address { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "0x{}", self.to_hex_string()) + } +} + +impl ToBytes for Address { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Ok(self.0.to_vec()) + } + + fn serialized_length(&self) -> usize { + ADDRESS_SERIALIZED_LENGTH + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.extend_from_slice(&self.0); + Ok(()) + } +} + +impl FromBytes for Address { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + if bytes.len() < ADDRESS_LENGTH { + return Err(bytesrepr::Error::EarlyEndOfStream); + } + let (address, remainder) = bytes.split_at(ADDRESS_LENGTH); + let address = + <[u8; ADDRESS_LENGTH]>::try_from(address).map_err(|_| bytesrepr::Error::Formatting)?; + Ok((Address(address), remainder)) + } +} diff --git a/types/src/evm/config.rs b/types/src/evm/config.rs new file mode 100644 index 0000000000..3f1b3e3a52 --- /dev/null +++ b/types/src/evm/config.rs @@ -0,0 +1,206 @@ +use alloc::vec::Vec; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}; + +/// Supported EVM hardfork specifications for chainspec configuration. +#[derive( + Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize, +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(rename_all = "snake_case")] +pub enum EvmSpec { + /// Frontier. + Frontier, + /// Frontier thawing. + FrontierThawing, + /// Homestead. + Homestead, + /// DAO fork. + DaoFork, + /// Tangerine Whistle. + Tangerine, + /// Spurious Dragon. + SpuriousDragon, + /// Byzantium. + Byzantium, + /// Constantinople. + Constantinople, + /// Petersburg. + Petersburg, + /// Istanbul. + Istanbul, + /// Muir Glacier. + MuirGlacier, + /// Berlin. + Berlin, + /// London. + London, + /// Arrow Glacier. + ArrowGlacier, + /// Gray Glacier. + GrayGlacier, + /// Paris, also known as the merge. + Merge, + /// Shanghai. + Shanghai, + /// Cancun. + Cancun, + /// Prague. + #[default] + Prague, + /// Osaka. + Osaka, +} + +impl EvmSpec { + fn tag(self) -> u8 { + match self { + EvmSpec::Frontier => 0, + EvmSpec::FrontierThawing => 1, + EvmSpec::Homestead => 2, + EvmSpec::DaoFork => 3, + EvmSpec::Tangerine => 4, + EvmSpec::SpuriousDragon => 5, + EvmSpec::Byzantium => 6, + EvmSpec::Constantinople => 7, + EvmSpec::Petersburg => 8, + EvmSpec::Istanbul => 9, + EvmSpec::MuirGlacier => 10, + EvmSpec::Berlin => 11, + EvmSpec::London => 12, + EvmSpec::ArrowGlacier => 13, + EvmSpec::GrayGlacier => 14, + EvmSpec::Merge => 15, + EvmSpec::Shanghai => 16, + EvmSpec::Cancun => 17, + EvmSpec::Prague => 18, + EvmSpec::Osaka => 19, + } + } +} + +impl ToBytes for EvmSpec { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Ok(vec![self.tag()]) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.push(self.tag()); + Ok(()) + } +} + +impl FromBytes for EvmSpec { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let spec = match tag { + 0 => EvmSpec::Frontier, + 1 => EvmSpec::FrontierThawing, + 2 => EvmSpec::Homestead, + 3 => EvmSpec::DaoFork, + 4 => EvmSpec::Tangerine, + 5 => EvmSpec::SpuriousDragon, + 6 => EvmSpec::Byzantium, + 7 => EvmSpec::Constantinople, + 8 => EvmSpec::Petersburg, + 9 => EvmSpec::Istanbul, + 10 => EvmSpec::MuirGlacier, + 11 => EvmSpec::Berlin, + 12 => EvmSpec::London, + 13 => EvmSpec::ArrowGlacier, + 14 => EvmSpec::GrayGlacier, + 15 => EvmSpec::Merge, + 16 => EvmSpec::Shanghai, + 17 => EvmSpec::Cancun, + 18 => EvmSpec::Prague, + 19 => EvmSpec::Osaka, + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((spec, remainder)) + } +} + +/// Chainspec configuration for EVM execution. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct EvmConfig { + /// Whether EVM execution is enabled. + pub enabled: bool, + /// EVM chain ID used for the `CHAINID` opcode and transaction validation. + pub chain_id: u64, + /// Hardfork specification used by the EVM executor. + pub spec: EvmSpec, + /// Per-block gas limit supplied to the EVM block context. + pub block_gas_limit: u64, + /// Base fee supplied to the EVM block context. + pub base_fee: u64, +} + +impl Default for EvmConfig { + fn default() -> Self { + EvmConfig { + enabled: false, + chain_id: 0, + spec: EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + } + } +} + +impl ToBytes for EvmConfig { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.enabled.serialized_length() + + self.chain_id.serialized_length() + + self.spec.serialized_length() + + self.block_gas_limit.serialized_length() + + self.base_fee.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.enabled.write_bytes(writer)?; + self.chain_id.write_bytes(writer)?; + self.spec.write_bytes(writer)?; + self.block_gas_limit.write_bytes(writer)?; + self.base_fee.write_bytes(writer) + } +} + +impl FromBytes for EvmConfig { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (enabled, remainder) = bool::from_bytes(bytes)?; + let (chain_id, remainder) = u64::from_bytes(remainder)?; + let (spec, remainder) = EvmSpec::from_bytes(remainder)?; + let (block_gas_limit, remainder) = u64::from_bytes(remainder)?; + let (base_fee, remainder) = u64::from_bytes(remainder)?; + Ok(( + EvmConfig { + enabled, + chain_id, + spec, + block_gas_limit, + base_fee, + }, + remainder, + )) + } +} diff --git a/types/src/evm/hash.rs b/types/src/evm/hash.rs new file mode 100644 index 0000000000..d58707a024 --- /dev/null +++ b/types/src/evm/hash.rs @@ -0,0 +1,94 @@ +use alloc::{string::String, vec::Vec}; +use core::{ + convert::TryFrom, + fmt::{self, Display, Formatter}, +}; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::bytesrepr::{self, FromBytes, ToBytes}; + +/// The number of bytes in an EVM 256-bit hash or word. +pub const HASH_LENGTH: usize = 32; + +const HASH_SERIALIZED_LENGTH: usize = HASH_LENGTH; + +/// A 32-byte EVM hash or storage word. +#[derive( + Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct Hash([u8; HASH_LENGTH]); + +impl Hash { + /// The zero hash. + pub const ZERO: Hash = Hash([0; HASH_LENGTH]); + + /// Creates a hash from raw bytes. + pub const fn new(bytes: [u8; HASH_LENGTH]) -> Self { + Hash(bytes) + } + + /// Returns the raw bytes backing this hash. + pub const fn value(self) -> [u8; HASH_LENGTH] { + self.0 + } + + /// Returns the raw bytes backing this hash by reference. + pub const fn as_bytes(&self) -> &[u8; HASH_LENGTH] { + &self.0 + } + + /// Returns `true` when all bytes are zero. + pub fn is_zero(&self) -> bool { + self.0.iter().all(|byte| *byte == 0) + } + + /// Returns a lower-case hexadecimal string without a `0x` prefix. + pub fn to_hex_string(self) -> String { + base16::encode_lower(&self.0) + } +} + +impl AsRef<[u8]> for Hash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Display for Hash { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "0x{}", self.to_hex_string()) + } +} + +impl ToBytes for Hash { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Ok(self.0.to_vec()) + } + + fn serialized_length(&self) -> usize { + HASH_SERIALIZED_LENGTH + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.extend_from_slice(&self.0); + Ok(()) + } +} + +impl FromBytes for Hash { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + if bytes.len() < HASH_LENGTH { + return Err(bytesrepr::Error::EarlyEndOfStream); + } + let (hash, remainder) = bytes.split_at(HASH_LENGTH); + let hash = <[u8; HASH_LENGTH]>::try_from(hash).map_err(|_| bytesrepr::Error::Formatting)?; + Ok((Hash(hash), remainder)) + } +} diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs new file mode 100644 index 0000000000..54984dcebc --- /dev/null +++ b/types/src/evm/transaction.rs @@ -0,0 +1,493 @@ +use alloc::{ + format, + string::{String, ToString}, + vec::Vec, +}; +use core::fmt::{self, Display, Formatter}; + +use alloy_consensus::{ + transaction::SignerRecoverable, Transaction as AlloyTransaction, TxEnvelope, +}; +use alloy_eips::eip2718::{Decodable2718, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID}; +use alloy_primitives::{Address as AlloyAddress, TxKind as AlloyTxKind, B256}; +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(any(feature = "testing", test))] +use rand::Rng; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +#[cfg(any(feature = "std", test))] +use serde::{de, Deserializer, Serializer}; +use serde::{Deserialize, Serialize}; + +use super::{Address, Hash, HASH_LENGTH}; +#[cfg(any(feature = "testing", test))] +use crate::testing::TestRng; +use crate::{ + bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, + TimeDiff, Timestamp, +}; + +const TRANSACTION_KIND_SERIALIZED_LENGTH: usize = U8_SERIALIZED_LENGTH; + +/// Ethereum transaction type ID for EIP-4844 blob transactions. +pub const EIP4844_TRANSACTION_TYPE_ID: u8 = EIP4844_TX_TYPE_ID; + +/// Ethereum transaction type ID for EIP-7702 set-code transactions. +pub const EIP7702_TRANSACTION_TYPE_ID: u8 = EIP7702_TX_TYPE_ID; + +/// A transaction hash produced by Ethereum transaction RLP hashing rules. +#[derive( + Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct TransactionHash(Hash); + +impl TransactionHash { + /// Creates a transaction hash from raw bytes. + pub const fn new(bytes: [u8; HASH_LENGTH]) -> Self { + TransactionHash(Hash::new(bytes)) + } + + /// Returns the wrapped hash. + pub const fn hash(self) -> Hash { + self.0 + } + + /// Returns the raw bytes backing this hash. + pub const fn value(self) -> [u8; HASH_LENGTH] { + self.0.value() + } + + /// Returns a lower-case hexadecimal string without a `0x` prefix. + pub fn to_hex_string(self) -> String { + self.0.to_hex_string() + } + + /// Returns a random EVM transaction hash. + #[cfg(any(feature = "testing", test))] + pub fn random(rng: &mut TestRng) -> Self { + TransactionHash(Hash::new(rng.gen())) + } +} + +impl AsRef<[u8]> for TransactionHash { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Display for TransactionHash { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, formatter) + } +} + +impl ToBytes for TransactionHash { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + self.0.to_bytes() + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.0.write_bytes(writer) + } +} + +impl FromBytes for TransactionHash { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + Hash::from_bytes(bytes).map(|(hash, remainder)| (TransactionHash(hash), remainder)) + } +} + +/// The supported Ethereum signed transaction envelope kinds. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum TransactionKind { + /// A legacy Ethereum transaction. + Legacy, + /// An EIP-2930 access-list transaction. + Eip2930, + /// An EIP-1559 dynamic-fee transaction. + Eip1559, +} + +impl TransactionKind { + fn tag(self) -> u8 { + match self { + TransactionKind::Legacy => 0, + TransactionKind::Eip2930 => 1, + TransactionKind::Eip1559 => 2, + } + } +} + +impl Display for TransactionKind { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + match self { + TransactionKind::Legacy => formatter.write_str("legacy"), + TransactionKind::Eip2930 => formatter.write_str("eip2930"), + TransactionKind::Eip1559 => formatter.write_str("eip1559"), + } + } +} + +impl ToBytes for TransactionKind { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Ok(vec![self.tag()]) + } + + fn serialized_length(&self) -> usize { + TRANSACTION_KIND_SERIALIZED_LENGTH + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.push(self.tag()); + Ok(()) + } +} + +impl FromBytes for TransactionKind { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let kind = match tag { + 0 => TransactionKind::Legacy, + 1 => TransactionKind::Eip2930, + 2 => TransactionKind::Eip1559, + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((kind, remainder)) + } +} + +/// Errors returned while decoding or validating signed EVM transactions. +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum TransactionError { + /// The signed RLP was malformed or not a supported Ethereum envelope. + Decode(String), + /// The transaction envelope type is not supported by this first-pass executor. + UnsupportedTransactionType(u8), + /// The sender address could not be recovered from the signature. + SenderRecovery(String), + /// Re-decoding the raw RLP produced metadata different from this transaction. + InconsistentEnvelope, +} + +impl Display for TransactionError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + match self { + TransactionError::Decode(error) => { + write!(formatter, "EVM transaction decode error: {error}") + } + TransactionError::UnsupportedTransactionType(kind) => { + write!(formatter, "unsupported EVM transaction type: {kind}") + } + TransactionError::SenderRecovery(error) => { + write!(formatter, "EVM transaction sender recovery error: {error}") + } + TransactionError::InconsistentEnvelope => { + formatter.write_str("EVM transaction fields do not match signed RLP") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TransactionError {} + +/// A decoded signed Ethereum transaction plus Casper envelope metadata. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct Transaction { + raw_signed_rlp: Vec, + timestamp: Timestamp, + ttl: TimeDiff, + hash: TransactionHash, + from: Address, + kind: TransactionKind, + to: Option
, + nonce: u64, + gas_limit: u64, + gas_price: Option, + max_fee_per_gas: u128, + max_priority_fee_per_gas: Option, + value: Hash, + input: Vec, + chain_id: Option, +} + +#[cfg(any(feature = "std", test))] +#[derive(Serialize)] +struct TransactionSerHelper<'a> { + raw_signed_rlp: &'a Vec, + timestamp: Timestamp, + ttl: TimeDiff, +} + +#[cfg(any(feature = "std", test))] +#[derive(Deserialize)] +struct TransactionDeserHelper { + raw_signed_rlp: Vec, + timestamp: Timestamp, + ttl: TimeDiff, +} + +#[cfg(any(feature = "std", test))] +impl Serialize for Transaction { + fn serialize(&self, serializer: S) -> Result { + TransactionSerHelper { + raw_signed_rlp: &self.raw_signed_rlp, + timestamp: self.timestamp, + ttl: self.ttl, + } + .serialize(serializer) + } +} + +#[cfg(any(feature = "std", test))] +impl<'de> Deserialize<'de> for Transaction { + fn deserialize>(deserializer: D) -> Result { + let helper = TransactionDeserHelper::deserialize(deserializer)?; + Transaction::from_signed_rlp(helper.raw_signed_rlp, helper.timestamp, helper.ttl) + .map_err(de::Error::custom) + } +} + +impl Transaction { + /// Decodes a signed Ethereum RLP transaction and attaches Casper envelope metadata. + pub fn from_signed_rlp( + raw_signed_rlp: Vec, + timestamp: Timestamp, + ttl: TimeDiff, + ) -> Result { + if matches!(raw_signed_rlp.first(), Some(&EIP4844_TRANSACTION_TYPE_ID)) { + // EIP-4844 is proto-danksharding/blob transaction support. It + // adds blob-carrying transactions with fields like + // `max_fee_per_blob_gas` and `blob_versioned_hashes`, plus blob + // gas accounting, blob base fee validation, KZG + // commitments/proofs, and separate network representations for + // blob sidecars. Our current transaction type and executor block + // context only model normal EVM call/create execution, not blob + // sidecars, blob fee markets, or block/header blob accounting. + return Err(TransactionError::UnsupportedTransactionType( + raw_signed_rlp[0], + )); + } + + if matches!(raw_signed_rlp.first(), Some(&EIP7702_TRANSACTION_TYPE_ID)) { + // EIP-7702 lets EOAs temporarily behave like they have delegated + // code by attaching an `authorization_list`; the protocol + // processes those authorizations before execution and writes + // delegation indicators like `0xef0100 || address` into account + // code. Our current transaction type does not store an + // authorization list, and the executor/state adapter does not + // implement that pre-execution account-code mutation and nonce + // logic. + return Err(TransactionError::UnsupportedTransactionType( + raw_signed_rlp[0], + )); + } + + let mut encoded = raw_signed_rlp.as_slice(); + let envelope = TxEnvelope::decode_2718(&mut encoded) + .map_err(|error| TransactionError::Decode(format!("{error:?}")))?; + if !encoded.is_empty() { + return Err(TransactionError::Decode( + "trailing bytes after transaction envelope".to_string(), + )); + } + let from = envelope + .recover_signer() + .map_err(|error| TransactionError::SenderRecovery(format!("{error:?}")))?; + let hash = b256_to_hash(*envelope.tx_hash()); + let kind = if envelope.is_legacy() { + TransactionKind::Legacy + } else if envelope.is_eip2930() { + TransactionKind::Eip2930 + } else if envelope.is_eip1559() { + TransactionKind::Eip1559 + } else { + return Err(TransactionError::UnsupportedTransactionType( + envelope.tx_type() as u8, + )); + }; + let to = match envelope.kind() { + AlloyTxKind::Call(address) => Some(alloy_address_to_address(address)), + AlloyTxKind::Create => None, + }; + + Ok(Transaction { + raw_signed_rlp, + timestamp, + ttl, + hash: TransactionHash(hash), + from: alloy_address_to_address(from), + kind, + to, + nonce: envelope.nonce(), + gas_limit: envelope.gas_limit(), + gas_price: envelope.gas_price(), + max_fee_per_gas: envelope.max_fee_per_gas(), + max_priority_fee_per_gas: envelope.max_priority_fee_per_gas(), + value: Hash::new(envelope.value().to_be_bytes()), + input: envelope.input().to_vec(), + chain_id: envelope.chain_id(), + }) + } + + /// Re-decodes the signed RLP and checks that all derived fields still match. + pub fn verify(&self) -> Result<(), TransactionError> { + let decoded = + Transaction::from_signed_rlp(self.raw_signed_rlp.clone(), self.timestamp, self.ttl)?; + if decoded == *self { + Ok(()) + } else { + Err(TransactionError::InconsistentEnvelope) + } + } + + /// Returns the raw signed Ethereum RLP bytes. + pub fn raw_signed_rlp(&self) -> &[u8] { + &self.raw_signed_rlp + } + + /// Returns the Casper envelope timestamp. + pub fn timestamp(&self) -> Timestamp { + self.timestamp + } + + /// Returns the Casper envelope time to live. + pub fn ttl(&self) -> TimeDiff { + self.ttl + } + + /// Returns the Ethereum transaction hash. + pub fn hash(&self) -> TransactionHash { + self.hash + } + + /// Returns the recovered Ethereum sender address. + pub fn from(&self) -> Address { + self.from + } + + /// Returns the signed transaction envelope kind. + pub fn kind(&self) -> TransactionKind { + self.kind + } + + /// Returns the recipient address, or `None` for contract creation. + pub fn to(&self) -> Option
{ + self.to + } + + /// Returns the account nonce. + pub fn nonce(&self) -> u64 { + self.nonce + } + + /// Returns the transaction gas limit. + pub fn gas_limit(&self) -> u64 { + self.gas_limit + } + + /// Returns the legacy gas price, if available. + pub fn gas_price(&self) -> Option { + self.gas_price + } + + /// Returns the maximum fee per gas. + pub fn max_fee_per_gas(&self) -> u128 { + self.max_fee_per_gas + } + + /// Returns the maximum priority fee per gas, if available. + pub fn max_priority_fee_per_gas(&self) -> Option { + self.max_priority_fee_per_gas + } + + /// Returns the transferred value as a 32-byte big-endian word. + pub fn value(&self) -> Hash { + self.value + } + + /// Returns transaction input bytes. + pub fn input(&self) -> &[u8] { + &self.input + } + + /// Returns the Ethereum chain ID encoded in the transaction, if present. + pub fn chain_id(&self) -> Option { + self.chain_id + } + + /// Returns `true` if the transaction has expired at the given timestamp. + pub fn expired(&self, current_instant: Timestamp) -> bool { + current_instant > self.expires() + } + + /// Returns the timestamp of when the transaction expires. + pub fn expires(&self) -> Timestamp { + self.timestamp + self.ttl + } +} + +impl Display for Transaction { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!( + formatter, + "EVM transaction {} from {}", + self.hash.to_hex_string(), + self.from + ) + } +} + +impl ToBytes for Transaction { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.raw_signed_rlp.serialized_length() + + self.timestamp.serialized_length() + + self.ttl.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.raw_signed_rlp.write_bytes(writer)?; + self.timestamp.write_bytes(writer)?; + self.ttl.write_bytes(writer) + } +} + +impl FromBytes for Transaction { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (raw_signed_rlp, remainder) = Vec::::from_bytes(bytes)?; + let (timestamp, remainder) = Timestamp::from_bytes(remainder)?; + let (ttl, remainder) = TimeDiff::from_bytes(remainder)?; + let transaction = Transaction::from_signed_rlp(raw_signed_rlp, timestamp, ttl) + .map_err(|_| bytesrepr::Error::Formatting)?; + Ok((transaction, remainder)) + } +} + +fn alloy_address_to_address(address: AlloyAddress) -> Address { + let mut bytes = [0u8; super::ADDRESS_LENGTH]; + bytes.copy_from_slice(address.as_slice()); + Address::new(bytes) +} + +fn b256_to_hash(hash: B256) -> Hash { + Hash::new(hash.0) +} diff --git a/types/src/execution/transform_kind.rs b/types/src/execution/transform_kind.rs index 461739d4c7..7fdedf3cbe 100644 --- a/types/src/execution/transform_kind.rs +++ b/types/src/execution/transform_kind.rs @@ -208,6 +208,21 @@ impl TransformKindV2 { let found = "EntryPoint".to_string(); Err(StoredValueTypeMismatch::new(expected, found).into()) } + StoredValue::EvmAccount(_) => { + let expected = "Contract or Account".to_string(); + let found = "EvmAccount".to_string(); + Err(StoredValueTypeMismatch::new(expected, found).into()) + } + StoredValue::EvmByteCode(_) => { + let expected = "Contract or Account".to_string(); + let found = "EvmByteCode".to_string(); + Err(StoredValueTypeMismatch::new(expected, found).into()) + } + StoredValue::EvmStorage(_) => { + let expected = "Contract or Account".to_string(); + let found = "EvmStorage".to_string(); + Err(StoredValueTypeMismatch::new(expected, found).into()) + } }, TransformKindV2::Failure(error) => Err(error), } diff --git a/types/src/gens.rs b/types/src/gens.rs index 163384ba9a..2468c7a3ce 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -31,6 +31,7 @@ use crate::{ gens::{public_key_arb_no_system, secret_key_arb_no_system}, }, deploy_info::gens::deploy_info_arb, + evm, global_state::{Pointer, TrieMerkleProof, TrieMerkleProofStep}, package::{EntityVersionKey, EntityVersions, Groups, PackageStatus}, system::{ @@ -985,6 +986,18 @@ pub fn stored_value_arb() -> impl Strategy { message_summary_arb().prop_map(StoredValue::Message), named_key_value_arb().prop_map(StoredValue::NamedKey), collection::vec(any::(), 0..1000).prop_map(StoredValue::RawBytes), + (any::(), u8_slice_32(), uref_arb()).prop_map(|(nonce, code_hash, main_purse)| { + StoredValue::EvmAccount(crate::evm::Account::new( + nonce, + crate::evm::Hash::new(code_hash), + main_purse, + )) + }), + collection::vec(any::(), 0..1000) + .prop_map(|bytes| StoredValue::EvmByteCode(crate::evm::ByteCode::new(bytes))), + u8_slice_32().prop_map(|value| { + StoredValue::EvmStorage(crate::evm::StorageValue::new(crate::evm::Hash::new(value))) + }), ] .prop_map(|stored_value| // The following match statement is here only to make sure @@ -1011,6 +1024,9 @@ pub fn stored_value_arb() -> impl Strategy { StoredValue::Prepayment(_) => stored_value, StoredValue::EntryPoint(_) => stored_value, StoredValue::RawBytes(_) => stored_value, + StoredValue::EvmAccount(_) => stored_value, + StoredValue::EvmByteCode(_) => stored_value, + StoredValue::EvmStorage(_) => stored_value, }) } @@ -1303,6 +1319,8 @@ pub fn initiator_addr_arb() -> impl Strategy { prop_oneof![ public_key_arb_no_system().prop_map(InitiatorAddr::PublicKey), u2_slice_32().prop_map(|hash| InitiatorAddr::AccountHash(AccountHash::new(hash))), + array::uniform20(any::()) + .prop_map(|address| InitiatorAddr::EvmAddress(evm::Address::new(address))), ] } diff --git a/types/src/key.rs b/types/src/key.rs index 141a278838..bbcdbb9d71 100644 --- a/types/src/key.rs +++ b/types/src/key.rs @@ -48,6 +48,10 @@ use crate::{ contract_messages::{self, MessageAddr, TopicNameHash, TOPIC_NAME_HASH_LENGTH}, contract_wasm::ContractWasmHash, contracts::{ContractHash, ContractPackageHash}, + evm::{ + Address as EvmAddress, Hash as EvmHash, StorageAddr as EvmStorageAddr, + ADDRESS_LENGTH as EVM_ADDRESS_LENGTH, + }, package::PackageHash, system::{ auction::{BidAddr, BidAddrTag}, @@ -80,6 +84,9 @@ const BLOCK_GLOBAL_PROTOCOL_VERSION_PREFIX: &str = "block-protocol-version-"; const BLOCK_GLOBAL_ADDRESSABLE_ENTITY_PREFIX: &str = "block-addressable-entity-"; const STATE_PREFIX: &str = "state-"; const REWARDS_HANDLING_PREFIX: &str = "rewards-handling-"; +const EVM_ACCOUNT_PREFIX: &str = "evm-account-"; +const EVM_BYTE_CODE_PREFIX: &str = "evm-byte-code-"; +const EVM_STORAGE_PREFIX: &str = "evm-storage-"; /// The number of bytes in a Blake2b hash pub const BLAKE2B_DIGEST_LENGTH: usize = 32; @@ -118,6 +125,10 @@ const KEY_CHECKSUM_REGISTRY_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + PADDING_BYTES.len(); const KEY_REWARDS_HANDLING_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + PADDING_BYTES.len(); +const KEY_EVM_ACCOUNT_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + EVM_ADDRESS_LENGTH; +const KEY_EVM_BYTE_CODE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_HASH_LENGTH; +const KEY_EVM_STORAGE_SERIALIZED_LENGTH: usize = + KEY_ID_SERIALIZED_LENGTH + EVM_ADDRESS_LENGTH + KEY_HASH_LENGTH; const KEY_PACKAGE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + 32; const KEY_MESSAGE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + U8_SERIALIZED_LENGTH @@ -167,13 +178,16 @@ pub enum KeyTag { EntryPoint = 23, State = 24, RewardsHandling = 25, + EvmAccount = 26, + EvmByteCode = 27, + EvmStorage = 28, } impl KeyTag { /// Returns a random `KeyTag`. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..=23) { + match rng.gen_range(0..=28) { 0 => KeyTag::Account, 1 => KeyTag::Hash, 2 => KeyTag::URef, @@ -199,7 +213,11 @@ impl KeyTag { 22 => KeyTag::BalanceHold, 23 => KeyTag::EntryPoint, 24 => KeyTag::State, - _ => panic!(), + 25 => KeyTag::RewardsHandling, + 26 => KeyTag::EvmAccount, + 27 => KeyTag::EvmByteCode, + 28 => KeyTag::EvmStorage, + _ => unreachable!(), } } } @@ -233,6 +251,9 @@ impl Display for KeyTag { KeyTag::State => write!(f, "State"), KeyTag::EntryPoint => write!(f, "EntryPoint"), KeyTag::RewardsHandling => write!(f, "RewardsHandling"), + KeyTag::EvmAccount => write!(f, "EvmAccount"), + KeyTag::EvmByteCode => write!(f, "EvmByteCode"), + KeyTag::EvmStorage => write!(f, "EvmStorage"), } } } @@ -284,6 +305,9 @@ impl FromBytes for KeyTag { tag if tag == KeyTag::EntryPoint as u8 => KeyTag::EntryPoint, tag if tag == KeyTag::State as u8 => KeyTag::State, tag if tag == KeyTag::RewardsHandling as u8 => KeyTag::RewardsHandling, + tag if tag == KeyTag::EvmAccount as u8 => KeyTag::EvmAccount, + tag if tag == KeyTag::EvmByteCode as u8 => KeyTag::EvmByteCode, + tag if tag == KeyTag::EvmStorage as u8 => KeyTag::EvmStorage, _ => return Err(Error::Formatting), }; Ok((tag, rem)) @@ -350,6 +374,12 @@ pub enum Key { State(EntityAddr), /// A `Key` under which we store rewards handling information RewardsHandling, + /// A `Key` under which EVM account metadata is stored. + EvmAccount(EvmAddress), + /// A `Key` under which EVM contract bytecode is stored by hash. + EvmByteCode(EvmHash), + /// A `Key` under which one EVM contract storage slot is stored. + EvmStorage(EvmStorageAddr), } #[cfg(feature = "json-schema")] @@ -424,6 +454,12 @@ pub enum FromStrError { EntryPoint(String), /// State key parse error. State(String), + /// EVM account key parse error. + EvmAccount(String), + /// EVM bytecode key parse error. + EvmByteCode(String), + /// EVM storage key parse error. + EvmStorage(String), RewardsHandling(String), /// Unknown prefix. UnknownPrefix, @@ -510,6 +546,15 @@ impl Display for FromStrError { } FromStrError::UnknownPrefix => write!(f, "unknown prefix for key"), FromStrError::State(error) => write!(f, "state-key from string error: {}", error), + FromStrError::EvmAccount(error) => { + write!(f, "evm-account-key from string error: {}", error) + } + FromStrError::EvmByteCode(error) => { + write!(f, "evm-byte-code-key from string error: {}", error) + } + FromStrError::EvmStorage(error) => { + write!(f, "evm-storage-key from string error: {}", error) + } FromStrError::RewardsHandling(error) => { write!(f, "rewards-handling-key from string error: {}", error) @@ -549,6 +594,9 @@ impl Key { Key::EntryPoint(_) => String::from("Key::EntryPoint"), Key::State(_) => String::from("Key::State"), Key::RewardsHandling => String::from("Key::RewardsHandling"), + Key::EvmAccount(_) => String::from("Key::EvmAccount"), + Key::EvmByteCode(_) => String::from("Key::EvmByteCode"), + Key::EvmStorage(_) => String::from("Key::EvmStorage"), } } @@ -684,6 +732,20 @@ impl Key { base16::encode_lower(&PADDING_BYTES) ) } + Key::EvmAccount(address) => { + format!("{}{}", EVM_ACCOUNT_PREFIX, address.to_hex_string()) + } + Key::EvmByteCode(hash) => { + format!("{}{}", EVM_BYTE_CODE_PREFIX, hash.to_hex_string()) + } + Key::EvmStorage(addr) => { + format!( + "{}{}{}", + EVM_STORAGE_PREFIX, + addr.address().to_hex_string(), + addr.slot().to_hex_string() + ) + } } } @@ -1011,6 +1073,42 @@ impl Key { return Ok(Key::RewardsHandling); } + if let Some(hex) = input.strip_prefix(EVM_ACCOUNT_PREFIX) { + let bytes = checksummed_hex::decode(hex) + .map_err(|error| FromStrError::EvmAccount(error.to_string()))?; + let address = <[u8; EVM_ADDRESS_LENGTH]>::try_from(bytes.as_ref()) + .map_err(|error| FromStrError::EvmAccount(error.to_string()))?; + return Ok(Key::EvmAccount(EvmAddress::new(address))); + } + + if let Some(hex) = input.strip_prefix(EVM_BYTE_CODE_PREFIX) { + let bytes = checksummed_hex::decode(hex) + .map_err(|error| FromStrError::EvmByteCode(error.to_string()))?; + let hash = <[u8; KEY_HASH_LENGTH]>::try_from(bytes.as_ref()) + .map_err(|error| FromStrError::EvmByteCode(error.to_string()))?; + return Ok(Key::EvmByteCode(EvmHash::new(hash))); + } + + if let Some(hex) = input.strip_prefix(EVM_STORAGE_PREFIX) { + let bytes = checksummed_hex::decode(hex) + .map_err(|error| FromStrError::EvmStorage(error.to_string()))?; + if bytes.len() != EVM_ADDRESS_LENGTH + KEY_HASH_LENGTH { + return Err(FromStrError::EvmStorage(format!( + "expected {} bytes, got {}", + EVM_ADDRESS_LENGTH + KEY_HASH_LENGTH, + bytes.len() + ))); + } + let address = <[u8; EVM_ADDRESS_LENGTH]>::try_from(&bytes[..EVM_ADDRESS_LENGTH]) + .map_err(|error| FromStrError::EvmStorage(error.to_string()))?; + let slot = <[u8; KEY_HASH_LENGTH]>::try_from(&bytes[EVM_ADDRESS_LENGTH..]) + .map_err(|error| FromStrError::EvmStorage(error.to_string()))?; + return Ok(Key::EvmStorage(EvmStorageAddr::new( + EvmAddress::new(address), + EvmHash::new(slot), + ))); + } + Err(FromStrError::UnknownPrefix) } @@ -1151,6 +1249,24 @@ impl Key { } } + /// Returns the EVM address if this key stores an EVM account. + pub fn as_evm_account(&self) -> Option<&EvmAddress> { + if let Self::EvmAccount(address) = self { + Some(address) + } else { + None + } + } + + /// Returns the EVM storage owner and slot if this key stores an EVM storage value. + pub fn as_evm_storage(&self) -> Option<&EvmStorageAddr> { + if let Self::EvmStorage(addr) = self { + Some(addr) + } else { + None + } + } + /// Casts a [`Key::URef`] to a [`Key::Hash`] pub fn uref_to_hash(&self) -> Option { let uref = self.as_uref()?; @@ -1329,7 +1445,10 @@ impl Key { | Key::Dictionary(_) | Key::Message(_) | Key::BlockGlobal(_) - | Key::EntryPoint(_) => true, + | Key::EntryPoint(_) + | Key::EvmAccount(_) + | Key::EvmByteCode(_) + | Key::EvmStorage(_) => true, _ => false, }; if !ret { @@ -1487,6 +1606,11 @@ impl Display for Key { "Key::RewardsHandling({})", base16::encode_lower(&PADDING_BYTES), ), + Key::EvmAccount(address) => write!(f, "Key::EvmAccount({})", address), + Key::EvmByteCode(hash) => write!(f, "Key::EvmByteCode({})", hash), + Key::EvmStorage(addr) => { + write!(f, "Key::EvmStorage({}-{})", addr.address(), addr.slot()) + } } } } @@ -1526,6 +1650,9 @@ impl Tagged for Key { Key::EntryPoint(_) => KeyTag::EntryPoint, Key::State(_) => KeyTag::State, Key::RewardsHandling => KeyTag::RewardsHandling, + Key::EvmAccount(_) => KeyTag::EvmAccount, + Key::EvmByteCode(_) => KeyTag::EvmByteCode, + Key::EvmStorage(_) => KeyTag::EvmStorage, } } } @@ -1644,6 +1771,9 @@ impl ToBytes for Key { } Key::State(entity_addr) => KEY_ID_SERIALIZED_LENGTH + entity_addr.serialized_length(), Key::RewardsHandling => KEY_REWARDS_HANDLING_SERIALIZED_LENGTH, + Key::EvmAccount(_) => KEY_EVM_ACCOUNT_SERIALIZED_LENGTH, + Key::EvmByteCode(_) => KEY_EVM_BYTE_CODE_SERIALIZED_LENGTH, + Key::EvmStorage(_) => KEY_EVM_STORAGE_SERIALIZED_LENGTH, } } @@ -1679,6 +1809,9 @@ impl ToBytes for Key { Key::BalanceHold(balance_hold_addr) => balance_hold_addr.write_bytes(writer), Key::EntryPoint(entry_point_addr) => entry_point_addr.write_bytes(writer), Key::State(entity_addr) => entity_addr.write_bytes(writer), + Key::EvmAccount(address) => address.write_bytes(writer), + Key::EvmByteCode(hash) => hash.write_bytes(writer), + Key::EvmStorage(addr) => addr.write_bytes(writer), } } } @@ -1801,6 +1934,18 @@ impl FromBytes for Key { let (_, rem) = <[u8; 32]>::from_bytes(remainder)?; Ok((Key::RewardsHandling, rem)) } + KeyTag::EvmAccount => { + let (address, rem) = EvmAddress::from_bytes(remainder)?; + Ok((Key::EvmAccount(address), rem)) + } + KeyTag::EvmByteCode => { + let (hash, rem) = EvmHash::from_bytes(remainder)?; + Ok((Key::EvmByteCode(hash), rem)) + } + KeyTag::EvmStorage => { + let (addr, rem) = EvmStorageAddr::from_bytes(remainder)?; + Ok((Key::EvmStorage(addr), rem)) + } } } } @@ -1836,13 +1981,16 @@ fn please_add_to_distribution_impl(key: Key) { Key::EntryPoint(_) => unimplemented!(), Key::State(_) => unimplemented!(), Key::RewardsHandling => unimplemented!(), + Key::EvmAccount(_) => unimplemented!(), + Key::EvmByteCode(_) => unimplemented!(), + Key::EvmStorage(_) => unimplemented!(), } } #[cfg(any(feature = "testing", test))] impl Distribution for Standard { fn sample(&self, rng: &mut R) -> Key { - match rng.gen_range(0..=24) { + match rng.gen_range(0..=28) { 0 => Key::Account(rng.gen()), 1 => Key::Hash(rng.gen()), 2 => Key::URef(rng.gen()), @@ -1868,6 +2016,13 @@ impl Distribution for Standard { 22 => Key::BalanceHold(rng.gen()), 23 => Key::EntryPoint(rng.gen()), 24 => Key::State(rng.gen()), + 25 => Key::RewardsHandling, + 26 => Key::EvmAccount(EvmAddress::new(rng.gen())), + 27 => Key::EvmByteCode(EvmHash::new(rng.gen())), + 28 => Key::EvmStorage(EvmStorageAddr::new( + EvmAddress::new(rng.gen()), + EvmHash::new(rng.gen()), + )), _ => unreachable!(), } } @@ -1905,6 +2060,9 @@ mod serde_helpers { EntryPoint(&'a EntryPointAddr), State(&'a EntityAddr), RewardsHandling, + EvmAccount(&'a EvmAddress), + EvmByteCode(&'a EvmHash), + EvmStorage(&'a EvmStorageAddr), } #[derive(Deserialize)] @@ -1936,6 +2094,9 @@ mod serde_helpers { EntryPoint(EntryPointAddr), State(EntityAddr), RewardsHandling, + EvmAccount(EvmAddress), + EvmByteCode(EvmHash), + EvmStorage(EvmStorageAddr), } impl<'a> From<&'a Key> for BinarySerHelper<'a> { @@ -1971,6 +2132,9 @@ mod serde_helpers { Key::EntryPoint(entry_point_addr) => BinarySerHelper::EntryPoint(entry_point_addr), Key::State(entity_addr) => BinarySerHelper::State(entity_addr), Key::RewardsHandling => BinarySerHelper::RewardsHandling, + Key::EvmAccount(address) => BinarySerHelper::EvmAccount(address), + Key::EvmByteCode(hash) => BinarySerHelper::EvmByteCode(hash), + Key::EvmStorage(addr) => BinarySerHelper::EvmStorage(addr), } } } @@ -2010,6 +2174,9 @@ mod serde_helpers { } BinaryDeserHelper::State(entity_addr) => Key::State(entity_addr), BinaryDeserHelper::RewardsHandling => Key::RewardsHandling, + BinaryDeserHelper::EvmAccount(address) => Key::EvmAccount(address), + BinaryDeserHelper::EvmByteCode(hash) => Key::EvmByteCode(hash), + BinaryDeserHelper::EvmStorage(addr) => Key::EvmStorage(addr), } } } diff --git a/types/src/lib.rs b/types/src/lib.rs index 5b038d9f21..7581816783 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -44,6 +44,7 @@ mod deploy_info; mod digest; mod display_iter; mod era_id; +pub mod evm; pub mod execution; #[cfg(any(feature = "std-fs-io", test))] pub mod file_utils; diff --git a/types/src/stored_value.rs b/types/src/stored_value.rs index 00e307b0b2..1f09e26f11 100644 --- a/types/src/stored_value.rs +++ b/types/src/stored_value.rs @@ -22,6 +22,7 @@ use crate::{ contract_messages::{MessageChecksum, MessageTopicSummary}, contract_wasm::ContractWasm, contracts::{Contract, ContractPackage}, + evm, package::Package, system::{ auction::{Bid, BidKind, EraInfo, Unbond, UnbondingPurse, WithdrawPurse}, @@ -78,6 +79,12 @@ pub enum StoredValueTag { EntryPoint = 19, /// Raw bytes. RawBytes = 20, + /// EVM account metadata. + EvmAccount = 21, + /// EVM contract bytecode. + EvmByteCode = 22, + /// EVM contract storage value. + EvmStorage = 23, } /// A value stored in Global State. @@ -133,6 +140,12 @@ pub enum StoredValue { /// Raw bytes. Similar to a [`crate::StoredValue::CLValue`] but does not incur overhead of a /// [`crate::CLValue`] and [`crate::CLType`]. RawBytes(#[cfg_attr(feature = "json-schema", schemars(with = "String"))] Vec), + /// EVM account metadata. + EvmAccount(evm::Account), + /// EVM contract bytecode, addressed by [`crate::Key::EvmByteCode`]. + EvmByteCode(evm::ByteCode), + /// EVM storage value for a single contract slot. + EvmStorage(evm::StorageValue), } impl StoredValue { @@ -292,6 +305,30 @@ impl StoredValue { } } + /// Returns EVM account metadata if this is an `EvmAccount` variant. + pub fn as_evm_account(&self) -> Option<&evm::Account> { + match self { + StoredValue::EvmAccount(account) => Some(account), + _ => None, + } + } + + /// Returns EVM bytecode if this is an `EvmByteCode` variant. + pub fn as_evm_byte_code(&self) -> Option<&evm::ByteCode> { + match self { + StoredValue::EvmByteCode(byte_code) => Some(byte_code), + _ => None, + } + } + + /// Returns an EVM storage value if this is an `EvmStorage` variant. + pub fn as_evm_storage(&self) -> Option<&evm::StorageValue> { + match self { + StoredValue::EvmStorage(value) => Some(value), + _ => None, + } + } + /// Returns a reference to the wrapped `EntryPointValue` if this is a `EntryPointValue` variant. pub fn as_entry_point_value(&self) -> Option<&EntryPointValue> { match self { @@ -446,6 +483,9 @@ impl StoredValue { StoredValue::Prepayment(_) => "Prepayment".to_string(), StoredValue::EntryPoint(_) => "EntryPoint".to_string(), StoredValue::RawBytes(_) => "RawBytes".to_string(), + StoredValue::EvmAccount(_) => "EvmAccount".to_string(), + StoredValue::EvmByteCode(_) => "EvmByteCode".to_string(), + StoredValue::EvmStorage(_) => "EvmStorage".to_string(), } } @@ -473,6 +513,9 @@ impl StoredValue { StoredValue::Prepayment(_) => StoredValueTag::Prepayment, StoredValue::EntryPoint(_) => StoredValueTag::EntryPoint, StoredValue::RawBytes(_) => StoredValueTag::RawBytes, + StoredValue::EvmAccount(_) => StoredValueTag::EvmAccount, + StoredValue::EvmByteCode(_) => StoredValueTag::EvmByteCode, + StoredValue::EvmStorage(_) => StoredValueTag::EvmStorage, } } @@ -781,6 +824,9 @@ impl ToBytes for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.serialized_length(), StoredValue::EntryPoint(entry_point_value) => entry_point_value.serialized_length(), StoredValue::RawBytes(bytes) => bytes.serialized_length(), + StoredValue::EvmAccount(account) => account.serialized_length(), + StoredValue::EvmByteCode(byte_code) => byte_code.serialized_length(), + StoredValue::EvmStorage(value) => value.serialized_length(), } } @@ -810,6 +856,9 @@ impl ToBytes for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.write_bytes(writer), StoredValue::EntryPoint(entry_point_value) => entry_point_value.write_bytes(writer), StoredValue::RawBytes(bytes) => bytes.write_bytes(writer), + StoredValue::EvmAccount(account) => account.write_bytes(writer), + StoredValue::EvmByteCode(byte_code) => byte_code.write_bytes(writer), + StoredValue::EvmStorage(value) => value.write_bytes(writer), } } } @@ -881,6 +930,14 @@ impl FromBytes for StoredValue { let (bytes, remainder) = Bytes::from_bytes(remainder)?; Ok((StoredValue::RawBytes(bytes.into()), remainder)) } + tag if tag == StoredValueTag::EvmAccount as u8 => evm::Account::from_bytes(remainder) + .map(|(account, remainder)| (StoredValue::EvmAccount(account), remainder)), + tag if tag == StoredValueTag::EvmByteCode as u8 => evm::ByteCode::from_bytes(remainder) + .map(|(byte_code, remainder)| (StoredValue::EvmByteCode(byte_code), remainder)), + tag if tag == StoredValueTag::EvmStorage as u8 => { + evm::StorageValue::from_bytes(remainder) + .map(|(value, remainder)| (StoredValue::EvmStorage(value), remainder)) + } _ => Err(Error::Formatting), } } @@ -924,6 +981,9 @@ pub mod serde_helpers { Prepayment(&'a PrepaymentKind), EntryPoint(&'a EntryPointValue), RawBytes(Bytes), + EvmAccount(&'a evm::Account), + EvmByteCode(Bytes), + EvmStorage(&'a evm::StorageValue), } /// A value stored in Global State. @@ -980,6 +1040,12 @@ pub mod serde_helpers { /// Raw bytes. Similar to a [`crate::StoredValue::CLValue`] but does not incur overhead of /// a [`crate::CLValue`] and [`crate::CLType`]. RawBytes(Bytes), + /// EVM account metadata. + EvmAccount(evm::Account), + /// EVM contract bytecode. + EvmByteCode(Bytes), + /// EVM contract storage value. + EvmStorage(evm::StorageValue), } impl<'a> From<&'a StoredValue> for HumanReadableSerHelper<'a> { @@ -1018,6 +1084,11 @@ pub mod serde_helpers { StoredValue::RawBytes(bytes) => { HumanReadableSerHelper::RawBytes(bytes.as_slice().into()) } + StoredValue::EvmAccount(account) => HumanReadableSerHelper::EvmAccount(account), + StoredValue::EvmByteCode(byte_code) => { + HumanReadableSerHelper::EvmByteCode(byte_code.as_bytes().into()) + } + StoredValue::EvmStorage(value) => HumanReadableSerHelper::EvmStorage(value), } } } @@ -1085,6 +1156,11 @@ pub mod serde_helpers { HumanReadableDeserHelper::Prepayment(prepayment_kind) => { StoredValue::Prepayment(prepayment_kind) } + HumanReadableDeserHelper::EvmAccount(account) => StoredValue::EvmAccount(account), + HumanReadableDeserHelper::EvmByteCode(bytes) => { + StoredValue::EvmByteCode(evm::ByteCode::from(bytes)) + } + HumanReadableDeserHelper::EvmStorage(value) => StoredValue::EvmStorage(value), }) } } diff --git a/types/src/transaction.rs b/types/src/transaction.rs index d01ef03978..4d2b56e418 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -23,6 +23,7 @@ mod transfer_target; #[cfg(feature = "json-schema")] use crate::URef; use alloc::{ + boxed::Box, collections::BTreeSet, string::{String, ToString}, vec::Vec, @@ -57,7 +58,7 @@ use crate::testing::TestRng; use crate::{ account::AccountHash, bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - Digest, Phase, SecretKey, TimeDiff, Timestamp, + evm, Digest, Phase, SecretKey, TimeDiff, Timestamp, }; #[cfg(any(feature = "std", test))] use crate::{Chainspec, Gas, Motes, TransactionV1Config}; @@ -96,6 +97,7 @@ pub use transfer_target::TransferTarget; const DEPLOY_TAG: u8 = 0; const V1_TAG: u8 = 1; +const EVM_TAG: u8 = 2; #[cfg(feature = "json-schema")] pub(super) static TRANSACTION: Lazy = Lazy::new(|| { @@ -147,6 +149,8 @@ pub enum Transaction { schemars(with = "TransactionV1Json") )] V1(TransactionV1), + /// An EVM signed RLP transaction. + Evm(Box), } impl Transaction { @@ -160,11 +164,17 @@ impl Transaction { Transaction::V1(v1) } + /// EVM variant ctor. + pub fn from_evm(evm: evm::Transaction) -> Self { + Transaction::Evm(Box::new(evm)) + } + /// Returns the `TransactionHash` identifying this transaction. pub fn hash(&self) -> TransactionHash { match self { Transaction::Deploy(deploy) => TransactionHash::from(*deploy.hash()), Transaction::V1(txn) => TransactionHash::from(*txn.hash()), + Transaction::Evm(txn) => TransactionHash::from(txn.as_ref().hash()), } } @@ -173,6 +183,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.serialized_length(), Transaction::V1(v1) => v1.serialized_length(), + Transaction::Evm(txn) => txn.serialized_length(), } } @@ -181,6 +192,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.header().timestamp(), Transaction::V1(v1) => v1.payload().timestamp(), + Transaction::Evm(txn) => txn.timestamp(), } } @@ -189,6 +201,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.header().ttl(), Transaction::V1(v1) => v1.payload().ttl(), + Transaction::Evm(txn) => txn.ttl(), } } @@ -198,6 +211,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.is_valid().map_err(Into::into), Transaction::V1(v1) => v1.verify().map_err(Into::into), + Transaction::Evm(txn) => txn.verify().map_err(Into::into), } } @@ -206,6 +220,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.sign(secret_key), Transaction::V1(v1) => v1.sign(secret_key), + Transaction::Evm(_) => {} } } @@ -214,6 +229,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.approvals().clone(), Transaction::V1(v1) => v1.approvals().clone(), + Transaction::Evm(_) => BTreeSet::new(), } } @@ -222,6 +238,7 @@ impl Transaction { let approvals_hash = match self { Transaction::Deploy(deploy) => deploy.compute_approvals_hash()?, Transaction::V1(txn) => txn.compute_approvals_hash()?, + Transaction::Evm(_) => ApprovalsHash::compute(&BTreeSet::new())?, }; Ok(approvals_hash) } @@ -231,6 +248,10 @@ impl Transaction { match self { Transaction::Deploy(txn) => txn.chain_name().to_string(), Transaction::V1(txn) => txn.chain_name().to_string(), + Transaction::Evm(txn) => txn + .chain_id() + .map(|chain_id| format!("evm-chain-{chain_id}")) + .unwrap_or_else(|| "evm-chain".to_string()), } } @@ -248,6 +269,7 @@ impl Transaction { } => *standard_payment, _ => true, }, + Transaction::Evm(_) => true, } } @@ -271,6 +293,14 @@ impl Transaction { }); TransactionId::new(TransactionHash::V1(txn_hash), approvals_hash) } + Transaction::Evm(txn) => { + let approvals_hash = + ApprovalsHash::compute(&BTreeSet::new()).unwrap_or_else(|error| { + error!(%error, "failed to serialize empty EVM approvals"); + ApprovalsHash::from(Digest::default()) + }); + TransactionId::new(TransactionHash::Evm(txn.as_ref().hash()), approvals_hash) + } } } @@ -279,6 +309,23 @@ impl Transaction { match self { Transaction::Deploy(deploy) => InitiatorAddr::PublicKey(deploy.account().clone()), Transaction::V1(txn) => txn.initiator_addr().clone(), + Transaction::Evm(txn) => InitiatorAddr::EvmAddress(txn.from()), + } + } + + /// Returns the native EVM initiator address for an EVM transaction. + pub fn evm_initiator_addr(&self) -> Option { + match self { + Transaction::Evm(txn) => Some(txn.from()), + _ => None, + } + } + + /// Returns the native EVM transaction hash for an EVM transaction. + pub fn evm_hash(&self) -> Option { + match self { + Transaction::Evm(txn) => Some(txn.as_ref().hash()), + _ => None, } } @@ -287,6 +334,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.expired(current_instant), Transaction::V1(txn) => txn.expired(current_instant), + Transaction::Evm(txn) => txn.expired(current_instant), } } @@ -295,6 +343,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.header().expires(), Transaction::V1(txn) => txn.payload().expires(), + Transaction::Evm(txn) => txn.expires(), } } @@ -311,6 +360,7 @@ impl Transaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + Transaction::Evm(_) => BTreeSet::new(), } } @@ -325,6 +375,7 @@ impl Transaction { Transaction::V1(transaction_v1) => { Transaction::V1(transaction_v1.with_approvals(approvals)) } + Transaction::Evm(txn) => Transaction::Evm(txn), } } @@ -333,6 +384,15 @@ impl Transaction { match self { Transaction::Deploy(_) => None, Transaction::V1(v1) => Some(v1), + Transaction::Evm(_) => None, + } + } + + /// Get the wrapped EVM transaction. + pub fn as_evm(&self) -> Option<&evm::Transaction> { + match self { + Transaction::Evm(evm) => Some(evm), + _ => None, } } @@ -349,6 +409,7 @@ impl Transaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + Transaction::Evm(_) => BTreeSet::new(), } } @@ -357,6 +418,7 @@ impl Transaction { match self { Transaction::Deploy(_) => true, Transaction::V1(_) => false, + Transaction::Evm(_) => false, } } @@ -412,6 +474,7 @@ impl Transaction { Err(err) => Err(err), } } + Transaction::Evm(txn) => Ok(Gas::new(txn.gas_limit())), } } @@ -442,6 +505,10 @@ impl Transaction { .gas_cost(chainspec, lane_id, gas_price) .map_err(InvalidTransaction::from) } + Transaction::Evm(txn) => Ok(Motes::new( + txn.gas_limit() + .saturating_mul(txn.max_fee_per_gas().min(u64::MAX as u128) as u64), + )), } } @@ -505,6 +572,8 @@ enum TransactionJson { /// A version 1 transaction. #[serde(rename = "Version1")] V1(TransactionV1Json), + /// An EVM signed RLP transaction. + Evm(Box), } #[cfg(any(feature = "std", test))] @@ -530,6 +599,7 @@ impl TryFrom for Transaction { )) }) } + TransactionJson::Evm(evm) => Ok(Transaction::Evm(evm)), } } } @@ -548,6 +618,7 @@ impl TryFrom for TransactionJson { error )) }), + Transaction::Evm(evm) => Ok(TransactionJson::Evm(evm)), } } } @@ -583,6 +654,12 @@ impl From for Transaction { } } +impl From for Transaction { + fn from(txn: evm::Transaction) -> Self { + Self::Evm(Box::new(txn)) + } +} + impl ToBytes for Transaction { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut buffer = bytesrepr::allocate_buffer(self)?; @@ -595,6 +672,7 @@ impl ToBytes for Transaction { + match self { Transaction::Deploy(deploy) => deploy.serialized_length(), Transaction::V1(txn) => txn.serialized_length(), + Transaction::Evm(txn) => txn.serialized_length(), } } @@ -608,6 +686,10 @@ impl ToBytes for Transaction { V1_TAG.write_bytes(writer)?; txn.write_bytes(writer) } + Transaction::Evm(txn) => { + EVM_TAG.write_bytes(writer)?; + txn.write_bytes(writer) + } } } } @@ -624,6 +706,10 @@ impl FromBytes for Transaction { let (txn, remainder) = TransactionV1::from_bytes(remainder)?; Ok((Transaction::V1(txn), remainder)) } + EVM_TAG => { + let (txn, remainder) = evm::Transaction::from_bytes(remainder)?; + Ok((Transaction::Evm(Box::new(txn)), remainder)) + } _ => Err(bytesrepr::Error::Formatting), } } @@ -634,6 +720,7 @@ impl Display for Transaction { match self { Transaction::Deploy(deploy) => Display::fmt(deploy, formatter), Transaction::V1(txn) => Display::fmt(txn, formatter), + Transaction::Evm(txn) => Display::fmt(txn, formatter), } } } diff --git a/types/src/transaction/deploy.rs b/types/src/transaction/deploy.rs index 3b10aa5b97..a08c2dde8d 100644 --- a/types/src/transaction/deploy.rs +++ b/types/src/transaction/deploy.rs @@ -214,7 +214,7 @@ impl Deploy { let account = match initiator_addr_and_secret_key.initiator_addr() { InitiatorAddr::PublicKey(public_key) => public_key, - InitiatorAddr::AccountHash(_) => unreachable!(), + InitiatorAddr::AccountHash(_) | InitiatorAddr::EvmAddress(_) => unreachable!(), }; let dependencies = dependencies.into_iter().unique().collect(); diff --git a/types/src/transaction/error.rs b/types/src/transaction/error.rs index 6676a2e8ef..4adb4b8709 100644 --- a/types/src/transaction/error.rs +++ b/types/src/transaction/error.rs @@ -1,4 +1,4 @@ -use crate::InvalidDeploy; +use crate::{evm, InvalidDeploy}; use core::fmt::{Display, Formatter}; #[cfg(feature = "datasize")] use datasize::DataSize; @@ -20,6 +20,8 @@ pub enum InvalidTransaction { Deploy(InvalidDeploy), /// V1 transactions. V1(InvalidTransactionV1), + /// EVM transactions. + Evm(evm::TransactionError), } impl From for InvalidTransaction { @@ -34,12 +36,19 @@ impl From for InvalidTransaction { } } +impl From for InvalidTransaction { + fn from(value: evm::TransactionError) -> Self { + Self::Evm(value) + } +} + #[cfg(feature = "std")] impl StdError for InvalidTransaction { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { InvalidTransaction::Deploy(deploy) => deploy.source(), InvalidTransaction::V1(v1) => v1.source(), + InvalidTransaction::Evm(evm) => Some(evm), } } } @@ -49,6 +58,7 @@ impl Display for InvalidTransaction { match self { InvalidTransaction::Deploy(inner) => Display::fmt(inner, f), InvalidTransaction::V1(inner) => Display::fmt(inner, f), + InvalidTransaction::Evm(inner) => Display::fmt(inner, f), } } } diff --git a/types/src/transaction/initiator_addr.rs b/types/src/transaction/initiator_addr.rs index b9e6c40046..e59cddc8e6 100644 --- a/types/src/transaction/initiator_addr.rs +++ b/types/src/transaction/initiator_addr.rs @@ -7,6 +7,7 @@ use crate::{ Error::{self, Formatting}, FromBytes, ToBytes, }, + evm, transaction::serialization::CalltableSerializationEnvelopeBuilder, AsymmetricType, PublicKey, }; @@ -28,13 +29,16 @@ const PUBLIC_KEY_FIELD_INDEX: u16 = 1; const ACCOUNT_HASH_VARIANT_TAG: u8 = 1; const ACCOUNT_HASH_FIELD_INDEX: u16 = 1; +const EVM_ADDRESS_VARIANT_TAG: u8 = 2; +const EVM_ADDRESS_FIELD_INDEX: u16 = 1; + /// The address of the initiator of a [`crate::Transaction`]. #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr( feature = "json-schema", derive(JsonSchema), - schemars(description = "The address of the initiator of a TransactionV1.") + schemars(description = "The address of the initiator of a transaction.") )] #[serde(deny_unknown_fields)] pub enum InitiatorAddr { @@ -42,23 +46,39 @@ pub enum InitiatorAddr { PublicKey(PublicKey), /// The account hash derived from the public key of the initiator. AccountHash(AccountHash), + /// The EVM-native address recovered from the signed Ethereum transaction. + EvmAddress(evm::Address), } impl InitiatorAddr { - /// Gets the account hash. - pub fn account_hash(&self) -> AccountHash { + /// Returns the Casper account hash, if this initiator has one. + /// + /// EVM initiators do not have a native Casper account hash. EVM-aware code + /// should use [`InitiatorAddr::evm_address`] or + /// [`crate::Transaction::evm_initiator_addr`]. + pub fn account_hash(&self) -> Option { + match self { + InitiatorAddr::PublicKey(public_key) => Some(public_key.to_account_hash()), + InitiatorAddr::AccountHash(hash) => Some(*hash), + InitiatorAddr::EvmAddress(_) => None, + } + } + + /// Returns the native EVM address if this is an EVM initiator. + pub fn evm_address(&self) -> Option { match self { - InitiatorAddr::PublicKey(public_key) => public_key.to_account_hash(), - InitiatorAddr::AccountHash(hash) => *hash, + InitiatorAddr::EvmAddress(address) => Some(*address), + InitiatorAddr::PublicKey(_) | InitiatorAddr::AccountHash(_) => None, } } /// Returns a random `InitiatorAddr`. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..=1) { + match rng.gen_range(0..=2) { 0 => InitiatorAddr::PublicKey(PublicKey::random(rng)), 1 => InitiatorAddr::AccountHash(rng.gen()), + 2 => InitiatorAddr::EvmAddress(evm::Address::new(rng.gen())), _ => unreachable!(), } } @@ -77,6 +97,12 @@ impl InitiatorAddr { hash.serialized_length(), ] } + InitiatorAddr::EvmAddress(address) => { + vec![ + crate::bytesrepr::U8_SERIALIZED_LENGTH, + address.serialized_length(), + ] + } } } } @@ -96,6 +122,12 @@ impl ToBytes for InitiatorAddr { .add_field(ACCOUNT_HASH_FIELD_INDEX, &hash)? .binary_payload_bytes() } + InitiatorAddr::EvmAddress(address) => { + CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? + .add_field(TAG_FIELD_INDEX, &EVM_ADDRESS_VARIANT_TAG)? + .add_field(EVM_ADDRESS_FIELD_INDEX, &address)? + .binary_payload_bytes() + } } } fn serialized_length(&self) -> usize { @@ -128,6 +160,15 @@ impl FromBytes for InitiatorAddr { } Ok(InitiatorAddr::AccountHash(hash)) } + EVM_ADDRESS_VARIANT_TAG => { + let window = window.ok_or(Formatting)?; + window.verify_index(EVM_ADDRESS_FIELD_INDEX)?; + let (address, window) = window.deserialize_and_maybe_next::()?; + if window.is_some() { + return Err(Formatting); + } + Ok(InitiatorAddr::EvmAddress(address)) + } _ => Err(Formatting), }; to_ret.map(|endpoint| (endpoint, remainder)) @@ -146,6 +187,12 @@ impl From for InitiatorAddr { } } +impl From for InitiatorAddr { + fn from(address: evm::Address) -> Self { + InitiatorAddr::EvmAddress(address) + } +} + impl Display for InitiatorAddr { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { match self { @@ -155,6 +202,9 @@ impl Display for InitiatorAddr { InitiatorAddr::AccountHash(account_hash) => { write!(formatter, "account hash {}", account_hash) } + InitiatorAddr::EvmAddress(address) => { + write!(formatter, "EVM address {}", address) + } } } } @@ -170,6 +220,9 @@ impl Debug for InitiatorAddr { .debug_tuple("AccountHash") .field(account_hash) .finish(), + InitiatorAddr::EvmAddress(address) => { + formatter.debug_tuple("EvmAddress").field(address).finish() + } } } } diff --git a/types/src/transaction/transaction_hash.rs b/types/src/transaction/transaction_hash.rs index 948be894da..583c8bbc3b 100644 --- a/types/src/transaction/transaction_hash.rs +++ b/types/src/transaction/transaction_hash.rs @@ -14,11 +14,12 @@ use super::{DeployHash, TransactionV1Hash}; use crate::testing::TestRng; use crate::{ bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - Digest, + evm, Digest, }; const DEPLOY_TAG: u8 = 0; const V1_TAG: u8 = 1; +const EVM_TAG: u8 = 2; const TAG_LENGTH: u8 = 1; /// A versioned wrapper for a transaction hash or deploy hash. @@ -32,6 +33,8 @@ pub enum TransactionHash { /// A version 1 transaction hash. #[serde(rename = "Version1")] V1(TransactionV1Hash), + /// An EVM transaction hash. + Evm(evm::TransactionHash), } impl TransactionHash { @@ -42,6 +45,7 @@ impl TransactionHash { match self { TransactionHash::Deploy(deploy_hash) => *deploy_hash.inner(), TransactionHash::V1(transaction_hash) => *transaction_hash.inner(), + TransactionHash::Evm(transaction_hash) => Digest::from_raw(transaction_hash.value()), } } @@ -53,9 +57,10 @@ impl TransactionHash { /// Returns a random `TransactionHash`. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..2) { + match rng.gen_range(0..3) { 0 => TransactionHash::from(DeployHash::random(rng)), 1 => TransactionHash::from(TransactionV1Hash::random(rng)), + 2 => TransactionHash::from(evm::TransactionHash::random(rng)), _ => panic!(), } } @@ -91,6 +96,18 @@ impl From<&TransactionV1Hash> for TransactionHash { } } +impl From for TransactionHash { + fn from(hash: evm::TransactionHash) -> Self { + Self::Evm(hash) + } +} + +impl From<&evm::TransactionHash> for TransactionHash { + fn from(hash: &evm::TransactionHash) -> Self { + Self::from(*hash) + } +} + impl Default for TransactionHash { fn default() -> Self { TransactionHash::V1(TransactionV1Hash::default()) @@ -102,6 +119,7 @@ impl Display for TransactionHash { match self { TransactionHash::Deploy(hash) => Display::fmt(hash, formatter), TransactionHash::V1(hash) => Display::fmt(hash, formatter), + TransactionHash::Evm(hash) => Display::fmt(hash, formatter), } } } @@ -111,6 +129,7 @@ impl AsRef<[u8]> for TransactionHash { match self { TransactionHash::Deploy(hash) => hash.as_ref(), TransactionHash::V1(hash) => hash.as_ref(), + TransactionHash::Evm(hash) => hash.as_ref(), } } } @@ -127,6 +146,7 @@ impl ToBytes for TransactionHash { + match self { TransactionHash::Deploy(hash) => hash.serialized_length(), TransactionHash::V1(hash) => hash.serialized_length(), + TransactionHash::Evm(hash) => hash.serialized_length(), } } @@ -140,6 +160,10 @@ impl ToBytes for TransactionHash { V1_TAG.write_bytes(writer)?; hash.write_bytes(writer) } + TransactionHash::Evm(hash) => { + EVM_TAG.write_bytes(writer)?; + hash.write_bytes(writer) + } } } } @@ -156,6 +180,10 @@ impl FromBytes for TransactionHash { let (hash, remainder) = TransactionV1Hash::from_bytes(remainder)?; Ok((TransactionHash::V1(hash), remainder)) } + EVM_TAG => { + let (hash, remainder) = evm::TransactionHash::from_bytes(remainder)?; + Ok((TransactionHash::Evm(hash), remainder)) + } _ => Err(bytesrepr::Error::Formatting), } } diff --git a/types/tests/evm_transaction.rs b/types/tests/evm_transaction.rs new file mode 100644 index 0000000000..d1e989dd3e --- /dev/null +++ b/types/tests/evm_transaction.rs @@ -0,0 +1,113 @@ +use casper_types::{ + evm::{ + Address, Hash, Transaction, TransactionError, TransactionKind, EIP4844_TRANSACTION_TYPE_ID, + EIP7702_TRANSACTION_TYPE_ID, + }, + TimeDiff, Timestamp, +}; +use hex_literal::hex; + +const SENDER: Address = Address::new(hex!("dceea13df2f85e3a1de99a2f1c119fa6b2296a1e")); + +#[test] +fn decodes_legacy_signed_rlp() { + let transaction = decode(hex!("f86380843b9aca008252089400000000000000000000000000000000000000017b8031a0f9f5275265b6eb94b3c40777a78b78a6f2271bee070f22baf95cc373241ad424a0455f2ffb56667d638e4c5a3e859de7f0094996cffdfbe0e6b8b5025a600002c9")); + + assert_eq!(transaction.kind(), TransactionKind::Legacy); + assert_eq!(transaction.from(), SENDER); + assert_eq!( + transaction.to(), + Some(Address::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + ])) + ); + assert_eq!(transaction.nonce(), 0); + assert_eq!(transaction.gas_limit(), 21_000); + assert_eq!(transaction.gas_price(), Some(1_000_000_000)); + assert_eq!(transaction.value(), word(123)); + assert_eq!(transaction.chain_id(), Some(7)); + transaction + .verify() + .expect("legacy transaction should verify"); +} + +#[test] +fn decodes_eip2930_signed_rlp() { + let transaction = decode(hex!("01f8690701843b9aca0082c3509400000000000000000000000000000000000000028201c8821234c080a0ae81543cd30ddc7a55203a0df0d0d1182a448754e2569c32c9758db31e324cdfa0422e1fa2e2c7bf80261ccd8b4f9ac8b6c422cf0b09505d406256855156213e92")); + + assert_eq!(transaction.kind(), TransactionKind::Eip2930); + assert_eq!(transaction.from(), SENDER); + assert_eq!( + transaction.to(), + Some(Address::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 + ])) + ); + assert_eq!(transaction.nonce(), 1); + assert_eq!(transaction.gas_limit(), 50_000); + assert_eq!(transaction.gas_price(), Some(1_000_000_000)); + assert_eq!(transaction.value(), word(456)); + assert_eq!(transaction.input(), &[0x12, 0x34]); + assert_eq!(transaction.chain_id(), Some(7)); + transaction + .verify() + .expect("EIP-2930 transaction should verify"); +} + +#[test] +fn decodes_eip1559_signed_rlp() { + let transaction = decode(hex!("02f86d07028405f5e100847735940082ea6094000000000000000000000000000000000000000382031582abcdc080a0cc943bbac7dcda95bc138f08375a3be9543d34f7aecde0b386dbc0575c9cd5bc9fdfdb2712cdea574dfc83b372c490f705c2ba59421af0a84614d63024787fee")); + + assert_eq!(transaction.kind(), TransactionKind::Eip1559); + assert_eq!(transaction.from(), SENDER); + assert_eq!( + transaction.to(), + Some(Address::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3 + ])) + ); + assert_eq!(transaction.nonce(), 2); + assert_eq!(transaction.gas_limit(), 60_000); + assert_eq!(transaction.max_fee_per_gas(), 2_000_000_000); + assert_eq!(transaction.max_priority_fee_per_gas(), Some(100_000_000)); + assert_eq!(transaction.value(), word(789)); + assert_eq!(transaction.input(), &[0xab, 0xcd]); + assert_eq!(transaction.chain_id(), Some(7)); + transaction + .verify() + .expect("EIP-1559 transaction should verify"); +} + +#[test] +fn unsupported_typed_transactions_are_clear_errors() { + let timestamp = Timestamp::zero(); + let ttl = TimeDiff::from_seconds(60); + + assert_eq!( + Transaction::from_signed_rlp(vec![EIP4844_TRANSACTION_TYPE_ID], timestamp, ttl), + Err(TransactionError::UnsupportedTransactionType( + EIP4844_TRANSACTION_TYPE_ID + )) + ); + assert_eq!( + Transaction::from_signed_rlp(vec![EIP7702_TRANSACTION_TYPE_ID], timestamp, ttl), + Err(TransactionError::UnsupportedTransactionType( + EIP7702_TRANSACTION_TYPE_ID + )) + ); +} + +fn decode(bytes: [u8; N]) -> Transaction { + Transaction::from_signed_rlp( + bytes.to_vec(), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") +} + +fn word(value: u64) -> Hash { + let mut bytes = [0u8; 32]; + bytes[24..].copy_from_slice(&value.to_be_bytes()); + Hash::new(bytes) +} From a2c8d9fffa4333e54a7c853bfff184a77d2988f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Wed, 6 May 2026 14:27:26 +0200 Subject: [PATCH 02/17] Harden EVM executor validation and block hash handling Require signed EVM transactions to carry the configured chain ID so legacy pre-EIP-155 transactions cannot bypass the Casper EVM replay domain. Reject non-empty Ethereum access lists until the executor maps them into revm transaction environments. Add explicit validation modes for unsigned call requests so simulation calls remain available while commit-capable calls can opt into revm balance, nonce, chain-id, base-fee, and gas-limit checks. Add a block hash provider abstraction for the EVM BLOCKHASH opcode, including an LMDB-backed provider for future node wiring and tests for the 256-block lookup window. Complete selfdestruct cleanup by pruning the deterministic main purse balance alongside the EVM account and storage.# Please enter the commit message for your changes. Lines starting --- Cargo.lock | 3 + executor/evm/Cargo.toml | 5 + executor/evm/src/block_hash.rs | 68 ++++++++++ executor/evm/src/db.rs | 30 +++-- executor/evm/src/error.rs | 13 ++ executor/evm/src/executor.rs | 52 +++++--- executor/evm/src/lib.rs | 7 +- executor/evm/src/request.rs | 25 +++- executor/evm/src/state.rs | 6 +- executor/evm/tests/executor.rs | 221 ++++++++++++++++++++++++++++++++- types/src/evm/transaction.rs | 11 ++ types/tests/evm_transaction.rs | 36 ++++++ 12 files changed, 447 insertions(+), 30 deletions(-) create mode 100644 executor/evm/src/block_hash.rs diff --git a/Cargo.lock b/Cargo.lock index 27e2602ea2..24698e17b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1446,6 +1446,9 @@ dependencies = [ name = "casper-executor-evm" version = "0.1.0" dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", "casper-storage", "casper-types", "revm", diff --git a/executor/evm/Cargo.toml b/executor/evm/Cargo.toml index b88cf1ad6d..f60f8e6d8a 100644 --- a/executor/evm/Cargo.toml +++ b/executor/evm/Cargo.toml @@ -13,3 +13,8 @@ casper-storage = { version = "5.0.0", path = "../../storage" } casper-types = { version = "7.0.0", path = "../../types", features = ["std"] } revm = { version = "27", features = ["dev"] } thiserror = "2" + +[dev-dependencies] +alloy-consensus = { version = "=1.0.22", default-features = false, features = ["k256"] } +alloy-eips = { version = "=1.0.22", default-features = false, features = ["k256"] } +alloy-primitives = { version = "=1.2.0", default-features = false, features = ["rlp", "sha3-keccak"] } diff --git a/executor/evm/src/block_hash.rs b/executor/evm/src/block_hash.rs new file mode 100644 index 0000000000..03b8d00e66 --- /dev/null +++ b/executor/evm/src/block_hash.rs @@ -0,0 +1,68 @@ +//! Block hash provider abstractions for the EVM `BLOCKHASH` opcode. + +use std::sync::Arc; + +use casper_storage::block_store::{ + lmdb::IndexedLmdbBlockStore, types::BlockHeight, BlockStoreError, BlockStoreProvider, + DataReader, +}; +use casper_types::{evm, BlockHeader}; + +/// Result type returned by block hash providers. +pub type BlockHashProviderResult = core::result::Result; + +/// Errors returned while resolving historical block hashes. +#[derive(Debug, thiserror::Error)] +pub enum BlockHashProviderError { + /// Failed to read from Casper block storage. + #[error(transparent)] + BlockStore(#[from] BlockStoreError), +} + +/// Resolves Casper block hashes by height for the EVM `BLOCKHASH` opcode. +/// +/// The executor applies the EVM availability rules around the provider: current +/// and future block numbers, and block numbers older than 256 blocks, return +/// the zero hash. Providers only need to answer canonical historical heights. +pub trait BlockHashProvider { + /// Returns the block hash for `block_height`, or `None` when unavailable. + fn block_hash(&self, block_height: u64) -> BlockHashProviderResult>; +} + +/// Block hash provider that returns no historical hashes. +#[derive(Clone, Copy, Debug, Default)] +pub struct NoBlockHashProvider; + +impl BlockHashProvider for NoBlockHashProvider { + fn block_hash(&self, _block_height: u64) -> BlockHashProviderResult> { + Ok(None) + } +} + +/// Block hash provider backed by Casper's indexed LMDB block store. +#[derive(Clone, Debug)] +pub struct IndexedLmdbBlockHashProvider { + block_store: Arc, +} + +impl IndexedLmdbBlockHashProvider { + /// Creates a block hash provider backed by `block_store`. + pub fn new(block_store: Arc) -> Self { + Self { block_store } + } +} + +impl BlockHashProvider for IndexedLmdbBlockHashProvider { + fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { + let txn = self.block_store.checkout_ro()?; + let maybe_header: Option = + DataReader::::read(&txn, block_height)?; + Ok(maybe_header.map(|header| block_hash_to_evm_hash(header.block_hash()))) + } +} + +fn block_hash_to_evm_hash(block_hash: casper_types::BlockHash) -> evm::Hash { + let mut bytes = [0u8; evm::HASH_LENGTH]; + bytes.copy_from_slice(block_hash.as_ref()); + evm::Hash::new(bytes) +} diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs index 38c7febc05..a62ee0879d 100644 --- a/executor/evm/src/db.rs +++ b/executor/evm/src/db.rs @@ -11,21 +11,27 @@ use revm::{ state::{AccountInfo, Bytecode}, }; -use crate::{tx, DbError}; +use crate::{tx, BlockHashProvider, DbError}; -pub(crate) struct CasperDb<'a, R> +pub(crate) struct CasperDb<'a, R, B> where R: StateReader, + B: BlockHashProvider + ?Sized, { tracking_copy: &'a mut TrackingCopy, + block_hash_provider: &'a B, } -impl<'a, R> CasperDb<'a, R> +impl<'a, R, B> CasperDb<'a, R, B> where R: StateReader, + B: BlockHashProvider + ?Sized, { - pub(crate) fn new(tracking_copy: &'a mut TrackingCopy) -> Self { - Self { tracking_copy } + pub(crate) fn new(tracking_copy: &'a mut TrackingCopy, block_hash_provider: &'a B) -> Self { + Self { + tracking_copy, + block_hash_provider, + } } fn balance(&mut self, main_purse: casper_types::URef) -> Result { @@ -42,9 +48,10 @@ where } } -impl Database for CasperDb<'_, R> +impl Database for CasperDb<'_, R, B> where R: StateReader, + B: BlockHashProvider + ?Sized, { type Error = DbError; @@ -105,8 +112,15 @@ where } } - fn block_hash(&mut self, _number: u64) -> Result { - Ok(B256::ZERO) + fn block_hash(&mut self, number: u64) -> Result { + let maybe_block_hash = self + .block_hash_provider + .block_hash(number) + .map_err(|error| DbError::BlockHash { + height: number, + error, + })?; + Ok(maybe_block_hash.map(tx::to_revm_hash).unwrap_or(B256::ZERO)) } } diff --git a/executor/evm/src/error.rs b/executor/evm/src/error.rs index 033e3668a0..2936f5b91e 100644 --- a/executor/evm/src/error.rs +++ b/executor/evm/src/error.rs @@ -3,6 +3,8 @@ use casper_storage::tracking_copy::TrackingCopyError; use casper_types::Key; +use crate::BlockHashProviderError; + /// Result type returned by the EVM executor. pub type Result = core::result::Result; @@ -12,6 +14,9 @@ pub enum Error { /// EVM execution is disabled in the chainspec configuration. #[error("EVM execution is disabled")] Disabled, + /// Signed EVM transaction does not include an EIP-155 replay-protection chain id. + #[error("EVM transaction is missing replay-protection chain id")] + MissingChainId, /// Transaction chain id does not match the executor configuration. #[error("EVM transaction chain id {actual} does not match configured chain id {expected}")] ChainIdMismatch { @@ -64,6 +69,14 @@ pub enum DbError { /// Decode error text. error: String, }, + /// Failed to resolve a historical block hash for the EVM `BLOCKHASH` opcode. + #[error("failed to resolve EVM block hash at height {height}: {error}")] + BlockHash { + /// Block height requested by the EVM. + height: u64, + /// Provider error. + error: BlockHashProviderError, + }, } impl revm::database_interface::DBErrorMarker for DbError {} diff --git a/executor/evm/src/executor.rs b/executor/evm/src/executor.rs index f4bfe1480b..2212414da4 100644 --- a/executor/evm/src/executor.rs +++ b/executor/evm/src/executor.rs @@ -11,7 +11,8 @@ use revm::{ }; use crate::{ - db::CasperDb, state, tx, DbError, Error, ExecuteKind, ExecuteRequest, ExecutionOutcome, Result, + db::CasperDb, state, tx, BlockHashProvider, DbError, Error, ExecuteKind, ExecuteRequest, + ExecutionOutcome, NoBlockHashProvider, Result, }; /// Executes EVM transactions and calls against a Casper tracking copy. @@ -43,40 +44,63 @@ impl EvmExecutor { ) -> Result where R: StateReader, + { + let block_hash_provider = NoBlockHashProvider; + self.execute_with_block_hash_provider(tracking_copy, request, &block_hash_provider) + } + + /// Executes with a provider for historical block hashes. + /// + /// The provider is used by the EVM `BLOCKHASH` opcode. Current/future + /// blocks and block numbers older than the EVM 256-block lookup window + /// return the zero hash before the provider is consulted. + pub fn execute_with_block_hash_provider( + &self, + tracking_copy: &mut TrackingCopy, + request: ExecuteRequest, + block_hash_provider: &B, + ) -> Result + where + R: StateReader, + B: BlockHashProvider + ?Sized, { if !self.config.enabled { return Err(Error::Disabled); } if let ExecuteKind::Transaction(transaction) = &request.kind { - if let Some(actual) = transaction.chain_id() { - if actual != self.config.chain_id { - return Err(Error::ChainIdMismatch { - expected: self.config.chain_id, - actual, - }); - } + let Some(actual) = transaction.chain_id() else { + return Err(Error::MissingChainId); + }; + if actual != self.config.chain_id { + return Err(Error::ChainIdMismatch { + expected: self.config.chain_id, + actual, + }); } } let spec = spec_id(self.config.spec); let tx_env = tx::build_tx_env(&self.config, &request.kind)?; let block = request.block.to_revm_block(&self.config); - let is_call = matches!(request.kind, ExecuteKind::Call(_)); + let skip_validation = match &request.kind { + ExecuteKind::Transaction(_) => false, + ExecuteKind::Call(call) => call.validation.is_unchecked_simulation(), + }; let result_and_state = { - let db = CasperDb::new(tracking_copy); + let db = CasperDb::new(tracking_copy, block_hash_provider); let mut evm = Context::mainnet() .with_db(db) .with_block(block) .modify_cfg_chained(|cfg| { cfg.spec = spec; cfg.chain_id = self.config.chain_id; - cfg.tx_chain_id_check = !is_call; + cfg.tx_chain_id_check = !skip_validation; cfg.disable_block_gas_limit = false; - cfg.disable_base_fee = is_call; - cfg.disable_balance_check = is_call; - cfg.disable_nonce_check = is_call; + cfg.disable_base_fee = skip_validation; + cfg.disable_balance_check = skip_validation; + cfg.disable_nonce_check = skip_validation; }) .build_mainnet(); diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs index 7276fcd0d7..7eccb8673e 100644 --- a/executor/evm/src/lib.rs +++ b/executor/evm/src/lib.rs @@ -3,6 +3,7 @@ //! This crate provides a small execution API over `TrackingCopy` and keeps //! `revm` details behind internal adapter modules. +mod block_hash; mod db; mod error; mod executor; @@ -11,10 +12,14 @@ mod request; mod state; mod tx; +pub use block_hash::{ + BlockHashProvider, BlockHashProviderError, BlockHashProviderResult, + IndexedLmdbBlockHashProvider, NoBlockHashProvider, +}; pub use error::{DbError, Error, Result}; pub use executor::EvmExecutor; pub use outcome::{ExecutionOutcome, ExecutionStatus, Log}; -pub use request::{BlockContext, CallRequest, ExecuteKind, ExecuteRequest}; +pub use request::{BlockContext, CallRequest, CallValidation, ExecuteKind, ExecuteRequest}; use casper_types::evm; diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs index 416259732e..a17faeef51 100644 --- a/executor/evm/src/request.rs +++ b/executor/evm/src/request.rs @@ -24,9 +24,9 @@ pub enum ExecuteKind { /// Unsigned EVM call request. /// -/// Calls are useful for views and simulations. They still write effects into -/// the supplied tracking copy, so callers should pass a fork when they want to -/// discard the result. +/// Calls are useful for views, simulations, tests, and controlled system +/// execution. They still write effects into the supplied tracking copy, so +/// callers should pass a fork when they want to discard the result. #[derive(Clone, Debug, Eq, PartialEq)] pub struct CallRequest { /// EVM address used as `msg.sender`. @@ -41,8 +41,25 @@ pub struct CallRequest { pub gas_limit: u64, /// Gas price used by gas-price-sensitive contracts. pub gas_price: u128, - /// Nonce presented to revm when nonce checks are enabled by future callers. + /// Nonce presented to revm when nonce checks are enabled. pub nonce: u64, + /// Validation mode used for this unsigned call. + pub validation: CallValidation, +} + +/// Validation mode for unsigned EVM calls. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CallValidation { + /// Enforce EVM balance, nonce, chain-id, base-fee, and block-gas-limit checks. + Checked, + /// Disable EVM transaction validation checks for local simulations or controlled tests. + UncheckedSimulation, +} + +impl CallValidation { + pub(crate) fn is_unchecked_simulation(self) -> bool { + matches!(self, CallValidation::UncheckedSimulation) + } } /// Per-execution block context. diff --git a/executor/evm/src/state.rs b/executor/evm/src/state.rs index bdaff0799f..1c9197fd5d 100644 --- a/executor/evm/src/state.rs +++ b/executor/evm/src/state.rs @@ -34,7 +34,9 @@ where let account_key = Key::EvmAccount(address); if account.is_selfdestructed() { - prune_account(tracking_copy, address, account_key)?; + let main_purse = existing_main_purse(tracking_copy, &account_key)? + .unwrap_or_else(|| evm::deterministic_purse(address)); + prune_account(tracking_copy, address, account_key, main_purse)?; return Ok(()); } @@ -79,6 +81,7 @@ fn prune_account( tracking_copy: &mut TrackingCopy, address: evm::Address, account_key: Key, + main_purse: casper_types::URef, ) -> Result<(), Error> where R: StateReader, @@ -89,6 +92,7 @@ where for key in storage_keys { tracking_copy.prune(key); } + tracking_copy.prune(Key::Balance(main_purse.addr())); tracking_copy.prune(account_key); Ok(()) } diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 8239601325..fc47bd4274 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -1,8 +1,11 @@ use std::path::PathBuf; +use alloy_consensus::{SignableTransaction, TxEnvelope, TxLegacy}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, U256}; use casper_executor_evm::{ - BlockContext, CallRequest, EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, - EMPTY_CODE_HASH, + BlockContext, BlockHashProvider, BlockHashProviderResult, CallRequest, CallValidation, Error, + EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, EMPTY_CODE_HASH, }; use casper_storage::{ data_access_layer::{GenesisRequest, GenesisResult}, @@ -18,6 +21,7 @@ use casper_types::{ Key, Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, StoredValue, SystemConfig, Timestamp, WasmConfig, U512, }; +use revm::bytecode::opcode; fn tracking_copy() -> (TrackingCopy, impl Send) { let accounts = (1u8..=3) @@ -91,6 +95,65 @@ fn block() -> BlockContext { } } +#[derive(Clone, Copy)] +struct HeightBlockHashProvider; + +impl BlockHashProvider for HeightBlockHashProvider { + fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { + Ok(Some(block_hash_for_height(block_height))) + } +} + +fn block_hash_for_height(block_height: u64) -> evm::Hash { + let mut bytes = [0u8; evm::HASH_LENGTH]; + bytes[24..].copy_from_slice(&block_height.to_be_bytes()); + evm::Hash::new(bytes) +} + +fn init_code_returning(runtime: Vec) -> Vec { + let runtime_len = u8::try_from(runtime.len()).expect("runtime should fit in PUSH1"); + let runtime_offset = 12u8; + let mut init_code = vec![ + // memory[0..runtime_len] = code[runtime_offset..runtime_offset + runtime_len] + opcode::PUSH1, + runtime_len, + opcode::PUSH1, + runtime_offset, + opcode::PUSH1, + 0, + opcode::CODECOPY, + // return memory[0..runtime_len] + opcode::PUSH1, + runtime_len, + opcode::PUSH1, + 0, + opcode::RETURN, + ]; + assert_eq!(init_code.len(), usize::from(runtime_offset)); + init_code.extend(runtime); + init_code +} + +fn blockhash_contract_init_code() -> Vec { + let runtime = vec![ + // bytes32 hash = blockhash(1); + opcode::PUSH1, + 1, + opcode::BLOCKHASH, + // mstore(0, hash); + opcode::PUSH1, + 0, + opcode::MSTORE, + // return abi.encode(hash); + opcode::PUSH1, + 32, + opcode::PUSH1, + 0, + opcode::RETURN, + ]; + init_code_returning(runtime) +} + fn call_request( from: evm::Address, to: Option, @@ -107,6 +170,28 @@ fn call_request( gas_limit: 5_000_000, gas_price: 0, nonce: 0, + validation: CallValidation::UncheckedSimulation, + }), + } +} + +fn checked_call_request( + from: evm::Address, + to: Option, + input: Vec, + value: evm::Hash, +) -> ExecuteRequest { + ExecuteRequest { + block: block(), + kind: ExecuteKind::Call(CallRequest { + from, + to, + value, + input, + gas_limit: 5_000_000, + gas_price: 0, + nonce: 0, + validation: CallValidation::Checked, }), } } @@ -198,6 +283,30 @@ fn decode_hex(hex: &str) -> Vec { .collect() } +fn legacy_transaction(chain_id: Option) -> evm::Transaction { + let tx = TxLegacy { + chain_id, + nonce: 0, + gas_price: 1, + gas_limit: 21_000, + to: TxKind::Call(AlloyAddress::from([1u8; 20])), + value: U256::ZERO, + input: Default::default(), + }; + let tx = tx.into_signed(Signature::test_signature().with_parity(true)); + let envelope: TxEnvelope = tx.into(); + evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + casper_types::TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") +} + +fn legacy_transaction_without_chain_id() -> evm::Transaction { + legacy_transaction(None) +} + fn read_storage>( tracking_copy: &mut TrackingCopy, address: evm::Address, @@ -251,6 +360,57 @@ fn seed_evm_balance>( ); } +#[test] +fn blockhash_uses_supplied_provider() { + let executor = executor(evm::EvmSpec::Prague); + let from = evm::Address::new([1; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let contract = execute_call( + &executor, + &mut tracking_copy, + from, + None, + blockhash_contract_init_code(), + ) + .created_contract_address + .expect("deploy should return a contract address"); + let block_hash_provider = HeightBlockHashProvider; + + let outcome = executor + .execute_with_block_hash_provider( + &mut tracking_copy, + call_request(from, Some(contract), Vec::new(), evm::Hash::ZERO), + &block_hash_provider, + ) + .expect("EVM execution should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(outcome.output.as_slice(), evm::Hash::ZERO.as_bytes()); + + let mut too_old_request = call_request(from, Some(contract), Vec::new(), evm::Hash::ZERO); + too_old_request.block.number = 258; + let outcome = executor + .execute_with_block_hash_provider(&mut tracking_copy, too_old_request, &block_hash_provider) + .expect("EVM execution should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(outcome.output.as_slice(), evm::Hash::ZERO.as_bytes()); + + let mut historical_request = call_request(from, Some(contract), Vec::new(), evm::Hash::ZERO); + historical_request.block.number = 2; + let outcome = executor + .execute_with_block_hash_provider( + &mut tracking_copy, + historical_request, + &block_hash_provider, + ) + .expect("EVM execution should succeed"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!( + outcome.output.as_slice(), + block_hash_for_height(1).as_bytes() + ); +} + #[test] fn counter_supports_committed_and_discarded_execution() { let executor = executor(evm::EvmSpec::Prague); @@ -500,6 +660,14 @@ fn selfdestruct_cleanup_follows_selected_fork() { .unwrap(), None ); + assert_eq!( + shanghai_tracking_copy + .read(&Key::Balance( + evm::deterministic_purse(shanghai_contract).addr() + )) + .unwrap(), + None + ); assert_eq!( read_storage( &mut shanghai_tracking_copy, @@ -529,3 +697,52 @@ fn selfdestruct_cleanup_follows_selected_fork() { .unwrap() .is_some()); } + +#[test] +fn signed_transactions_require_configured_chain_id() { + let executor = executor(evm::EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let missing_chain_id = legacy_transaction_without_chain_id(); + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(missing_chain_id), + }; + assert!(matches!( + executor.execute(&mut tracking_copy, request), + Err(Error::MissingChainId) + )); + + let wrong_chain_executor = EvmExecutor::new(evm::EvmConfig { + enabled: true, + chain_id: 8, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }); + let transaction = legacy_transaction(Some(7)); + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(transaction), + }; + assert!(matches!( + wrong_chain_executor.execute(&mut tracking_copy, request), + Err(Error::ChainIdMismatch { + expected: 8, + actual: 7 + }) + )); +} + +#[test] +fn checked_calls_enforce_transaction_validation() { + let executor = executor(evm::EvmSpec::Prague); + let from = evm::Address::new([1; 20]); + let recipient = evm::Address::new([2; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + let request = checked_call_request(from, Some(recipient), Vec::new(), word(1)); + assert!(matches!( + executor.execute(&mut tracking_copy, request), + Err(Error::Revm(_)) + )); +} diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index 54984dcebc..398855254b 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -174,6 +174,8 @@ pub enum TransactionError { Decode(String), /// The transaction envelope type is not supported by this first-pass executor. UnsupportedTransactionType(u8), + /// The transaction contains an access list, which this first-pass executor does not model. + UnsupportedAccessList, /// The sender address could not be recovered from the signature. SenderRecovery(String), /// Re-decoding the raw RLP produced metadata different from this transaction. @@ -189,6 +191,9 @@ impl Display for TransactionError { TransactionError::UnsupportedTransactionType(kind) => { write!(formatter, "unsupported EVM transaction type: {kind}") } + TransactionError::UnsupportedAccessList => { + formatter.write_str("unsupported EVM transaction access list") + } TransactionError::SenderRecovery(error) => { write!(formatter, "EVM transaction sender recovery error: {error}") } @@ -304,6 +309,12 @@ impl Transaction { "trailing bytes after transaction envelope".to_string(), )); } + if envelope + .access_list() + .is_some_and(|access_list| !access_list.is_empty()) + { + return Err(TransactionError::UnsupportedAccessList); + } let from = envelope .recover_signer() .map_err(|error| TransactionError::SenderRecovery(format!("{error:?}")))?; diff --git a/types/tests/evm_transaction.rs b/types/tests/evm_transaction.rs index d1e989dd3e..89c4296080 100644 --- a/types/tests/evm_transaction.rs +++ b/types/tests/evm_transaction.rs @@ -1,3 +1,9 @@ +use alloy_consensus::{SignableTransaction, TxEip2930, TxEnvelope}; +use alloy_eips::{ + eip2718::Encodable2718, + eip2930::{AccessList, AccessListItem}, +}; +use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, B256, U256}; use casper_types::{ evm::{ Address, Hash, Transaction, TransactionError, TransactionKind, EIP4844_TRANSACTION_TYPE_ID, @@ -97,6 +103,17 @@ fn unsupported_typed_transactions_are_clear_errors() { ); } +#[test] +fn non_empty_access_lists_are_rejected() { + let timestamp = Timestamp::zero(); + let ttl = TimeDiff::from_seconds(60); + + assert_eq!( + Transaction::from_signed_rlp(signed_eip2930_with_access_list(), timestamp, ttl), + Err(TransactionError::UnsupportedAccessList) + ); +} + fn decode(bytes: [u8; N]) -> Transaction { Transaction::from_signed_rlp( bytes.to_vec(), @@ -111,3 +128,22 @@ fn word(value: u64) -> Hash { bytes[24..].copy_from_slice(&value.to_be_bytes()); Hash::new(bytes) } + +fn signed_eip2930_with_access_list() -> Vec { + let tx = TxEip2930 { + chain_id: 7, + nonce: 0, + gas_price: 1_000_000_000, + gas_limit: 50_000, + to: TxKind::Call(AlloyAddress::from([2u8; 20])), + value: U256::from(456u64), + input: vec![0x12, 0x34].into(), + access_list: AccessList(vec![AccessListItem { + address: AlloyAddress::from([8u8; 20]), + storage_keys: vec![B256::from([9u8; 32])], + }]), + }; + let tx = tx.into_signed(Signature::test_signature()); + let envelope: TxEnvelope = tx.into(); + envelope.encoded_2718() +} From ae5f0383750b62cafaa9068279cbdc2082f569f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Wed, 6 May 2026 17:09:05 +0200 Subject: [PATCH 03/17] Add EVM execution result support --- executor/evm/src/lib.rs | 4 +- executor/evm/src/outcome.rs | 100 +++- node/src/components/event_stream_server.rs | 2 +- node/src/components/storage.rs | 16 +- .../src/reactor/main_reactor/tests/fixture.rs | 11 + .../tests/transaction_scenario/asertions.rs | 5 + .../main_reactor/tests/transactions.rs | 1 + .../meta_transaction/transaction_header.rs | 33 +- .../src/block_store/lmdb/lmdb_block_store.rs | 3 + types/src/evm.rs | 2 + types/src/evm/receipt.rs | 526 ++++++++++++++++++ types/src/execution.rs | 2 + types/src/execution/evm_execution_result.rs | 165 ++++++ types/src/execution/execution_result.rs | 52 +- 14 files changed, 871 insertions(+), 51 deletions(-) create mode 100644 types/src/evm/receipt.rs create mode 100644 types/src/execution/evm_execution_result.rs diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs index 7eccb8673e..f030ee2367 100644 --- a/executor/evm/src/lib.rs +++ b/executor/evm/src/lib.rs @@ -18,11 +18,13 @@ pub use block_hash::{ }; pub use error::{DbError, Error, Result}; pub use executor::EvmExecutor; -pub use outcome::{ExecutionOutcome, ExecutionStatus, Log}; +pub use outcome::{ExecutionOutcome, ExecutionStatus}; pub use request::{BlockContext, CallRequest, CallValidation, ExecuteKind, ExecuteRequest}; use casper_types::evm; +pub use casper_types::evm::Log; + /// Keccak-256 hash of empty EVM bytecode. pub const EMPTY_CODE_HASH: evm::Hash = evm::Hash::new([ 0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0, diff --git a/executor/evm/src/outcome.rs b/executor/evm/src/outcome.rs index 1abff25e9b..778246eba5 100644 --- a/executor/evm/src/outcome.rs +++ b/executor/evm/src/outcome.rs @@ -1,7 +1,9 @@ //! Public execution outcome types. use casper_types::evm; -use revm::context_interface::result::{ExecutionResult, Output}; +use revm::context_interface::result::{ + ExecutionResult, HaltReason as RevmHaltReason, OutOfGasError as RevmOutOfGasError, Output, +}; use crate::tx; @@ -15,7 +17,7 @@ pub struct ExecutionOutcome { /// Return or revert bytes. pub output: Vec, /// Logs emitted by successful execution. - pub logs: Vec, + pub logs: Vec, /// Address created by a successful create transaction. pub created_contract_address: Option, } @@ -39,7 +41,7 @@ impl ExecutionOutcome { status: ExecutionStatus::Success, gas_used: *gas_used, output: output_bytes, - logs: logs.iter().map(Log::from_revm_log).collect(), + logs: logs.iter().map(from_revm_log).collect(), created_contract_address, } } @@ -50,8 +52,8 @@ impl ExecutionOutcome { logs: Vec::new(), created_contract_address: None, }, - ExecutionResult::Halt { gas_used, .. } => Self { - status: ExecutionStatus::Halt, + ExecutionResult::Halt { gas_used, reason } => Self { + status: ExecutionStatus::Halt(from_revm_halt_reason(reason)), gas_used: *gas_used, output: Vec::new(), logs: Vec::new(), @@ -59,6 +61,22 @@ impl ExecutionOutcome { }, } } + + /// Converts this execution outcome into EVM receipt data. + pub fn to_receipt(&self, effective_gas_price: u128) -> evm::Receipt { + let status = match self.status { + ExecutionStatus::Success => evm::ReceiptStatus::Success, + ExecutionStatus::Revert => evm::ReceiptStatus::Revert, + ExecutionStatus::Halt(reason) => evm::ReceiptStatus::Halt(reason), + }; + evm::Receipt { + status, + gas_used: self.gas_used, + effective_gas_price, + contract_address: self.created_contract_address, + logs: self.logs.clone(), + } + } } /// High-level EVM execution status. @@ -69,32 +87,58 @@ pub enum ExecutionStatus { /// Execution reverted and returned revert bytes. Revert, /// Execution halted, usually consuming all supplied gas. - Halt, + Halt(evm::HaltReason), } -/// EVM log entry emitted by a successful transaction. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Log { - /// Contract address that emitted the log. - pub address: evm::Address, - /// Indexed log topics. - pub topics: Vec, - /// Unindexed log data. - pub data: Vec, +fn from_revm_halt_reason(reason: &RevmHaltReason) -> evm::HaltReason { + match reason { + RevmHaltReason::OutOfGas(reason) => { + evm::HaltReason::OutOfGas(from_revm_out_of_gas_error(*reason)) + } + RevmHaltReason::OpcodeNotFound => evm::HaltReason::OpcodeNotFound, + RevmHaltReason::InvalidFEOpcode => evm::HaltReason::InvalidFEOpcode, + RevmHaltReason::InvalidJump => evm::HaltReason::InvalidJump, + RevmHaltReason::NotActivated => evm::HaltReason::NotActivated, + RevmHaltReason::StackUnderflow => evm::HaltReason::StackUnderflow, + RevmHaltReason::StackOverflow => evm::HaltReason::StackOverflow, + RevmHaltReason::OutOfOffset => evm::HaltReason::OutOfOffset, + RevmHaltReason::CreateCollision => evm::HaltReason::CreateCollision, + RevmHaltReason::PrecompileError => evm::HaltReason::PrecompileError, + RevmHaltReason::NonceOverflow => evm::HaltReason::NonceOverflow, + RevmHaltReason::CreateContractSizeLimit => evm::HaltReason::CreateContractSizeLimit, + RevmHaltReason::CreateContractStartingWithEF => { + evm::HaltReason::CreateContractStartingWithEF + } + RevmHaltReason::CreateInitCodeSizeLimit => evm::HaltReason::CreateInitCodeSizeLimit, + RevmHaltReason::OverflowPayment => evm::HaltReason::OverflowPayment, + RevmHaltReason::StateChangeDuringStaticCall => evm::HaltReason::StateChangeDuringStaticCall, + RevmHaltReason::CallNotAllowedInsideStatic => evm::HaltReason::CallNotAllowedInsideStatic, + RevmHaltReason::OutOfFunds => evm::HaltReason::OutOfFunds, + RevmHaltReason::CallTooDeep => evm::HaltReason::CallTooDeep, + } } -impl Log { - fn from_revm_log(log: &revm::primitives::Log) -> Self { - Self { - address: tx::from_revm_address(log.address), - topics: log - .data - .topics() - .iter() - .copied() - .map(tx::from_revm_hash) - .collect(), - data: log.data.data.to_vec(), - } +fn from_revm_out_of_gas_error(error: RevmOutOfGasError) -> evm::OutOfGasError { + match error { + RevmOutOfGasError::Basic => evm::OutOfGasError::Basic, + RevmOutOfGasError::MemoryLimit => evm::OutOfGasError::MemoryLimit, + RevmOutOfGasError::Memory => evm::OutOfGasError::Memory, + RevmOutOfGasError::Precompile => evm::OutOfGasError::Precompile, + RevmOutOfGasError::InvalidOperand => evm::OutOfGasError::InvalidOperand, + RevmOutOfGasError::ReentrancySentry => evm::OutOfGasError::ReentrancySentry, + } +} + +fn from_revm_log(log: &revm::primitives::Log) -> evm::Log { + evm::Log { + address: tx::from_revm_address(log.address), + topics: log + .data + .topics() + .iter() + .copied() + .map(tx::from_revm_hash) + .collect(), + data: log.data.data.to_vec().into(), } } diff --git a/node/src/components/event_stream_server.rs b/node/src/components/event_stream_server.rs index e8d9820e3c..2d76519c14 100644 --- a/node/src/components/event_stream_server.rs +++ b/node/src/components/event_stream_server.rs @@ -305,7 +305,7 @@ where deploy_header.timestamp(), deploy_header.ttl(), ), - TransactionHeader::V1(metadata) => ( + TransactionHeader::V1(metadata) | TransactionHeader::Evm(metadata) => ( metadata.initiator_addr().clone(), metadata.timestamp(), metadata.ttl(), diff --git a/node/src/components/storage.rs b/node/src/components/storage.rs index 17010c9e14..fc35d1777a 100644 --- a/node/src/components/storage.rs +++ b/node/src/components/storage.rs @@ -2047,11 +2047,11 @@ impl Storage { Some(Transaction::V1(transaction_v1)) => { ret.push((transaction_hash, (&transaction_v1).into(), execution_result)) } - Some(transaction @ Transaction::Evm(_)) => { - let mismatch = VariantMismatch(Box::new((transaction_hash, transaction))); - error!(%mismatch, "failed getting transaction header"); - return Err(FatalStorageError::from(mismatch)); - } + Some(Transaction::Evm(transaction)) => ret.push(( + transaction_hash, + transaction.as_ref().into(), + execution_result, + )), }; } Ok(Some(ret)) @@ -2141,6 +2141,7 @@ impl Storage { ExecutionResultV1::Success { cost, .. } => *cost, }, ExecutionResult::V2(v2_result) => v2_result.limit.value(), + ExecutionResult::Evm(evm_result) => evm_result.limit.value(), }) .sum(); @@ -2157,6 +2158,8 @@ impl Storage { .map(|results| { if let ExecutionResult::V2(result) = results { result.size_estimate + } else if let ExecutionResult::Evm(result) = results { + result.size_estimate } else { 0u64 } @@ -2321,6 +2324,9 @@ fn successful_transfers(execution_result: &ExecutionResult) -> Vec { } // else no-op: we only record transfers from successful executions. } + ExecutionResult::Evm(_) => { + // No-op: EVM receipt logs are not Casper transfers. + } ExecutionResult::V1(ExecutionResultV1::Failure { .. }) => { // No-op: we only record transfers from successful executions. } diff --git a/node/src/reactor/main_reactor/tests/fixture.rs b/node/src/reactor/main_reactor/tests/fixture.rs index 4bbda7c74e..7cc72f0ee1 100644 --- a/node/src/reactor/main_reactor/tests/fixture.rs +++ b/node/src/reactor/main_reactor/tests/fixture.rs @@ -902,6 +902,17 @@ impl TestFixture { ); } } + ExecutionResult::Evm(execution_result) => { + if execution_result.receipt.status.is_success() { + execution_result.effects.transforms().to_vec() + } else { + panic!( + "EVM transaction execution failed: {:?} gas: {}", + execution_result.receipt.status.message(), + execution_result.receipt.gas_used + ); + } + } } } diff --git a/node/src/reactor/main_reactor/tests/transaction_scenario/asertions.rs b/node/src/reactor/main_reactor/tests/transaction_scenario/asertions.rs index c9c33a702c..074343e20b 100644 --- a/node/src/reactor/main_reactor/tests/transaction_scenario/asertions.rs +++ b/node/src/reactor/main_reactor/tests/transaction_scenario/asertions.rs @@ -65,6 +65,11 @@ impl Assertion for TransactionFailure { casper_types::execution::ExecutionResult::V2(execution_result_v2) => { execution_result_v2.error_message.clone() } + casper_types::execution::ExecutionResult::Evm(execution_result) => execution_result + .receipt + .status + .message() + .map(str::to_string), }; assert!(error_msg.is_some()); if let Some(msg) = &self.expected_error_message { diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index dbc3cce486..71d7c07c71 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -759,6 +759,7 @@ pub(crate) fn assert_exec_result_cost( pub fn exec_result_is_success(exec_result: &ExecutionResult) -> bool { match exec_result { ExecutionResult::V2(execution_result_v2) => execution_result_v2.error_message.is_none(), + ExecutionResult::Evm(execution_result) => execution_result.receipt.status.is_success(), ExecutionResult::V1(ExecutionResultV1::Success { .. }) => true, ExecutionResult::V1(ExecutionResultV1::Failure { .. }) => false, } diff --git a/node/src/types/transaction/meta_transaction/transaction_header.rs b/node/src/types/transaction/meta_transaction/transaction_header.rs index c4b6820638..5d6a3e2b51 100644 --- a/node/src/types/transaction/meta_transaction/transaction_header.rs +++ b/node/src/types/transaction/meta_transaction/transaction_header.rs @@ -1,16 +1,18 @@ -use casper_types::{DeployHeader, InitiatorAddr, TimeDiff, Timestamp, Transaction, TransactionV1}; +use casper_types::{ + evm, DeployHeader, InitiatorAddr, TimeDiff, Timestamp, Transaction, TransactionV1, +}; use core::fmt::{self, Display, Formatter}; use datasize::DataSize; use serde::Serialize; #[derive(Debug, Clone, DataSize, PartialEq, Eq, Serialize)] -pub(crate) struct TransactionV1Metadata { +pub(crate) struct TransactionMetadata { initiator_addr: InitiatorAddr, timestamp: Timestamp, ttl: TimeDiff, } -impl TransactionV1Metadata { +impl TransactionMetadata { pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { &self.initiator_addr } @@ -24,11 +26,11 @@ impl TransactionV1Metadata { } } -impl Display for TransactionV1Metadata { +impl Display for TransactionMetadata { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { write!( formatter, - "transaction-v1-metadata[initiator_addr: {}]", + "transaction-metadata[initiator_addr: {}]", self.initiator_addr, ) } @@ -38,7 +40,8 @@ impl Display for TransactionV1Metadata { /// A versioned wrapper for a transaction header or deploy header. pub(crate) enum TransactionHeader { Deploy(DeployHeader), - V1(TransactionV1Metadata), + V1(TransactionMetadata), + Evm(TransactionMetadata), } impl From for TransactionHeader { @@ -49,7 +52,7 @@ impl From for TransactionHeader { impl From<&TransactionV1> for TransactionHeader { fn from(transaction_v1: &TransactionV1) -> Self { - let meta = TransactionV1Metadata { + let meta = TransactionMetadata { initiator_addr: transaction_v1.initiator_addr().clone(), timestamp: transaction_v1.timestamp(), ttl: transaction_v1.ttl(), @@ -58,14 +61,23 @@ impl From<&TransactionV1> for TransactionHeader { } } +impl From<&evm::Transaction> for TransactionHeader { + fn from(transaction: &evm::Transaction) -> Self { + let meta = TransactionMetadata { + initiator_addr: InitiatorAddr::EvmAddress(transaction.from()), + timestamp: transaction.timestamp(), + ttl: transaction.ttl(), + }; + Self::Evm(meta) + } +} + impl From<&Transaction> for TransactionHeader { fn from(transaction: &Transaction) -> Self { match transaction { Transaction::Deploy(deploy) => deploy.header().clone().into(), Transaction::V1(v1) => v1.into(), - Transaction::Evm(_) => { - panic!("EVM transactions are not routed through node transaction metadata") - } + Transaction::Evm(evm) => evm.as_ref().into(), } } } @@ -75,6 +87,7 @@ impl Display for TransactionHeader { match self { TransactionHeader::Deploy(header) => Display::fmt(header, formatter), TransactionHeader::V1(meta) => Display::fmt(meta, formatter), + TransactionHeader::Evm(meta) => Display::fmt(meta, formatter), } } } diff --git a/storage/src/block_store/lmdb/lmdb_block_store.rs b/storage/src/block_store/lmdb/lmdb_block_store.rs index 02fea2156f..4e975fb2e1 100644 --- a/storage/src/block_store/lmdb/lmdb_block_store.rs +++ b/storage/src/block_store/lmdb/lmdb_block_store.rs @@ -598,6 +598,9 @@ fn successful_transfers(execution_result: &ExecutionResult) -> Vec { } // else no-op: we only record transfers from successful executions. } + ExecutionResult::Evm(_) => { + // No-op: EVM receipt logs are not Casper transfers. + } ExecutionResult::V1(ExecutionResultV1::Failure { .. }) => { // No-op: we only record transfers from successful executions. } diff --git a/types/src/evm.rs b/types/src/evm.rs index 0cd0af2ae8..d6ac08b7cc 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -9,12 +9,14 @@ mod account; mod address; mod config; mod hash; +mod receipt; mod transaction; pub use account::{deterministic_purse, Account, ByteCode, StorageAddr, StorageValue}; pub use address::{Address, ADDRESS_LENGTH}; pub use config::{EvmConfig, EvmSpec}; pub use hash::{Hash, HASH_LENGTH}; +pub use receipt::{HaltReason, Log, OutOfGasError, Receipt, ReceiptStatus}; pub use transaction::{ Transaction, TransactionError, TransactionHash, TransactionKind, EIP4844_TRANSACTION_TYPE_ID, EIP7702_TRANSACTION_TYPE_ID, diff --git a/types/src/evm/receipt.rs b/types/src/evm/receipt.rs new file mode 100644 index 0000000000..735d6c2fe8 --- /dev/null +++ b/types/src/evm/receipt.rs @@ -0,0 +1,526 @@ +//! EVM transaction receipt types. + +use alloc::vec::Vec; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(any(feature = "testing", test))] +use rand::Rng; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::{Address, Hash}; +use crate::bytesrepr::{self, Bytes, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}; +#[cfg(any(feature = "testing", test))] +use crate::testing::TestRng; + +/// High-level status recorded in an EVM transaction receipt. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum ReceiptStatus { + /// EVM execution completed successfully. + Success, + /// EVM execution reverted and returned revert bytes. + Revert, + /// EVM execution halted for an exceptional reason. + Halt(HaltReason), +} + +impl ReceiptStatus { + fn tag(self) -> u8 { + match self { + ReceiptStatus::Success => 0, + ReceiptStatus::Revert => 1, + ReceiptStatus::Halt(_) => 2, + } + } + + /// Returns the binary status expected by `eth_getTransactionReceipt`. + pub fn eth_status(self) -> u8 { + match self { + ReceiptStatus::Success => 1, + ReceiptStatus::Revert | ReceiptStatus::Halt(_) => 0, + } + } + + /// Returns `true` when the receipt represents successful EVM execution. + pub fn is_success(self) -> bool { + matches!(self, ReceiptStatus::Success) + } + + /// Returns a stable diagnostic message derived from the typed status. + pub fn message(self) -> Option<&'static str> { + match self { + ReceiptStatus::Success => None, + ReceiptStatus::Revert => Some("EVM reverted"), + ReceiptStatus::Halt(reason) => Some(reason.message()), + } + } +} + +impl ToBytes for ReceiptStatus { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + + match self { + ReceiptStatus::Success | ReceiptStatus::Revert => 0, + ReceiptStatus::Halt(reason) => reason.serialized_length(), + } + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.push(self.tag()); + if let ReceiptStatus::Halt(reason) = self { + reason.write_bytes(writer)?; + } + Ok(()) + } +} + +impl FromBytes for ReceiptStatus { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let status = match tag { + 0 => ReceiptStatus::Success, + 1 => ReceiptStatus::Revert, + 2 => { + let (reason, remainder) = HaltReason::from_bytes(remainder)?; + return Ok((ReceiptStatus::Halt(reason), remainder)); + } + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((status, remainder)) + } +} + +/// Reason an EVM execution halted exceptionally. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum HaltReason { + /// Execution ran out of gas. + OutOfGas(OutOfGasError), + /// The bytecode contained an unknown opcode. + OpcodeNotFound, + /// The bytecode executed the invalid `0xFE` opcode. + InvalidFEOpcode, + /// Execution jumped to an invalid destination. + InvalidJump, + /// The opcode or feature is not active for the configured hardfork. + NotActivated, + /// Execution attempted to pop a value from an empty stack. + StackUnderflow, + /// Execution attempted to push a value onto a full stack. + StackOverflow, + /// Execution used an invalid memory or storage offset. + OutOfOffset, + /// Contract creation collided with an existing account. + CreateCollision, + /// A precompile failed. + PrecompileError, + /// Account nonce overflowed. + NonceOverflow, + /// Created contract runtime bytecode exceeded the configured limit. + CreateContractSizeLimit, + /// Created contract runtime bytecode starts with `0xEF`. + CreateContractStartingWithEF, + /// Contract init code exceeded the configured limit. + CreateInitCodeSizeLimit, + /// Payment accounting overflowed. + OverflowPayment, + /// Execution attempted a state change during a static call. + StateChangeDuringStaticCall, + /// Execution attempted a call disallowed during a static call. + CallNotAllowedInsideStatic, + /// The caller did not have enough funds. + OutOfFunds, + /// Call depth exceeded the EVM limit. + CallTooDeep, + /// Halt reason was not recognized by this version. + Unknown, +} + +impl HaltReason { + fn tag(self) -> u8 { + match self { + HaltReason::OutOfGas(_) => 0, + HaltReason::OpcodeNotFound => 1, + HaltReason::InvalidFEOpcode => 2, + HaltReason::InvalidJump => 3, + HaltReason::NotActivated => 4, + HaltReason::StackUnderflow => 5, + HaltReason::StackOverflow => 6, + HaltReason::OutOfOffset => 7, + HaltReason::CreateCollision => 8, + HaltReason::PrecompileError => 9, + HaltReason::NonceOverflow => 10, + HaltReason::CreateContractSizeLimit => 11, + HaltReason::CreateContractStartingWithEF => 12, + HaltReason::CreateInitCodeSizeLimit => 13, + HaltReason::OverflowPayment => 14, + HaltReason::StateChangeDuringStaticCall => 15, + HaltReason::CallNotAllowedInsideStatic => 16, + HaltReason::OutOfFunds => 17, + HaltReason::CallTooDeep => 18, + HaltReason::Unknown => 19, + } + } + + /// Returns a stable diagnostic message for this halt reason. + pub fn message(self) -> &'static str { + match self { + HaltReason::OutOfGas(reason) => reason.message(), + HaltReason::OpcodeNotFound => "EVM halted: opcode not found", + HaltReason::InvalidFEOpcode => "EVM halted: invalid 0xFE opcode", + HaltReason::InvalidJump => "EVM halted: invalid jump destination", + HaltReason::NotActivated => "EVM halted: feature or opcode not activated", + HaltReason::StackUnderflow => "EVM halted: stack underflow", + HaltReason::StackOverflow => "EVM halted: stack overflow", + HaltReason::OutOfOffset => "EVM halted: out of offset", + HaltReason::CreateCollision => "EVM halted: create collision", + HaltReason::PrecompileError => "EVM halted: precompile error", + HaltReason::NonceOverflow => "EVM halted: nonce overflow", + HaltReason::CreateContractSizeLimit => "EVM halted: create contract size limit", + HaltReason::CreateContractStartingWithEF => { + "EVM halted: create contract starting with 0xEF" + } + HaltReason::CreateInitCodeSizeLimit => "EVM halted: create initcode size limit", + HaltReason::OverflowPayment => "EVM halted: overflow payment", + HaltReason::StateChangeDuringStaticCall => { + "EVM halted: state change during static call" + } + HaltReason::CallNotAllowedInsideStatic => { + "EVM halted: call not allowed inside static call" + } + HaltReason::OutOfFunds => "EVM halted: out of funds", + HaltReason::CallTooDeep => "EVM halted: call too deep", + HaltReason::Unknown => "EVM halted: unknown reason", + } + } + + /// Returns a random EVM halt reason. + #[cfg(any(feature = "testing", test))] + pub fn random(rng: &mut TestRng) -> Self { + match rng.gen_range(0..20) { + 0 => HaltReason::OutOfGas(OutOfGasError::random(rng)), + 1 => HaltReason::OpcodeNotFound, + 2 => HaltReason::InvalidFEOpcode, + 3 => HaltReason::InvalidJump, + 4 => HaltReason::NotActivated, + 5 => HaltReason::StackUnderflow, + 6 => HaltReason::StackOverflow, + 7 => HaltReason::OutOfOffset, + 8 => HaltReason::CreateCollision, + 9 => HaltReason::PrecompileError, + 10 => HaltReason::NonceOverflow, + 11 => HaltReason::CreateContractSizeLimit, + 12 => HaltReason::CreateContractStartingWithEF, + 13 => HaltReason::CreateInitCodeSizeLimit, + 14 => HaltReason::OverflowPayment, + 15 => HaltReason::StateChangeDuringStaticCall, + 16 => HaltReason::CallNotAllowedInsideStatic, + 17 => HaltReason::OutOfFunds, + 18 => HaltReason::CallTooDeep, + 19 => HaltReason::Unknown, + _ => unreachable!(), + } + } +} + +impl ToBytes for HaltReason { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + + match self { + HaltReason::OutOfGas(reason) => reason.serialized_length(), + _ => 0, + } + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.push(self.tag()); + if let HaltReason::OutOfGas(reason) = self { + reason.write_bytes(writer)?; + } + Ok(()) + } +} + +impl FromBytes for HaltReason { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let reason = match tag { + 0 => { + let (reason, remainder) = OutOfGasError::from_bytes(remainder)?; + return Ok((HaltReason::OutOfGas(reason), remainder)); + } + 1 => HaltReason::OpcodeNotFound, + 2 => HaltReason::InvalidFEOpcode, + 3 => HaltReason::InvalidJump, + 4 => HaltReason::NotActivated, + 5 => HaltReason::StackUnderflow, + 6 => HaltReason::StackOverflow, + 7 => HaltReason::OutOfOffset, + 8 => HaltReason::CreateCollision, + 9 => HaltReason::PrecompileError, + 10 => HaltReason::NonceOverflow, + 11 => HaltReason::CreateContractSizeLimit, + 12 => HaltReason::CreateContractStartingWithEF, + 13 => HaltReason::CreateInitCodeSizeLimit, + 14 => HaltReason::OverflowPayment, + 15 => HaltReason::StateChangeDuringStaticCall, + 16 => HaltReason::CallNotAllowedInsideStatic, + 17 => HaltReason::OutOfFunds, + 18 => HaltReason::CallTooDeep, + 19 => HaltReason::Unknown, + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((reason, remainder)) + } +} + +/// Reason execution ran out of gas. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum OutOfGasError { + /// Not enough gas to execute an opcode. + Basic, + /// Memory limit exceeded. + MemoryLimit, + /// Memory expansion ran out of gas. + Memory, + /// Precompile ran out of gas. + Precompile, + /// Operand was too large to fit into the required native type. + InvalidOperand, + /// `SSTORE` was attempted with too little gas remaining. + ReentrancySentry, +} + +impl OutOfGasError { + fn tag(self) -> u8 { + match self { + OutOfGasError::Basic => 0, + OutOfGasError::MemoryLimit => 1, + OutOfGasError::Memory => 2, + OutOfGasError::Precompile => 3, + OutOfGasError::InvalidOperand => 4, + OutOfGasError::ReentrancySentry => 5, + } + } + + /// Returns a stable diagnostic message for this out-of-gas reason. + pub fn message(self) -> &'static str { + match self { + OutOfGasError::Basic => "EVM halted: out of gas", + OutOfGasError::MemoryLimit => "EVM halted: out of gas: memory limit exceeded", + OutOfGasError::Memory => "EVM halted: out of gas: memory expansion", + OutOfGasError::Precompile => "EVM halted: out of gas: precompile", + OutOfGasError::InvalidOperand => "EVM halted: out of gas: invalid operand", + OutOfGasError::ReentrancySentry => "EVM halted: out of gas: reentrancy sentry", + } + } + + /// Returns a random out-of-gas error. + #[cfg(any(feature = "testing", test))] + pub fn random(rng: &mut TestRng) -> Self { + match rng.gen_range(0..6) { + 0 => OutOfGasError::Basic, + 1 => OutOfGasError::MemoryLimit, + 2 => OutOfGasError::Memory, + 3 => OutOfGasError::Precompile, + 4 => OutOfGasError::InvalidOperand, + 5 => OutOfGasError::ReentrancySentry, + _ => unreachable!(), + } + } +} + +impl ToBytes for OutOfGasError { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Ok(vec![self.tag()]) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.push(self.tag()); + Ok(()) + } +} + +impl FromBytes for OutOfGasError { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let reason = match tag { + 0 => OutOfGasError::Basic, + 1 => OutOfGasError::MemoryLimit, + 2 => OutOfGasError::Memory, + 3 => OutOfGasError::Precompile, + 4 => OutOfGasError::InvalidOperand, + 5 => OutOfGasError::ReentrancySentry, + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((reason, remainder)) + } +} + +/// EVM log entry emitted by a transaction. +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct Log { + /// Contract address that emitted the log. + pub address: Address, + /// Indexed log topics. + pub topics: Vec, + /// Unindexed log data. + pub data: Bytes, +} + +impl ToBytes for Log { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.address.serialized_length() + + self.topics.serialized_length() + + self.data.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.address.write_bytes(writer)?; + self.topics.write_bytes(writer)?; + self.data.write_bytes(writer) + } +} + +impl FromBytes for Log { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (address, remainder) = Address::from_bytes(bytes)?; + let (topics, remainder) = Vec::::from_bytes(remainder)?; + let (data, remainder) = Bytes::from_bytes(remainder)?; + Ok(( + Log { + address, + topics, + data, + }, + remainder, + )) + } +} + +/// EVM transaction receipt data persisted with an EVM execution result. +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct Receipt { + /// Transaction execution status. + pub status: ReceiptStatus, + /// Gas consumed by EVM execution. + pub gas_used: u64, + /// Effective gas price used for Ethereum receipt projection. + pub effective_gas_price: u128, + /// Contract address created by the transaction, if any. + pub contract_address: Option
, + /// Logs emitted by successful execution. + pub logs: Vec, +} + +impl Receipt { + /// Returns a random EVM receipt. + #[cfg(any(feature = "testing", test))] + pub fn random(rng: &mut TestRng) -> Self { + let log_count = rng.gen_range(0..4); + let logs = (0..log_count) + .map(|_| Log { + address: Address::new(rng.gen()), + topics: (0..rng.gen_range(0..4)) + .map(|_| Hash::new(rng.gen())) + .collect(), + data: Bytes::from({ + let mut data = vec![0; rng.gen_range(0..16)]; + rng.fill(data.as_mut_slice()); + data + }), + }) + .collect(); + Receipt { + status: match rng.gen_range(0..3) { + 0 => ReceiptStatus::Success, + 1 => ReceiptStatus::Revert, + 2 => ReceiptStatus::Halt(HaltReason::random(rng)), + _ => unreachable!(), + }, + gas_used: rng.gen(), + effective_gas_price: rng.gen(), + contract_address: rng.gen::().then(|| Address::new(rng.gen())), + logs, + } + } +} + +impl ToBytes for Receipt { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.status.serialized_length() + + self.gas_used.serialized_length() + + self.effective_gas_price.serialized_length() + + self.contract_address.serialized_length() + + self.logs.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.status.write_bytes(writer)?; + self.gas_used.write_bytes(writer)?; + self.effective_gas_price.write_bytes(writer)?; + self.contract_address.write_bytes(writer)?; + self.logs.write_bytes(writer) + } +} + +impl FromBytes for Receipt { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (status, remainder) = ReceiptStatus::from_bytes(bytes)?; + let (gas_used, remainder) = u64::from_bytes(remainder)?; + let (effective_gas_price, remainder) = u128::from_bytes(remainder)?; + let (contract_address, remainder) = Option::
::from_bytes(remainder)?; + let (logs, remainder) = Vec::::from_bytes(remainder)?; + Ok(( + Receipt { + status, + gas_used, + effective_gas_price, + contract_address, + logs, + }, + remainder, + )) + } +} diff --git a/types/src/execution.rs b/types/src/execution.rs index f1f190ad44..c4a7be1aa4 100644 --- a/types/src/execution.rs +++ b/types/src/execution.rs @@ -1,6 +1,7 @@ //! Types related to execution of deploys. mod effects; +mod evm_execution_result; mod execution_result; pub mod execution_result_v1; mod execution_result_v2; @@ -9,6 +10,7 @@ mod transform_error; mod transform_kind; pub use effects::Effects; +pub use evm_execution_result::EvmExecutionResult; pub use execution_result::ExecutionResult; pub use execution_result_v1::ExecutionResultV1; pub use execution_result_v2::ExecutionResultV2; diff --git a/types/src/execution/evm_execution_result.rs b/types/src/execution/evm_execution_result.rs new file mode 100644 index 0000000000..d0b0f58b09 --- /dev/null +++ b/types/src/execution/evm_execution_result.rs @@ -0,0 +1,165 @@ +//! EVM transaction execution result types. + +use alloc::vec::Vec; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(any(feature = "testing", test))] +use rand::Rng; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::Effects; +#[cfg(any(feature = "testing", test))] +use crate::testing::TestRng; +use crate::{ + bytesrepr::{self, FromBytes, ToBytes}, + evm, Gas, InitiatorAddr, U512, +}; + +/// The result of executing a single EVM transaction. +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct EvmExecutionResult { + /// Who initiated this EVM transaction. + pub initiator: InitiatorAddr, + /// The current Casper gas price used for fee accounting. + pub current_price: u8, + /// The maximum allowed gas limit for this transaction. + pub limit: Gas, + /// How much was paid for this transaction. + pub cost: U512, + /// How much unconsumed gas was refunded, if any. + pub refund: U512, + /// The size estimate of the transaction. + pub size_estimate: u64, + /// The effects of executing this transaction. + pub effects: Effects, + /// EVM-native receipt data used by Ethereum JSON-RPC projections. + pub receipt: evm::Receipt, +} + +impl EvmExecutionResult { + /// Returns a random `EvmExecutionResult`. + #[cfg(any(feature = "testing", test))] + pub fn random(rng: &mut TestRng) -> Self { + let limit = Gas::new(rng.gen::()); + let gas_price = rng.gen_range(1..6); + let cost = limit.value() * U512::from(gas_price); + EvmExecutionResult { + initiator: InitiatorAddr::random(rng), + current_price: gas_price, + limit, + cost, + refund: rng.gen::().into(), + size_estimate: rng.gen(), + effects: Effects::random(rng), + receipt: evm::Receipt::random(rng), + } + } +} + +impl ToBytes for EvmExecutionResult { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.initiator.serialized_length() + + self.current_price.serialized_length() + + self.limit.serialized_length() + + self.cost.serialized_length() + + self.refund.serialized_length() + + self.size_estimate.serialized_length() + + self.effects.serialized_length() + + self.receipt.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.initiator.write_bytes(writer)?; + self.current_price.write_bytes(writer)?; + self.limit.write_bytes(writer)?; + self.cost.write_bytes(writer)?; + self.refund.write_bytes(writer)?; + self.size_estimate.write_bytes(writer)?; + self.effects.write_bytes(writer)?; + self.receipt.write_bytes(writer) + } +} + +impl FromBytes for EvmExecutionResult { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (initiator, remainder) = InitiatorAddr::from_bytes(bytes)?; + let (current_price, remainder) = u8::from_bytes(remainder)?; + let (limit, remainder) = Gas::from_bytes(remainder)?; + let (cost, remainder) = U512::from_bytes(remainder)?; + let (refund, remainder) = U512::from_bytes(remainder)?; + let (size_estimate, remainder) = u64::from_bytes(remainder)?; + let (effects, remainder) = Effects::from_bytes(remainder)?; + let (receipt, remainder) = evm::Receipt::from_bytes(remainder)?; + Ok(( + EvmExecutionResult { + initiator, + current_price, + limit, + cost, + refund, + size_estimate, + effects, + receipt, + }, + remainder, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bytesrepr_roundtrip() { + let rng = &mut TestRng::new(); + for _ in 0..10 { + let execution_result = EvmExecutionResult::random(rng); + bytesrepr::test_serialization_roundtrip(&execution_result); + } + } + + #[test] + fn json_schema() { + #[cfg(feature = "json-schema")] + { + let schema = schemars::schema_for!(EvmExecutionResult); + serde_json::to_value(&schema).unwrap(); + } + } + + #[test] + fn receipt_bytesrepr_roundtrip() { + let rng = &mut TestRng::new(); + for _ in 0..10 { + let receipt = evm::Receipt::random(rng); + bytesrepr::test_serialization_roundtrip(&receipt); + for log in &receipt.logs { + bytesrepr::test_serialization_roundtrip(log); + } + } + } + + #[test] + fn receipt_json_schema() { + #[cfg(feature = "json-schema")] + { + let receipt_schema = schemars::schema_for!(evm::Receipt); + let log_schema = schemars::schema_for!(evm::Log); + serde_json::to_value(&receipt_schema).unwrap(); + serde_json::to_value(&log_schema).unwrap(); + } + } +} diff --git a/types/src/execution/execution_result.rs b/types/src/execution/execution_result.rs index 04b9ab1273..f84435c242 100644 --- a/types/src/execution/execution_result.rs +++ b/types/src/execution/execution_result.rs @@ -1,4 +1,8 @@ -use alloc::{boxed::Box, string::String, vec::Vec}; +use alloc::{ + boxed::Box, + string::{String, ToString}, + vec::Vec, +}; #[cfg(feature = "datasize")] use datasize::DataSize; @@ -11,7 +15,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tracing::error; -use super::{ExecutionResultV1, ExecutionResultV2}; +use super::{EvmExecutionResult, ExecutionResultV1, ExecutionResultV2}; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; use crate::{ @@ -21,6 +25,7 @@ use crate::{ const V1_TAG: u8 = 0; const V2_TAG: u8 = 1; +const EVM_TAG: u8 = 2; /// The versioned result of executing a single deploy. #[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug)] @@ -34,6 +39,8 @@ pub enum ExecutionResult { /// Version 2 of execution result type. #[serde(rename = "Version2")] V2(Box), + /// EVM transaction execution result type. + Evm(Box), } impl ExecutionResult { @@ -42,6 +49,7 @@ impl ExecutionResult { match self { ExecutionResult::V1(result) => result.cost(), ExecutionResult::V2(result) => result.cost, + ExecutionResult::Evm(result) => result.cost, } } @@ -50,6 +58,7 @@ impl ExecutionResult { match self { ExecutionResult::V1(result) => result.cost(), ExecutionResult::V2(result) => result.consumed.value(), + ExecutionResult::Evm(result) => result.receipt.gas_used.into(), } } @@ -58,16 +67,18 @@ impl ExecutionResult { match self { ExecutionResult::V1(_) => None, ExecutionResult::V2(result) => Some(result.refund), + ExecutionResult::Evm(result) => Some(result.refund), } } /// Returns a random ExecutionResult. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - if rng.gen_bool(0.5) { - Self::V1(rand::distributions::Standard.sample(rng)) - } else { - Self::V2(Box::new(ExecutionResultV2::random(rng))) + match rng.gen_range(0..3) { + 0 => Self::V1(rand::distributions::Standard.sample(rng)), + 1 => Self::V2(Box::new(ExecutionResultV2::random(rng))), + 2 => Self::Evm(Box::new(EvmExecutionResult::random(rng))), + _ => unreachable!(), } } @@ -79,6 +90,7 @@ impl ExecutionResult { ExecutionResultV1::Success { .. } => None, }, ExecutionResult::V2(v2) => v2.error_message.clone(), + ExecutionResult::Evm(evm) => evm.receipt.status.message().map(str::to_string), } } @@ -89,6 +101,7 @@ impl ExecutionResult { vec![] } ExecutionResult::V2(execution_result) => execution_result.transfers.clone(), + ExecutionResult::Evm(_) => vec![], } } } @@ -105,6 +118,12 @@ impl From for ExecutionResult { } } +impl From for ExecutionResult { + fn from(value: EvmExecutionResult) -> Self { + ExecutionResult::Evm(Box::new(value)) + } +} + impl ToBytes for ExecutionResult { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut buffer = bytesrepr::allocate_buffer(self)?; @@ -117,6 +136,7 @@ impl ToBytes for ExecutionResult { + match self { ExecutionResult::V1(result) => result.serialized_length(), ExecutionResult::V2(result) => result.serialized_length(), + ExecutionResult::Evm(result) => result.serialized_length(), } } @@ -130,6 +150,10 @@ impl ToBytes for ExecutionResult { V2_TAG.write_bytes(writer)?; result.write_bytes(writer) } + ExecutionResult::Evm(result) => { + EVM_TAG.write_bytes(writer)?; + result.write_bytes(writer) + } } } } @@ -155,6 +179,10 @@ impl FromBytes for ExecutionResult { let (result, remainder) = ExecutionResultV2::from_bytes(remainder)?; Ok((ExecutionResult::V2(Box::new(result)), remainder)) } + EVM_TAG => { + let (result, remainder) = EvmExecutionResult::from_bytes(remainder)?; + Ok((ExecutionResult::Evm(Box::new(result)), remainder)) + } _ => { error!(%tag, rem_len = remainder.len(), "FromBytes for ExecutionResult: unknown tag"); Err(bytesrepr::Error::Formatting) @@ -177,6 +205,8 @@ mod tests { bytesrepr::test_serialization_roundtrip(&execution_result); let execution_result = ExecutionResult::from(ExecutionResultV2::random(rng)); bytesrepr::test_serialization_roundtrip(&execution_result); + let execution_result = ExecutionResult::from(EvmExecutionResult::random(rng)); + bytesrepr::test_serialization_roundtrip(&execution_result); } #[test] @@ -191,6 +221,11 @@ mod tests { let serialized = bincode::serialize(&execution_result).unwrap(); let deserialized = bincode::deserialize(&serialized).unwrap(); assert_eq!(execution_result, deserialized); + + let execution_result = ExecutionResult::from(EvmExecutionResult::random(rng)); + let serialized = bincode::serialize(&execution_result).unwrap(); + let deserialized = bincode::deserialize(&serialized).unwrap(); + assert_eq!(execution_result, deserialized); } #[test] @@ -206,5 +241,10 @@ mod tests { println!("{:#}", serialized); let deserialized = serde_json::from_str(&serialized).unwrap(); assert_eq!(execution_result, deserialized); + + let execution_result = ExecutionResult::from(EvmExecutionResult::random(rng)); + let serialized = serde_json::to_string(&execution_result).unwrap(); + let deserialized = serde_json::from_str(&serialized).unwrap(); + assert_eq!(execution_result, deserialized); } } From 97124c23e2da044108bf7d46ad8b10bdc7a1b7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Wed, 6 May 2026 17:47:17 +0200 Subject: [PATCH 04/17] Update Rust toolchain to 1.91.0 Bump the pinned stable Rust toolchain from 1.85.1 to 1.91.0 so the workspace can use dependencies that require the newer compiler. Keep the existing Makefile lint gate on -D warnings while allowing lint categories that were introduced or tightened by newer clippy versions and would otherwise force unrelated API churn. Apply small mechanical updates and targeted dead-code annotations needed for the 1.91.0 lint run. --- Makefile | 29 +++++++++++++++---- binary_port/src/error_code.rs | 2 +- .../tests/src/test/regression/ee_1152.rs | 2 +- .../test/regression/regression_20220221.rs | 2 +- node/src/components/consensus.rs | 1 + node/src/components/rest_server/docs.rs | 2 ++ node/src/components/storage/tests.rs | 1 + node/src/tls.rs | 2 ++ node/src/types/node_id.rs | 1 + node/src/types/status_feed.rs | 2 ++ node/src/utils/display_error.rs | 2 +- rust-toolchain.toml | 2 +- .../sdk/src/collections/iterable_map.rs | 2 +- storage/src/data_access_layer/balance_hold.rs | 2 ++ storage/src/data_access_layer/genesis.rs | 1 + storage/src/global_state/trie/mod.rs | 4 +-- .../src/global_state/trie_store/cache/mod.rs | 2 +- storage/src/tracking_copy/tests.rs | 6 ++-- types/src/block/era_end.rs | 2 +- types/src/block/rewarded_signatures.rs | 2 +- .../test_block_v1_builder.rs | 3 +- .../src/chainspec/accounts_config/genesis.rs | 6 ++-- types/src/cl_type.rs | 8 ++--- types/src/contracts.rs | 9 ++---- types/src/digest.rs | 4 +-- types/src/lib.rs | 6 ++-- types/src/package.rs | 9 ++---- types/src/system/auction.rs | 6 ++-- types/src/transaction.rs | 16 +++++----- types/src/transaction/deploy.rs | 2 ++ .../src/transaction/deploy/deploy_category.rs | 1 + types/src/transaction/deploy/error.rs | 2 +- types/src/transaction/serialization/mod.rs | 6 ++-- utils/validation/src/abi.rs | 1 + 34 files changed, 87 insertions(+), 61 deletions(-) diff --git a/Makefile b/Makefile index 236991923f..40a210fc6d 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,25 @@ WASM_STRIP_VERSION := $(shell wasm-strip --version) CARGO_OPTS := --locked CARGO_PINNED_NIGHTLY := $(CARGO) +$(PINNED_NIGHTLY) $(CARGO_OPTS) CARGO := $(CARGO) $(CARGO_OPTS) +# TODO: Pay these down after the Rust 1.91.0 bump. They keep this +# toolchain-only commit from becoming a broad unrelated refactor while +# preserving `-D warnings`; new lints should be fixed or locally allowed +# instead of extending this shared list. +CLIPPY_LINT_ARGS := \ + -D warnings \ + -A unknown_lints \ + -A clippy::large_enum_variant \ + -A clippy::result_large_err \ + -A clippy::manual_repeat_n \ + -A clippy::manual_is_multiple_of \ + -A clippy::iter_kv_map \ + -A clippy::unneeded_struct_pattern \ + -A clippy::io_other_error \ + -A clippy::cloned_ref_to_slice_refs \ + -A clippy::double_ended_iterator_last \ + -A clippy::manual_div_ceil \ + -A clippy::mem_replace_option_with_some \ + -A mismatched_lifetime_syntaxes DISABLE_LOGGING = RUST_LOG=MatchesNothing @@ -125,26 +144,26 @@ format: $(CARGO_PINNED_NIGHTLY) fmt --all lint-contracts-rs: - cd smart_contracts/contracts && $(CARGO) clippy $(patsubst %, -p %, $(ALL_CONTRACTS)) -- -D warnings -A renamed_and_removed_lints + cd smart_contracts/contracts && $(CARGO) clippy $(patsubst %, -p %, $(ALL_CONTRACTS)) -- $(CLIPPY_LINT_ARGS) -A renamed_and_removed_lints .PHONY: lint lint: lint-contracts-rs lint-default-features lint-all-features lint-smart-contracts lint-no-default-features .PHONY: lint-default-features lint-default-features: - $(CARGO) clippy --all-targets -- -D warnings + $(CARGO) clippy --all-targets -- $(CLIPPY_LINT_ARGS) .PHONY: lint-no-default-features lint-no-default-features: - $(CARGO) clippy --all-targets --no-default-features -- -D warnings + $(CARGO) clippy --all-targets --no-default-features -- $(CLIPPY_LINT_ARGS) .PHONY: lint-all-features lint-all-features: - $(CARGO) clippy --all-targets --all-features -- -D warnings + LC_ALL=C LANG=C LC_CTYPE=C $(CARGO) clippy --all-targets --all-features -- $(CLIPPY_LINT_ARGS) .PHONY: lint-smart-contracts lint-smart-contracts: - cd smart_contracts/contract && $(CARGO) clippy --all-targets -- -D warnings -A renamed_and_removed_lints + cd smart_contracts/contract && $(CARGO) clippy --all-targets -- $(CLIPPY_LINT_ARGS) -A renamed_and_removed_lints .PHONY: audit-rs audit-rs: diff --git a/binary_port/src/error_code.rs b/binary_port/src/error_code.rs index e3ea4b3a0a..6bcb8ee8a1 100644 --- a/binary_port/src/error_code.rs +++ b/binary_port/src/error_code.rs @@ -552,7 +552,7 @@ impl From for ErrorCode { InvalidTransactionV1::UnexpectedEntryPoint { .. } => { ErrorCode::InvalidTransactionUnexpectedEntryPoint } - InvalidTransactionV1::CouldNotSerializeTransaction { .. } => { + InvalidTransactionV1::CouldNotSerializeTransaction => { ErrorCode::TransactionHasMalformedBinaryRepresentation } InvalidTransactionV1::InsufficientAmount { .. } => { diff --git a/execution_engine_testing/tests/src/test/regression/ee_1152.rs b/execution_engine_testing/tests/src/test/regression/ee_1152.rs index c435de9f77..3ac0b440e9 100644 --- a/execution_engine_testing/tests/src/test/regression/ee_1152.rs +++ b/execution_engine_testing/tests/src/test/regression/ee_1152.rs @@ -134,7 +134,7 @@ fn should_run_ee_1152_regression_test() { let (era_id, _) = era_validators .into_iter() - .last() + .next_back() .expect("should have last element"); assert!(era_id > INITIAL_ERA_ID, "{}", era_id); diff --git a/execution_engine_testing/tests/src/test/regression/regression_20220221.rs b/execution_engine_testing/tests/src/test/regression/regression_20220221.rs index b49012ce78..1fe9f4cc39 100644 --- a/execution_engine_testing/tests/src/test/regression/regression_20220221.rs +++ b/execution_engine_testing/tests/src/test/regression/regression_20220221.rs @@ -108,7 +108,7 @@ fn regression_20220221_should_distribute_to_many_validators() { let (era_id, trusted_era_validators) = era_validators .into_iter() - .last() + .next_back() .expect("should have last element"); assert!(era_id > INITIAL_ERA_ID, "{}", era_id); diff --git a/node/src/components/consensus.rs b/node/src/components/consensus.rs index 02b4a7cea6..2ae4a880f1 100644 --- a/node/src/components/consensus.rs +++ b/node/src/components/consensus.rs @@ -99,6 +99,7 @@ pub(crate) use relaxed::{ConsensusMessage, ConsensusMessageDiscriminants}; /// A request to be handled by the consensus protocol instance in a particular era. #[derive(DataSize, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, From)] +#[allow(dead_code)] pub(crate) enum EraRequest where C: Context, diff --git a/node/src/components/rest_server/docs.rs b/node/src/components/rest_server/docs.rs index 1c6ae930da..8db6b05e67 100644 --- a/node/src/components/rest_server/docs.rs +++ b/node/src/components/rest_server/docs.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use casper_types::{ProtocolVersion, PublicKey, SecretKey, Timestamp}; use once_cell::sync::Lazy; diff --git a/node/src/components/storage/tests.rs b/node/src/components/storage/tests.rs index 1ba5ea286c..3b583fd9e5 100644 --- a/node/src/components/storage/tests.rs +++ b/node/src/components/storage/tests.rs @@ -1541,6 +1541,7 @@ fn should_provide_transfers_after_emptied() { /// Example state used in storage. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[allow(dead_code)] struct StateData { a: Vec, b: i32, diff --git a/node/src/tls.rs b/node/src/tls.rs index b5bcaf0003..793c695ccc 100644 --- a/node/src/tls.rs +++ b/node/src/tls.rs @@ -181,6 +181,7 @@ impl Distribution for Standard { /// Cryptographic signature. #[derive(Clone, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[allow(dead_code)] struct Signature(Vec); impl Debug for Signature { @@ -295,6 +296,7 @@ pub(crate) fn load_secret_key>(src: P) -> Result, L /// Combines a value `V` with a `Signature` and a signature scheme. The signature scheme involves /// serializing the value to bytes and signing the result. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[allow(dead_code)] pub struct Signed { data: Vec, signature: Signature, diff --git a/node/src/types/node_id.rs b/node/src/types/node_id.rs index c4b9410078..427c87e533 100644 --- a/node/src/types/node_id.rs +++ b/node/src/types/node_id.rs @@ -74,6 +74,7 @@ impl<'de> Deserialize<'de> for NodeId { } } +#[allow(dead_code)] static NODE_ID: Lazy = Lazy::new(|| NodeId(KeyFingerprint::from([1u8; KeyFingerprint::LENGTH]))); diff --git a/node/src/types/status_feed.rs b/node/src/types/status_feed.rs index 6c6f154321..fa0683473d 100644 --- a/node/src/types/status_feed.rs +++ b/node/src/types/status_feed.rs @@ -20,6 +20,7 @@ use crate::{ types::NodeId, }; +#[allow(dead_code)] static CHAINSPEC_INFO: Lazy = Lazy::new(|| { let next_upgrade = NextUpgrade::new( ActivationPoint::EraId(EraId::from(42)), @@ -31,6 +32,7 @@ static CHAINSPEC_INFO: Lazy = Lazy::new(|| { } }); +#[allow(dead_code)] static GET_STATUS_RESULT: Lazy = Lazy::new(|| { let node_id = NodeId::doc_example(); let socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 54321); diff --git a/node/src/utils/display_error.rs b/node/src/utils/display_error.rs index a99cb712b5..647d637d8a 100644 --- a/node/src/utils/display_error.rs +++ b/node/src/utils/display_error.rs @@ -31,7 +31,7 @@ where T: error::Error, { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut opt_source: Option<&(dyn error::Error)> = Some(self.0); + let mut opt_source: Option<&dyn error::Error> = Some(self.0); while let Some(source) = opt_source { write!(f, "{}", source)?; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 00822fdf58..d72668b05a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.85.1" +channel = "1.91.0" diff --git a/smart_contracts/sdk/src/collections/iterable_map.rs b/smart_contracts/sdk/src/collections/iterable_map.rs index c1b06368c0..ee785c2457 100644 --- a/smart_contracts/sdk/src/collections/iterable_map.rs +++ b/smart_contracts/sdk/src/collections/iterable_map.rs @@ -267,7 +267,7 @@ where /// /// Traverses entries in reverse-insertion order. /// Each item is a tuple of the hashed key and the value. - pub fn iter(&self) -> IterableMapIter { + pub fn iter(&self) -> IterableMapIter<'_, K, V> { IterableMapIter { prefix: &self.prefix, current: self.tail_key_hash, diff --git a/storage/src/data_access_layer/balance_hold.rs b/storage/src/data_access_layer/balance_hold.rs index c712e8257e..4d4925f445 100644 --- a/storage/src/data_access_layer/balance_hold.rs +++ b/storage/src/data_access_layer/balance_hold.rs @@ -165,6 +165,7 @@ impl BalanceHoldRequest { /// Possible balance hold errors. #[derive(Error, Debug, Clone)] +#[allow(clippy::large_enum_variant)] #[non_exhaustive] pub enum BalanceHoldError { /// Tracking copy error. @@ -219,6 +220,7 @@ impl Display for BalanceHoldError { /// Result enum that represents all possible outcomes of a balance hold request. #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub enum BalanceHoldResult { /// Returned if a passed state root hash is not found. RootNotFound, diff --git a/storage/src/data_access_layer/genesis.rs b/storage/src/data_access_layer/genesis.rs index c6c94f881a..fe88d9dff0 100644 --- a/storage/src/data_access_layer/genesis.rs +++ b/storage/src/data_access_layer/genesis.rs @@ -101,6 +101,7 @@ impl Distribution for Standard { /// Represents a result of a `genesis` request. #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub enum GenesisResult { /// Genesis fatal. Fatal(String), diff --git a/storage/src/global_state/trie/mod.rs b/storage/src/global_state/trie/mod.rs index 20ba5b8ddf..8acefc32f6 100644 --- a/storage/src/global_state/trie/mod.rs +++ b/storage/src/global_state/trie/mod.rs @@ -441,7 +441,7 @@ impl Trie { } /// Returns an iterator over descendants of the trie. - pub fn iter_children(&self) -> DescendantsIterator { + pub fn iter_children(&self) -> DescendantsIterator<'_> { match self { Trie::::Leaf { .. } => DescendantsIterator::ZeroOrOne(None), Trie::Node { pointer_block } => DescendantsIterator::PointerBlock { @@ -503,7 +503,7 @@ pub(crate) enum LazilyDeserializedTrie { } impl LazilyDeserializedTrie { - pub(crate) fn iter_children(&self) -> DescendantsIterator { + pub(crate) fn iter_children(&self) -> DescendantsIterator<'_> { match self { LazilyDeserializedTrie::Leaf(_) => { // Leaf bytes does not have any children diff --git a/storage/src/global_state/trie_store/cache/mod.rs b/storage/src/global_state/trie_store/cache/mod.rs index 312c9b2eda..009769afea 100644 --- a/storage/src/global_state/trie_store/cache/mod.rs +++ b/storage/src/global_state/trie_store/cache/mod.rs @@ -151,7 +151,7 @@ where } } else { let leaf = TrieCacheNode::Leaf { key, value }; - let _ = std::mem::replace(pointer, Some(CachePointer::InMem(leaf))); + let _ = pointer.replace(CachePointer::InMem(leaf)); return Ok(()); } } diff --git a/storage/src/tracking_copy/tests.rs b/storage/src/tracking_copy/tests.rs index 2f79204402..a891e4c40d 100644 --- a/storage/src/tracking_copy/tests.rs +++ b/storage/src/tracking_copy/tests.rs @@ -460,7 +460,7 @@ fn should_traverse_all_paths() { } let expected_contract = unpack( - tc.query(account_key, &[contract_alias.clone()]), + tc.query(account_key, std::slice::from_ref(&contract_alias)), "contract should exist".to_string(), ); assert_eq!( @@ -482,7 +482,7 @@ fn should_traverse_all_paths() { ); let expected_account = unpack( - tc.query(contract_key, &[account_alias.clone()]), + tc.query(contract_key, std::slice::from_ref(&account_alias)), "account should exist".to_string(), ); assert_eq!(expected_account, stored_account, "unexpected stored value"); @@ -507,7 +507,7 @@ fn should_traverse_all_paths() { assert_eq!(expected_value, misc_stored_value, "unexpected stored value"); let expected_account_misc = unpack( - tc.query(account_key, &[misc_alias.clone()]), + tc.query(account_key, std::slice::from_ref(&misc_alias)), "misc value should exist via account".to_string(), ); assert_eq!( diff --git a/types/src/block/era_end.rs b/types/src/block/era_end.rs index 1bb853d154..0c7c370aed 100644 --- a/types/src/block/era_end.rs +++ b/types/src/block/era_end.rs @@ -61,7 +61,7 @@ impl EraEnd { } /// Returns the rewards. - pub fn rewards(&self) -> Rewards { + pub fn rewards(&self) -> Rewards<'_> { match self { EraEnd::V1(v1) => Rewards::V1(v1.rewards()), EraEnd::V2(v2) => Rewards::V2(v2.rewards()), diff --git a/types/src/block/rewarded_signatures.rs b/types/src/block/rewarded_signatures.rs index e483f95a38..bbdab5153f 100644 --- a/types/src/block/rewarded_signatures.rs +++ b/types/src/block/rewarded_signatures.rs @@ -326,7 +326,7 @@ fn chunks_8(bits: impl Iterator) -> impl Iterator Self { - let mut bytes = vec![0; (n_validators + 7) / 8]; + let mut bytes = vec![0; n_validators.div_ceil(8)]; rand::RngCore::fill_bytes(rng, bytes.as_mut()); diff --git a/types/src/block/test_block_builder/test_block_v1_builder.rs b/types/src/block/test_block_builder/test_block_v1_builder.rs index 1a6b68a774..7f7909c448 100644 --- a/types/src/block/test_block_builder/test_block_v1_builder.rs +++ b/types/src/block/test_block_builder/test_block_v1_builder.rs @@ -102,8 +102,7 @@ impl TestBlockV1Builder { /// Associates a number of random deploys with the created block. pub fn random_deploys(mut self, count: usize, rng: &mut TestRng) -> Self { - self.deploys = iter::repeat(()) - .take(count) + self.deploys = iter::repeat_n((), count) .map(|_| Deploy::random(rng)) .collect(); self diff --git a/types/src/chainspec/accounts_config/genesis.rs b/types/src/chainspec/accounts_config/genesis.rs index 86f789e956..c1bbe02e57 100644 --- a/types/src/chainspec/accounts_config/genesis.rs +++ b/types/src/chainspec/accounts_config/genesis.rs @@ -281,7 +281,7 @@ impl GenesisAccount { /// some amount of delegated stake. pub fn staked_amount(&self) -> Motes { match self { - GenesisAccount::System { .. } + GenesisAccount::System | GenesisAccount::Account { validator: None, .. } => Motes::zero(), @@ -327,7 +327,7 @@ impl GenesisAccount { /// Is this a virtual system account. pub fn is_system_account(&self) -> bool { - matches!(self, GenesisAccount::System { .. }) + matches!(self, GenesisAccount::System) } /// Is this a validator account. @@ -336,7 +336,7 @@ impl GenesisAccount { GenesisAccount::Account { validator: Some(_), .. } => true, - GenesisAccount::System { .. } + GenesisAccount::System | GenesisAccount::Account { validator: None, .. } diff --git a/types/src/cl_type.rs b/types/src/cl_type.rs index 55abaee605..260f82276f 100644 --- a/types/src/cl_type.rs +++ b/types/src/cl_type.rs @@ -743,8 +743,7 @@ mod tests { // [18, 18, 18, ..., 9] for i in 1..1000 { - let bytes = iter::repeat(CL_TYPE_TAG_TUPLE1) - .take(i) + let bytes = iter::repeat_n(CL_TYPE_TAG_TUPLE1, i) .chain(iter::once(CL_TYPE_TAG_UNIT)) .collect(); match bytesrepr::deserialize(bytes) { @@ -761,9 +760,8 @@ mod tests { // [0, 0, 0, 0, 18, 18, 18, ..., 18, 9] for i in 1..1000 { - let bytes = iter::repeat(0) - .take(4) - .chain(iter::repeat(CL_TYPE_TAG_TUPLE1).take(i)) + let bytes = iter::repeat_n(0, 4) + .chain(iter::repeat_n(CL_TYPE_TAG_TUPLE1, i)) .chain(iter::once(CL_TYPE_TAG_UNIT)) .collect(); match bytesrepr::deserialize::(bytes) { diff --git a/types/src/contracts.rs b/types/src/contracts.rs index 5eee24582c..ae6cf4bc71 100644 --- a/types/src/contracts.rs +++ b/types/src/contracts.rs @@ -616,13 +616,14 @@ impl JsonSchema for ContractPackageHash { } /// A enum to determine the lock status of the contract package. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub enum ContractPackageStatus { /// The package is locked and cannot be versioned. Locked, /// The package is unlocked and can be versioned. + #[default] Unlocked, } @@ -637,12 +638,6 @@ impl ContractPackageStatus { } } -impl Default for ContractPackageStatus { - fn default() -> Self { - Self::Unlocked - } -} - impl ToBytes for ContractPackageStatus { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut result = bytesrepr::allocate_buffer(self)?; diff --git a/types/src/digest.rs b/types/src/digest.rs index bbf0002b2f..1d6b920e16 100644 --- a/types/src/digest.rs +++ b/types/src/digest.rs @@ -464,7 +464,7 @@ mod tests { #[test] fn from_valid_hex_should_succeed() { for char in "abcdefABCDEF0123456789".chars() { - let input: String = iter::repeat(char).take(64).collect(); + let input: String = iter::repeat_n(char, 64).collect(); assert!(Digest::from_hex(input).is_ok()); } } @@ -480,7 +480,7 @@ mod tests { #[test] fn from_hex_invalid_char_should_fail() { for char in "g %-".chars() { - let input: String = iter::repeat('f').take(63).chain(iter::once(char)).collect(); + let input: String = iter::repeat_n('f', 63).chain(iter::once(char)).collect(); assert!(Digest::from_hex(input).is_err()); } } diff --git a/types/src/lib.rs b/types/src/lib.rs index 7581816783..0a3fbfca9b 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -195,9 +195,9 @@ pub use timestamp::{TimeDiff, Timestamp}; #[cfg(any(feature = "std", test))] pub use transaction::{calculate_lane_id_for_deploy, calculate_transaction_lane, GasLimited}; pub use transaction::{ - AddressableEntityIdentifier, Approval, ApprovalsHash, Deploy, DeployDecodeFromJsonError, - DeployError, DeployExcessiveSizeError, DeployHash, DeployHeader, DeployId, - ExecutableDeployItem, ExecutableDeployItemIdentifier, ExecutionInfo, InitiatorAddr, + AddressableEntityIdentifier, Approval, ApprovalsHash, Deploy, DeployCategory, + DeployDecodeFromJsonError, DeployError, DeployExcessiveSizeError, DeployHash, DeployHeader, + DeployId, ExecutableDeployItem, ExecutableDeployItemIdentifier, ExecutionInfo, InitiatorAddr, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, NamedArg, PackageIdentifier, PricingMode, PricingModeError, RuntimeArgs, Transaction, TransactionArgs, TransactionEntryPoint, TransactionHash, TransactionId, TransactionInvocationTarget, diff --git a/types/src/package.rs b/types/src/package.rs index c6286eeff5..c5617552a2 100644 --- a/types/src/package.rs +++ b/types/src/package.rs @@ -562,13 +562,14 @@ impl From<&PublicKey> for PackageHash { } /// A enum to determine the lock status of the package. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub enum PackageStatus { /// The package is locked and cannot be versioned. Locked, /// The package is unlocked and can be versioned. + #[default] Unlocked, } @@ -583,12 +584,6 @@ impl PackageStatus { } } -impl Default for PackageStatus { - fn default() -> Self { - Self::Unlocked - } -} - impl ToBytes for PackageStatus { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut result = bytesrepr::allocate_buffer(self)?; diff --git a/types/src/system/auction.rs b/types/src/system/auction.rs index 0bb4c03c6d..016479e5c9 100644 --- a/types/src/system/auction.rs +++ b/types/src/system/auction.rs @@ -504,8 +504,10 @@ impl BidsExt for Vec { if let BidKind::Unified(unified) = bid_kind { let delegators = unified .delegators() - .iter() - .map(|(_, y)| DelegatorKind::PublicKey(y.delegator_public_key().clone())) + .values() + .map(|delegator| { + DelegatorKind::PublicKey(delegator.delegator_public_key().clone()) + }) .collect(); ret.insert(unified.validator_public_key().clone(), delegators); } diff --git a/types/src/transaction.rs b/types/src/transaction.rs index 4d2b56e418..36a96e10a3 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -68,8 +68,9 @@ pub use approvals_hash::ApprovalsHash; #[cfg(any(feature = "std", test))] pub use deploy::calculate_lane_id_for_deploy; pub use deploy::{ - Deploy, DeployDecodeFromJsonError, DeployError, DeployExcessiveSizeError, DeployHash, - DeployHeader, DeployId, ExecutableDeployItem, ExecutableDeployItemIdentifier, InvalidDeploy, + Deploy, DeployCategory, DeployDecodeFromJsonError, DeployError, DeployExcessiveSizeError, + DeployHash, DeployHeader, DeployId, ExecutableDeployItem, ExecutableDeployItemIdentifier, + InvalidDeploy, }; pub use error::InvalidTransaction; pub use execution_info::ExecutionInfo; @@ -568,10 +569,10 @@ impl<'de> Deserialize<'de> for Transaction { #[serde(deny_unknown_fields)] enum TransactionJson { /// A deploy. - Deploy(Deploy), + Deploy(Box), /// A version 1 transaction. #[serde(rename = "Version1")] - V1(TransactionV1Json), + V1(Box), /// An EVM signed RLP transaction. Evm(Box), } @@ -588,9 +589,9 @@ impl TryFrom for Transaction { type Error = TransactionJsonError; fn try_from(transaction: TransactionJson) -> Result { match transaction { - TransactionJson::Deploy(deploy) => Ok(Transaction::Deploy(deploy)), + TransactionJson::Deploy(deploy) => Ok(Transaction::Deploy(*deploy)), TransactionJson::V1(v1) => { - TransactionV1::try_from(v1) + TransactionV1::try_from(*v1) .map(Transaction::V1) .map_err(|error| { TransactionJsonError::FailedToMap(format!( @@ -609,8 +610,9 @@ impl TryFrom for TransactionJson { type Error = TransactionJsonError; fn try_from(transaction: Transaction) -> Result { match transaction { - Transaction::Deploy(deploy) => Ok(TransactionJson::Deploy(deploy)), + Transaction::Deploy(deploy) => Ok(TransactionJson::Deploy(Box::new(deploy))), Transaction::V1(v1) => TransactionV1Json::try_from(v1) + .map(Box::new) .map(TransactionJson::V1) .map_err(|error| { TransactionJsonError::FailedToMap(format!( diff --git a/types/src/transaction/deploy.rs b/types/src/transaction/deploy.rs index a08c2dde8d..b817aa4bda 100644 --- a/types/src/transaction/deploy.rs +++ b/types/src/transaction/deploy.rs @@ -5,6 +5,8 @@ mod deploy_id; mod error; mod executable_deploy_item; +pub use deploy_category::DeployCategory; + use alloc::{collections::BTreeSet, vec::Vec}; use core::{ cmp, diff --git a/types/src/transaction/deploy/deploy_category.rs b/types/src/transaction/deploy/deploy_category.rs index 9071fc41bd..91f62d4b9f 100644 --- a/types/src/transaction/deploy/deploy_category.rs +++ b/types/src/transaction/deploy/deploy_category.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; )] #[serde(deny_unknown_fields)] #[repr(u8)] +#[allow(dead_code)] pub enum DeployCategory { /// Standard transaction (the default). #[default] diff --git a/types/src/transaction/deploy/error.rs b/types/src/transaction/deploy/error.rs index 14ca089463..d81d492f1e 100644 --- a/types/src/transaction/deploy/error.rs +++ b/types/src/transaction/deploy/error.rs @@ -318,7 +318,7 @@ impl StdError for InvalidDeploy { match self { InvalidDeploy::InvalidApproval { error, .. } => Some(error), InvalidDeploy::InvalidChainName { .. } - | InvalidDeploy::DependenciesNoLongerSupported { .. } + | InvalidDeploy::DependenciesNoLongerSupported | InvalidDeploy::ExcessiveSize(_) | InvalidDeploy::ExcessiveTimeToLive { .. } | InvalidDeploy::TimestampInFuture { .. } diff --git a/types/src/transaction/serialization/mod.rs b/types/src/transaction/serialization/mod.rs index e410ac711b..cba5c2e344 100644 --- a/types/src/transaction/serialization/mod.rs +++ b/types/src/transaction/serialization/mod.rs @@ -98,7 +98,7 @@ impl CalltableSerializationEnvelope { size } - pub fn start_consuming(&self) -> Result, Error> { + pub fn start_consuming(&self) -> Result>, Error> { if self.fields.is_empty() { return Ok(None); } @@ -156,12 +156,12 @@ impl CalltableFieldsIterator<'_> { pub fn deserialize_and_maybe_next( &self, - ) -> Result<(T, Option), Error> { + ) -> Result<(T, Option>), Error> { let (t, maybe_window) = self.step()?; Ok((t, maybe_window)) } - fn step(&self) -> Result<(T, Option), Error> { + fn step(&self) -> Result<(T, Option>), Error> { let (t, remainder) = T::from_bytes(self.bytes)?; let parent_fields = &self.parent.fields; let parent_fields_len = parent_fields.len(); diff --git a/utils/validation/src/abi.rs b/utils/validation/src/abi.rs index 70d4770b07..dde4981caf 100644 --- a/utils/validation/src/abi.rs +++ b/utils/validation/src/abi.rs @@ -11,6 +11,7 @@ use crate::test_case::{Error, TestCase}; /// Representation of supported input value. #[derive(Serialize, Deserialize, Debug, From)] +#[allow(clippy::large_enum_variant)] #[serde(tag = "type", content = "value")] pub enum Input { U8(u8), From 3a5f50d8e0b24d14ee19ebc948f190303d37353e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 7 May 2026 13:44:48 +0200 Subject: [PATCH 05/17] Add Casper-style fee handling for EVM transactions --- Cargo.lock | 590 ++++++++++----- Cargo.toml | 5 - EVM.md | 714 ++++++++++++++++++ binary_port/src/command.rs | 30 +- binary_port/src/lib.rs | 5 +- binary_port/src/response_type.rs | 14 +- binary_port/src/type_wrappers.rs | 191 +++++ executor/evm/Cargo.toml | 8 +- executor/evm/src/block_hash.rs | 16 +- executor/evm/src/db.rs | 7 +- executor/evm/src/executor.rs | 72 +- executor/evm/src/lib.rs | 12 +- executor/evm/src/outcome.rs | 19 +- executor/evm/src/request.rs | 11 + executor/evm/src/state.rs | 33 + executor/evm/src/tx.rs | 32 +- executor/evm/tests/executor.rs | 25 +- node/Cargo.toml | 6 + node/src/components/binary_port.rs | 40 +- node/src/components/binary_port/event.rs | 1 + node/src/components/contract_runtime.rs | 26 +- .../components/contract_runtime/operations.rs | 311 ++++++-- node/src/components/contract_runtime/types.rs | 65 +- node/src/components/contract_runtime/utils.rs | 58 +- node/src/components/network/tasks.rs | 199 +++-- node/src/components/storage.rs | 13 +- node/src/components/transaction_acceptor.rs | 62 +- node/src/effect.rs | 22 + node/src/effect/requests.rs | 25 +- .../main_reactor/tests/configs_override.rs | 11 +- .../src/reactor/main_reactor/tests/fixture.rs | 4 + .../main_reactor/tests/transactions.rs | 411 +++++++++- .../src/types/transaction/meta_transaction.rs | 363 ++++++++- .../transaction/meta_transaction/meta_evm.rs | 168 +++++ .../meta_transaction/transaction_header.rs | 2 +- resources/integration-test/chainspec.toml | 6 +- resources/local/chainspec.toml.in | 8 +- resources/mainnet/chainspec.toml | 6 +- resources/production/chainspec.toml | 6 +- resources/testnet/chainspec.toml | 6 +- .../contract/src/no_std_handlers.rs | 2 - smart_contracts/rust-toolchain | 2 +- .../genesis/account_contract_installer.rs | 15 + .../src/system/genesis/entity_installer.rs | 15 + storage/src/system/transfer.rs | 35 +- types/Cargo.toml | 7 +- types/src/chainspec/genesis_config.rs | 9 + types/src/evm.rs | 4 +- types/src/evm/account.rs | 6 + types/src/evm/address.rs | 24 +- types/src/evm/hash.rs | 39 +- types/src/evm/receipt.rs | 41 +- types/src/evm/transaction.rs | 663 ++++++++++++++-- types/src/transaction.rs | 56 +- types/src/transaction/transaction_hash.rs | 2 +- types/src/uint.rs | 8 + types/tests/evm_transaction.rs | 309 ++++++-- 57 files changed, 4176 insertions(+), 664 deletions(-) create mode 100644 EVM.md create mode 100644 node/src/types/transaction/meta_transaction/meta_evm.rs diff --git a/Cargo.lock b/Cargo.lock index 24698e17b4..469b335003 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,9 +119,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-consensus" -version = "1.0.22" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3b746060277f3d7f9c36903bb39b593a741cb7afcb0044164c28f0e9b673f0" +checksum = "b9b151e38e42f1586a01369ec52a6934702731d07e8509a7307331b09f6c46dc" dependencies = [ "alloy-eips", "alloy-primitives", @@ -151,33 +151,48 @@ dependencies = [ [[package]] name = "alloy-eip2930" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "serde", ] [[package]] name = "alloy-eip7702" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "k256", "serde", "thiserror 2.0.12", ] +[[package]] +name = "alloy-eip7928" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec6ae911a2fc304a7cb80a79fb7bed6d1474aed4e7c203df1f8ff538f64fc78d" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "once_cell", + "serde", +] + [[package]] name = "alloy-eips" -version = "1.0.22" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f562a81278a3ed83290e68361f2d1c75d018ae3b8589a314faf9303883e18ec9" +checksum = "f076d25ddfcd2f1cbcc234e072baf97567d1df0e3fccdc1f8af8cc8b18dc6299" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -186,18 +201,21 @@ dependencies = [ "alloy-rlp", "alloy-serde", "auto_impl", + "borsh", "c-kzg", "derive_more 2.0.1", "either", "serde", - "sha2 0.10.9", + "serde_with", + "sha2", + "thiserror 2.0.12", ] [[package]] name = "alloy-primitives" -version = "1.2.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a326d47106039f38b811057215a92139f46eef7983a4b77b10930a0ea5685b1e" +checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" dependencies = [ "alloy-rlp", "bytes", @@ -205,7 +223,7 @@ dependencies = [ "const-hex", "derive_more 2.0.1", "foldhash", - "hashbrown 0.15.3", + "hashbrown 0.16.1", "indexmap 2.9.0", "itoa", "k256", @@ -213,11 +231,11 @@ dependencies = [ "paste", "proptest", "rand 0.9.4", + "rapidhash", "ruint", "rustc-hash 2.1.2", "serde", "sha3", - "tiny-keccak", ] [[package]] @@ -244,9 +262,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.41" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64600fc6c312b7e0ba76f73a381059af044f4f21f43e07f51f1fa76c868fe302" +checksum = "11ece63b89294b8614ab3f483560c08d016930f842bf36da56bf0b764a15c11e" dependencies = [ "alloy-primitives", "serde", @@ -255,32 +273,41 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.9.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bada1fc392a33665de0dc50d401a3701b62583c655e3522a323490a5da016962" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" dependencies = [ "alloy-primitives", "alloy-rlp", - "arrayvec 0.7.6", "derive_more 2.0.1", "nybbles", "smallvec", + "thiserror 2.0.12", "tracing", ] [[package]] name = "alloy-tx-macros" -version = "1.0.22" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1154c8187a5ff985c95a8b2daa2fedcf778b17d7668e5e50e556c4ff9c881154" +checksum = "f8e52276fdb553d3c11563afad2898f4085165e4093604afe3d78b69afbf408f" dependencies = [ "alloy-primitives", - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.101", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -771,12 +798,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "az" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" - [[package]] name = "backtrace" version = "0.3.74" @@ -843,7 +864,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -896,11 +917,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -967,15 +988,6 @@ dependencies = [ "constant_time_eq 0.3.1", ] -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -1282,7 +1294,7 @@ dependencies = [ "blake2-rfc", "casper-contract-sdk-sys", "casper-executor-wasm-common", - "darling", + "darling 0.20.11", "paste", "proc-macro2", "quote", @@ -1295,7 +1307,7 @@ name = "casper-contract-sdk" version = "0.1.3" dependencies = [ "base16", - "bitflags 2.9.1", + "bitflags 2.11.1", "bnum", "borsh", "bytes", @@ -1427,11 +1439,11 @@ dependencies = [ "proptest", "rand 0.8.5", "rand_chacha 0.3.1", - "schemars", + "schemars 0.8.22", "serde", "serde_bytes", "serde_json", - "sha2 0.10.9", + "sha2", "strum 0.24.1", "tempfile", "thiserror 1.0.69", @@ -1485,7 +1497,7 @@ dependencies = [ name = "casper-executor-wasm-common" version = "0.1.3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "blake2 0.10.6", "borsh", "casper-contract-sdk-sys", @@ -1554,6 +1566,9 @@ dependencies = [ name = "casper-node" version = "2.2.0" dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", "ansi_term", "anyhow", "aquamarine", @@ -1567,6 +1582,7 @@ dependencies = [ "bytes", "casper-binary-port", "casper-execution-engine", + "casper-executor-evm", "casper-executor-wasm", "casper-executor-wasm-interface", "casper-storage", @@ -1588,6 +1604,7 @@ dependencies = [ "humantime", "hyper", "itertools 0.10.5", + "k256", "libc", "linked-hash-map", "lmdb-rkv", @@ -1612,9 +1629,10 @@ dependencies = [ "rand_core 0.6.4", "regex", "reqwest", + "revm", "rmp", "rmp-serde", - "schemars", + "schemars 0.8.22", "serde", "serde-big-array", "serde-map-to-array", @@ -1688,7 +1706,6 @@ dependencies = [ "alloy-consensus", "alloy-eips", "alloy-primitives", - "alloy-tx-macros", "base16", "base64 0.13.1", "bincode", @@ -1720,7 +1737,7 @@ dependencies = [ "proptest-derive", "rand 0.8.5", "rand_pcg", - "schemars", + "schemars 0.8.22", "serde", "serde-map-to-array", "serde_bytes", @@ -1861,6 +1878,18 @@ dependencies = [ "casper-types", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -2366,7 +2395,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "crossterm_winapi", "derive_more 2.0.1", "document-features", @@ -2468,8 +2497,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -2486,13 +2525,38 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.101", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.101", ] @@ -2568,6 +2632,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + [[package]] name = "derivative" version = "2.2.0" @@ -2696,7 +2770,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", "crypto-common", "subtle", @@ -2841,7 +2915,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2 0.10.9", + "sha2", "subtle", "zeroize", ] @@ -3198,7 +3272,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6da3ea9e1d1a3b1593e15781f930120e72aa7501610b2f82e5b6739c72e8eac5" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.101", @@ -3420,9 +3494,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "foreign-types" @@ -3823,16 +3897,6 @@ dependencies = [ "toml 0.5.11", ] -[[package]] -name = "gmp-mpfr-sys" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db155b537cb791b133341f99f68371d86ee7fa4c79aacfbc376d72d23c70531" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "group" version = "0.13.0" @@ -3903,8 +3967,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", - "foldhash", - "serde", ] [[package]] @@ -3912,6 +3974,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] [[package]] name = "headers" @@ -4162,6 +4229,30 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -4468,7 +4559,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -4567,57 +4658,11 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "libc", "redox_syscall", ] -[[package]] -name = "libsecp256k1" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" -dependencies = [ - "arrayref", - "base64 0.22.1", - "digest 0.9.0", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand 0.8.5", - "serde", - "sha2 0.9.9", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" -dependencies = [ - "libsecp256k1-core", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -5106,6 +5151,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-derive" version = "0.4.2" @@ -5259,7 +5310,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "cfg-if 1.0.0", "foreign-types", "libc", @@ -5347,7 +5398,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -5444,9 +5495,9 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", "phf_shared", @@ -5455,19 +5506,19 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ + "fastrand", "phf_shared", - "rand 0.8.5", ] [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ "phf_generator", "phf_shared", @@ -5478,9 +5529,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ "siphasher", ] @@ -5658,6 +5709,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "pprof" version = "0.14.0" @@ -5818,7 +5875,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.1", + "bitflags 2.11.1", "lazy_static", "num-traits", "rand 0.8.5", @@ -5898,7 +5955,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "memchr", "unicase", ] @@ -6091,6 +6148,15 @@ dependencies = [ "casper-types", ] +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + [[package]] name = "raw-cpuid" version = "10.7.0" @@ -6150,7 +6216,27 @@ version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -6415,9 +6501,9 @@ dependencies = [ [[package]] name = "revm" -version = "27.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6bf82101a1ad8a2b637363a37aef27f88b4efc8a6e24c72bf5f64923dc5532" +checksum = "91202d39dbe8e8d10e9e8f2b76c30da68ecd1d25be69ba6d853ad0d03a3a398a" dependencies = [ "revm-bytecode", "revm-context", @@ -6434,12 +6520,11 @@ dependencies = [ [[package]] name = "revm-bytecode" -version = "6.1.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6922f7f4fbc15ca61ea459711ff75281cc875648c797088c34e4e064de8b8a7c" +checksum = "bdbb3a3d735efa94c91f2ef6bf20a35f99a77bc78f3e25bd758336901bdf9661" dependencies = [ "bitvec", - "once_cell", "phf", "revm-primitives", "serde", @@ -6447,10 +6532,11 @@ dependencies = [ [[package]] name = "revm-context" -version = "8.0.4" +version = "16.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd508416a35a4d8a9feaf5ccd06ac6d6661cd31ee2dc0252f9f7316455d71f9" +checksum = "c5f68d928d8b228e0faeb1c6ed75c4fde7d124f1ddf9119b67e7a0ad4041237d" dependencies = [ + "bitvec", "cfg-if 1.0.0", "derive-where", "revm-bytecode", @@ -6463,9 +6549,9 @@ dependencies = [ [[package]] name = "revm-context-interface" -version = "9.0.0" +version = "17.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc90302642d21c8f93e0876e201f3c5f7913c4fcb66fb465b0fd7b707dfe1c79" +checksum = "1f3758e6167c4ba7a59a689c519a047edaefcd4c37d74f279b93ed87bc8aece4" dependencies = [ "alloy-eip2930", "alloy-eip7702", @@ -6479,9 +6565,9 @@ dependencies = [ [[package]] name = "revm-database" -version = "7.0.2" +version = "13.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61495e01f01c343dd90e5cb41f406c7081a360e3506acf1be0fc7880bfb04eb" +checksum = "c281a1f11d3bcb8c0bba1199ed6bcb001d1aeb3d4fb366819e14f88723989a4e" dependencies = [ "alloy-eips", "revm-bytecode", @@ -6493,22 +6579,23 @@ dependencies = [ [[package]] name = "revm-database-interface" -version = "7.0.2" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20628d6cd62961a05f981230746c16854f903762d01937f13244716530bf98f" +checksum = "d89efb9832a4e3742bb4ded5f7fe5bf905e8860e69427d4dfec153484fc6d304" dependencies = [ "auto_impl", "either", "revm-primitives", "revm-state", "serde", + "thiserror 2.0.12", ] [[package]] name = "revm-handler" -version = "8.1.0" +version = "18.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1529c8050e663be64010e80ec92bf480315d21b1f2dbf65540028653a621b27d" +checksum = "783e903d6922b7f5f9a940d1bb229530502d2924b1aed9d5ca5a94ebf065d460" dependencies = [ "auto_impl", "derive-where", @@ -6525,9 +6612,9 @@ dependencies = [ [[package]] name = "revm-inspector" -version = "8.1.0" +version = "19.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78db140e332489094ef314eaeb0bd1849d6d01172c113ab0eb6ea8ab9372926" +checksum = "8216ad58422090d0daa9eb430e0a081f7ad07e7fd30681dee71f8420c99624e0" dependencies = [ "auto_impl", "either", @@ -6543,21 +6630,22 @@ dependencies = [ [[package]] name = "revm-interpreter" -version = "24.0.0" +version = "35.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9d7d9d71e8a33740b277b602165b6e3d25fff091ba3d7b5a8d373bf55f28a7" +checksum = "1ece9f41b69658c15d748288a4dbdfc06a63f3ce93d983af440de3f1631dce6a" dependencies = [ "revm-bytecode", "revm-context-interface", "revm-primitives", + "revm-state", "serde", ] [[package]] name = "revm-precompile" -version = "25.0.0" +version = "34.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cee3f336b83621294b4cfe84d817e3eef6f3d0fce00951973364cc7f860424d" +checksum = "a346a8cc6c8c39bd65306641c692191299c0a7b63d38810e39e8fe9b92378660" dependencies = [ "ark-bls12-381", "ark-bn254", @@ -6570,34 +6658,34 @@ dependencies = [ "c-kzg", "cfg-if 1.0.0", "k256", - "libsecp256k1", - "once_cell", "p256", + "revm-context-interface", "revm-primitives", "ripemd", - "rug", "secp256k1", - "sha2 0.10.9", + "sha2", ] [[package]] name = "revm-primitives" -version = "20.1.0" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66145d3dc61c0d6403f27fc0d18e0363bb3b7787e67970a05c71070092896599" +checksum = "0c99bda77d9661521ba0b4bc04558c6692074f01e65dd420fa3b893033d9b8a2" dependencies = [ "alloy-primitives", "num_enum", + "once_cell", "serde", ] [[package]] name = "revm-state" -version = "7.0.2" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc830a0fd2600b91e371598e3d123480cd7bb473dd6def425a51213aa6c6d57" +checksum = "c32490ed687dba31c3c882beb8c20408bdd30ef96690d8f145b0ee9a87040bfe" dependencies = [ - "bitflags 2.9.1", + "alloy-eip7928", + "bitflags 2.11.1", "revm-bytecode", "revm-primitives", "serde", @@ -6707,18 +6795,6 @@ dependencies = [ "serde", ] -[[package]] -name = "rug" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07a8857882aec59d27254b02481c709327c13de6fad1da60bfc4f9783eaaa61e" -dependencies = [ - "az", - "gmp-mpfr-sys", - "libc", - "libm", -] - [[package]] name = "ruint" version = "1.16.0" @@ -6800,7 +6876,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -6923,6 +6999,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -6987,7 +7087,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -7062,7 +7162,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c14b52efc56c711e0dbae3f26e0cc233f5dac336c1bf0b07e1b7dc2dca3b2cc7" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] @@ -7171,6 +7271,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "set-action-thresholds" version = "0.1.0" @@ -7190,19 +7322,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.9" @@ -7755,12 +7874,34 @@ dependencies = [ ] [[package]] -name = "tiny-keccak" -version = "2.0.2" +name = "time" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ - "crunchy", + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", ] [[package]] @@ -8954,7 +9095,7 @@ dependencies = [ "indexmap 2.9.0", "more-asserts", "rkyv", - "sha2 0.10.9", + "sha2", "target-lexicon", "thiserror 1.0.69", "xxhash-rust", @@ -8999,7 +9140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cc7c63191ae61c70befbe6045b9be65ef2082fa89421a386ae172cb1e08e92d" dependencies = [ "ahash", - "bitflags 2.9.1", + "bitflags 2.11.1", "hashbrown 0.14.5", "indexmap 2.9.0", "semver 1.0.26", @@ -9011,7 +9152,7 @@ version = "0.219.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5220ee4c6ffcc0cb9d7c47398052203bc902c8ef3985b0c8134118440c0b2921" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "indexmap 2.9.0", ] @@ -9021,7 +9162,7 @@ version = "0.230.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808198a69b5a0535583370a51d459baa14261dfab04800c4864ee9e1a14346ed" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "indexmap 2.9.0", "semver 1.0.26", ] @@ -9136,12 +9277,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -9397,7 +9591,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bfe0861afa..389280e7ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,11 +54,6 @@ exclude = ["utils/nctl/remotes/casper-client-rs"] resolver = "2" -# Include debug symbols in the release build of `casper-engine-tests` so that `simple-transfer` will yield useful -# perf data. -[profile.release.package.casper-engine-tests] -debug = true - [profile.release] codegen-units = 1 lto = true diff --git a/EVM.md b/EVM.md new file mode 100644 index 0000000000..19a37a983e --- /dev/null +++ b/EVM.md @@ -0,0 +1,714 @@ +# EVM Support Prototype + +This document describes the current EVM support implemented in this +workspace, how signed Ethereum transactions move through Casper node +execution, and how to reproduce the working Foundry deployment flow. + +The current scope is intentionally narrow. Casper node can accept and execute +`Transaction::Evm` transactions, store EVM execution results, and serve +read-only EVM calls through a binary-port command consumed by sidecar. Native +Ethereum JSON-RPC remains a sidecar concern. + +## EIP Glossary + +Only EIPs referenced by this document or the current code are listed here. + +| EIP | Link | Quick description | +| --- | --- | --- | +| [EIP-55][eip-55] | | Mixed-case checksum encoding for Ethereum addresses. Casper has similar checksummed hex helpers. | +| [EIP-155][eip-155] | | Replay protection for legacy transactions by including a chain ID in the signing payload. EVM transactions must carry the configured Casper EVM chain ID. | +| [EIP-2718][eip-2718] | | Typed transaction envelope format used by post-legacy Ethereum transaction types. The decoder accepts typed envelopes through Alloy. | +| [EIP-2930][eip-2930] | | Optional access-list transaction type. Empty access-list transactions decode; non-empty access lists are rejected for now. | +| [EIP-1559][eip-1559] | | Dynamic-fee transaction type with max fee and priority fee. Casper accepts this envelope for tooling compatibility only when the priority fee is zero. | +| [EIP-4844][eip-4844] | | Blob transaction support. Rejected because blob sidecars, blob gas, and KZG data are not modeled. | +| [EIP-7702][eip-7702] | | Set-code transactions for EOAs. Rejected because authorization-list processing and account-code mutation are not implemented. | + +## Current Scope + +Implemented in this workspace: + +- `casper_types::evm` wrapper types for EVM addresses, hashes, + transactions, accounts, config, and receipts. +- `Transaction::Evm` and `TransactionHash::Evm`. +- `ExecutionResult::Evm` carrying EVM receipt data. +- Global-state keys and values for EVM account metadata, bytecode, and + storage. +- EVM hash wrappers backed by Casper `Digest` while preserving raw Ethereum + 32-byte values. +- `casper-executor-evm`, backed by `revm`, with Casper-owned public types. +- Contract runtime execution for finalized `Transaction::Evm` values. +- Casper fee and refund handling for EVM transactions. +- Binary-port `EvmCall` for read-only `eth_call` support. +- EVM genesis account seeding for secp256k1 genesis accounts when + `[evm].enabled = true`. + +Implemented in the sidecar workspace for validation: + +- Minimal Ethereum JSON-RPC methods on the existing `/rpc` endpoint: + `eth_chainId`, `eth_blockNumber`, `eth_getBlockByNumber`, + `eth_getTransactionCount`, `eth_sendRawTransaction`, + `eth_getTransactionReceipt`, and `eth_call`. +- Development-only Cargo patches pointing sidecar at this node workspace for + unreleased `casper-types` and `casper-binary-port` changes. + +Not implemented yet: + +- Native Ethereum JSON-RPC in node. +- `eth_estimateGas`, `eth_getBalance`, `eth_getCode`, historical `eth_call`, + `eth_getLogs`, or `eth_getTransactionByHash`. +- [EIP-4844][eip-4844] blob transactions. +- [EIP-7702][eip-7702] set-code transactions. +- Non-empty [EIP-2930][eip-2930]/[EIP-1559][eip-1559] access lists. +- EVM log indexing optimized for historical queries. + +## Transaction Shape + +`Transaction::Evm` stores `casper_types::evm::Transaction` directly, like the +other top-level transaction variants. It is not boxed, and it is not stored as +a raw signed RLP blob. The EVM transaction is stored as: + +- Casper envelope metadata: `timestamp` and `ttl`. +- Decoded unsigned Ethereum payload fields: + `kind`, `chain_id`, `nonce`, gas fields, `value`, `input`, and `to`. +- Claimed/recovered EVM sender address: `from`. +- Ethereum signed transaction hash: `hash`. +- Exactly one Casper `Approval` containing the Ethereum secp256k1 signature. + +`evm::Hash` and `evm::TransactionHash` are `Digest`-backed wrappers, but their +constructors preserve the supplied 32 bytes as raw Ethereum values. They do +not hash the bytes again. `evm::Hash` is used for EVM words, storage keys, +storage values, topics, and bytecode hashes. `evm::TransactionHash` is the +Ethereum transaction hash produced from the signed Ethereum envelope. + +The Ethereum transaction hash remains Ethereum-compatible. For an EVM +transaction: + +```text +Transaction::hash() == TransactionHash::Evm(evm_transaction.hash()) +``` + +The EVM hash is the Ethereum signed transaction hash, not a Casper hash of the +serialized `Transaction::Evm` wrapper. + +## RLP To Approval Conversion + +The raw signed Ethereum RLP transaction is handled outside node by sidecar. +The current `eth_sendRawTransaction` flow is: + +1. Sidecar receives raw signed Ethereum RLP. +2. Sidecar calls: + + ```rust + evm::Transaction::from_signed_rlp(raw, Timestamp::now(), ttl) + ``` + +3. `from_signed_rlp` decodes the Ethereum envelope. +4. It rejects unsupported transaction forms: + - [EIP-4844][eip-4844] blob transactions. + - [EIP-7702][eip-7702] set-code transactions. + - Non-empty access lists. + - Unknown typed transactions. +5. It extracts the unsigned Ethereum payload fields. +6. It recovers the secp256k1 public key and EVM address. +7. It converts the Ethereum signature into one Casper `Approval`: + - `Approval.signer` is the recovered secp256k1 public key. + - `Approval.signature` is the 64-byte secp256k1 signature. +8. It stores the Ethereum signed transaction hash. +9. Sidecar wraps the value as `Transaction::Evm` and submits it to node over + the existing binary-port transaction submission path. + +Node does not receive the raw RLP blob for `eth_sendRawTransaction`. Node +receives a typed `Transaction::Evm` whose approval set contains the Ethereum +signature. + +## Approval Handling + +`Transaction::Evm` uses the same approval container and approval identity +mechanics as other transaction variants: + +- `Transaction::approvals()` returns the EVM approval set. +- `Transaction::compute_approvals_hash()` computes the hash of that approval + set. +- `Transaction::compute_id()` combines `TransactionHash::Evm` with the EVM + approvals hash. +- Finalized approvals checksums include EVM approvals through the same storage + mechanism used by other transactions. +- Block execution computes approvals checksums across EVM and non-EVM + transactions. + +The cryptographic verification is EVM-specific. Deploy and V1 transaction +approvals verify Casper signatures over Casper transaction hashes. +`Transaction::Evm::verify()` reconstructs the signed Ethereum envelope using +the stored payload and approval, then checks: + +- There is exactly one approval. +- The approval uses secp256k1. +- One of the valid secp256k1 recovery IDs recovers the approval signer. +- The recovered EVM address matches the stored `from`. +- The reconstructed Ethereum signed transaction hash matches the stored EVM + hash. + +This avoids double signing. The Ethereum signature becomes the single Casper +approval for the `Transaction::Evm` value. + +The generic `Transaction::sign` API also has an EVM implementation. For +`Transaction::Evm`, it requires a secp256k1 secret key, signs the Ethereum +signing hash using recoverable prehash semantics, replaces the approval set +with exactly one Ethereum-style approval, recomputes `from`, and recomputes the +Ethereum signed transaction hash. `evm::Transaction::try_sign` returns an +error for non-secp256k1 keys; the infallible top-level `Transaction::sign` +path panics with a clear message, matching the existing infallible signing API +style. + +Approvals for EVM transactions are part of the Ethereum transaction identity. +Changing the approval changes the reconstructed signed Ethereum envelope and +therefore the Ethereum transaction hash. For that reason finalized approval +handling treats the EVM approval as immutable transaction content. The +top-level approval replacement hook used after storage lookup leaves +`Transaction::Evm` unchanged. + +## Acceptor Validation + +`Transaction::Evm` is converted into `MetaTransaction::Evm` and routed through +the normal transaction acceptor skeleton. EVM branches only where the payload +genuinely differs from Deploy and V1/V2 transactions: identity, balance +lookup, no Casper session/payment validation, and EVM-specific chainspec +checks. + +For client-submitted EVM transactions, the acceptor currently validates: + +1. `[evm].enabled` must be true. +2. `evm_transaction.verify()` must pass. +3. The transaction must not be expired. +4. `evm_transaction.chain_id()` must be present. +5. The EVM chain ID must equal `[evm].chain_id`. +6. The EVM gas limit must not exceed `[evm].block_gas_limit`. +7. Legacy and [EIP-2930][eip-2930] gas price must be at least + `[evm].base_fee`. +8. [EIP-1559][eip-1559] `max_fee_per_gas` must be at least + `[evm].base_fee`. +9. [EIP-1559][eip-1559] `max_priority_fee_per_gas` must be zero because + Casper does not currently prioritize transactions based on transaction gas + parameters. +10. `BalanceIdentifier::Evm(from)` must resolve to a balance. +11. That balance must meet the chain baseline motes requirement. + +The acceptor does not require a Casper `AddressableEntity` for the EVM sender. +The sender identity is `InitiatorAddr::EvmAddress(transaction.from())`, and +balance checks use `BalanceIdentifier::Evm(address)`. The acceptor only checks +that the EVM initiator has a known balance and meets the same baseline balance +requirement used for other client transactions. The runtime later checks the +full EVM maximum fee amount. + +## Runtime Execution + +Finalized block execution now routes EVM transactions through the same +per-transaction accounting skeleton used by Deploy and V1/V2 transactions. +Runtime constructs `MetaTransaction::Evm` before entering the loop's normal +balance, hold, refund, fee, and artifact builder flow. + +At the start of each loop iteration runtime checks whether the stored +transaction is EVM: + +```text +evm_transaction = stored_transaction.as_evm() +meta_transaction = MetaTransaction::from_transaction(stored_transaction, ...) +``` + +Common metadata such as hash, initiator, authorization keys, size estimate, +gas limit, and cost is derived directly from `Transaction`. For EVM: + +- the initiator is `InitiatorAddr::EvmAddress(transaction.from())`, +- the transaction lane is currently the last configured Wasm lane, +- the gas limit is the Ethereum transaction gas limit, +- the maximum cost is `gas_limit * effective_gas_price`, +- payment is treated as standard-payment-like, +- custom payment and refund-purse setup are skipped. + +Configuration compliance is enforced by the acceptor through +`MetaTransaction::is_config_compliant`. Finalized block execution expects block +contents to have passed those checks and does not duplicate the acceptor's EVM +enablement, signature, TTL, chain ID, base-fee, or priority-fee validation. + +Runtime still checks the payment/accounting conditions that depend on current +state. If the EVM sender cannot cover the required balance, or if execution is +otherwise disallowed by the shared accounting loop, runtime stores an +`ExecutionResult::Evm` with a failure receipt, zero cost, zero consumed amount, +and no EVM execution effects. The EVM receipt status is typed; EVM receipts do +not persist a free-form error string for revert, halt, or accounting +precondition failure. + +When execution proceeds: + +1. Runtime creates a processing hold against + `BalanceIdentifier::Evm(transaction.from())`. +2. Runtime enters the shared execution `match` through the `_ if is_evm` arm. +3. Runtime checks out a tracking copy at the current scratch state root. +4. Runtime builds an EVM block context from Casper block data: + - block height, + - block timestamp, + - deterministic proposer-derived beneficiary, + - `[evm].block_gas_limit`, + - `[evm].base_fee`. +5. Runtime calls `casper-executor-evm` with `FeeCharge::External`. +6. `revm` executes EVM account, nonce, code, storage, log, create, and value + transfer semantics. +7. Runtime commits EVM tracking-copy effects into scratch global state. +8. `ExecutionArtifactBuilder` records the EVM receipt, EVM effects, and the + consumed amount. +9. Runtime clears the processing hold. +10. Runtime applies Casper refund handling. +11. Runtime applies Casper fee handling. +12. Runtime stores `ExecutionResult::Evm`. + +`FeeCharge::External` is important. It prevents `revm` from charging gas fees +from EVM balances. Casper runtime owns fee and refund policy. + +## Fee And Refund Policy + +EVM transactions deliberately use Casper chain-level fee and refund policy, +rather than silently emulating Ethereum's full gas escrow semantics. + +Runtime computes: + +```text +effective_gas_price = transaction.effective_gas_price([evm].base_fee) +max_fee_amount = gas_limit * effective_gas_price +``` + +The current chainspec base fee is: + +```text +[evm].base_fee = 1_000_000 motes per EVM gas +``` + +At the current `evm.block_gas_limit` of 30,000,000 gas, filling the EVM block +gas limit costs 30,000 CSPR before any refund policy is applied: + +```text +30,000,000 gas * 1,000,000 motes/gas = 30,000,000,000,000 motes +30,000,000,000,000 motes / 1,000,000,000 = 30,000 CSPR +``` + +For legacy and [EIP-2930][eip-2930] transactions, the effective gas price is +the signed gas price. For [EIP-1559][eip-1559] transactions, the acceptor +requires `max_priority_fee_per_gas == 0` because Casper does not currently +prioritize transactions based on transaction gas parameters. Accepted EIP-1559 +transactions therefore pay `[evm].base_fee`; `max_fee_per_gas` is only a +sender cap and must be high enough to cover the base fee. + +The maximum fee is held from `BalanceIdentifier::Evm(from)`. After execution: + +- Successful execution consumes `gas_used * effective_gas_price`. +- Failed/reverted/halted execution consumes the full held amount. +- The unconsumed portion is processed through Casper `RefundHandling`. +- The final fee is processed through Casper `FeeHandling`. + +This keeps EVM transactions aligned with the same chain policy knobs used by +Deploy and V1/V2 transactions. The EVM gas price is converted to motes before +calling the balance/fee/refund machinery. + +In the shared accounting loop, EVM cost is already expressed as motes. Refund +calculation therefore uses `cost_to_use()` with an effective runtime gas price +of `1` for the refund-mode call, instead of multiplying by Casper's current +native transaction gas price again. This prevents double scaling while still +allowing `RefundHandling::{NoRefund,Burn,Refund}` and +`FeeHandling::{NoFee,Burn,PayToProposer,Accumulate}` to apply uniformly. + +EVM does not support Casper custom payment or refund-purse selection in this +prototype. That is intentional: Ethereum payloads do not carry Casper payment +code, but the chain still owns fee and refund policy. The EVM sender's main +purse is the payer for the processing hold, refund calculation, and final fee +handling. + +## Global State Layout + +EVM state is stored in Casper global state using typed keys and values: + +- `Key::EvmAccount(Address)` stores `StoredValue::EvmAccount(Account)`. +- `Key::EvmByteCode(Hash)` stores `StoredValue::EvmByteCode(ByteCode)`. +- `Key::EvmStorage(StorageAddr)` stores `StoredValue::EvmStorage(StorageValue)`. + +An EVM account record contains: + +- nonce, +- code hash, +- main purse. + +Balances are Casper purse balances. EVM balance reads and writes reconcile +through the account main purse and `Key::Balance(main_purse.addr())`. + +When `[evm].enabled = true`, genesis creates an EVM account for each genesis +secp256k1 account. The EVM address is derived from the public key using +Ethereum address rules, and the EVM account uses the same main purse as the +Casper account. This avoids duplicating supply while allowing Ethereum +transactions to spend from the same funded devnet user. + +## Receipts + +Runtime stores EVM receipts in `ExecutionResult::Evm`. + +The receipt currently contains: + +- typed status: `Success`, `Revert`, or `Halt(reason)`, +- gas used, +- effective gas price, +- created contract address, +- logs. + +Sidecar maps the typed receipt status to Ethereum JSON-RPC receipt status: +`Success` becomes `0x1`; `Revert` and `Halt(_)` become `0x0`. The typed +Casper receipt keeps the extra distinction between revert and exceptional +halt, but Ethereum receipt projection remains compatible with standard tools. + +`ExecutionResult::Evm` stores the Casper accounting fields needed by existing +execution APIs: initiator, limit, cost, refund, current price, size estimate, +effects, and receipt. It does not store an `error_message`; EVM failure detail +lives in the typed receipt status. + +Block-derived Ethereum JSON-RPC fields are not persisted in the receipt. +Sidecar derives those fields from execution info and block transaction order: + +- block hash, +- block number, +- transaction index, +- cumulative gas used, +- log indexes, +- logs bloom, +- removed flag. + +## Read-only EVM Calls + +`eth_call` uses a node binary-port command, not transaction submission. + +The binary-port request carries: + +- `from`, +- `to`, +- `value`, +- input bytes, +- gas limit. + +Node handles the request only when speculative execution is enabled for the +binary port. Contract runtime checks out state at the requested/latest block, +runs `casper-executor-evm` with: + +- `ExecuteKind::Call`, +- `CallValidation::UncheckedSimulation`, +- `FeeCharge::External`, + +and returns output, status, and gas used. The tracking-copy effects are +discarded. + +## Block Hashes + +The executor exposes `BLOCK_HASH_HISTORY`, mirroring revm's Ethereum +`BLOCKHASH` history window. Node runtime and binary-port EVM calls use that +public executor constant when loading recent block hashes, so `casper-node` +does not need a production dependency on `revm`. + +Block hashes are loaded as Casper `BlockHash` values and converted to revm's +hash type only at the executor API boundary. If a contract requests a future +block, the current block, or a block outside the supported history window, +`BLOCKHASH` returns zero. + +## Chain ID + +The current local devnet EVM chain ID is: + +```text +0x435350ff +``` + +That is the Casper namespace prefix plus the local-network namespace. Sidecar +reports it through `eth_chainId`, and node requires signed EVM transactions to +carry the same value. + +## Reproducing With Foundry + +The following flow deploys and calls the EVM `Counter` fixture through +`casper-sidecar`. + +### Prerequisites + +Install the usual node build dependencies plus Foundry: + +```bash +forge --version +cast --version +``` + +Set shell variables for the local checkouts used below: + +```bash +export CASPER_NODE_WORKSPACE=/path/to/casper-node +export CASPER_SIDECAR_WORKSPACE=/path/to/casper-sidecar +``` + +The sidecar workspace must be patched to the node workspace because these EVM +types are unreleased. In `$CASPER_SIDECAR_WORKSPACE/Cargo.toml`: + +```toml +[patch."https://github.com/casper-network/casper-node.git"] +casper-binary-port = { path = "/path/to/casper-node/binary_port" } +casper-types = { path = "/path/to/casper-node/types" } +``` + +If the node workspace path changes, update the patch paths. + +### Build Node And Sidecar + +From the node workspace: + +```bash +cargo build -p casper-node --bin casper-node +``` + +From the sidecar workspace: + +```bash +cd "$CASPER_SIDECAR_WORKSPACE" +cargo build -p casper-sidecar +``` + +### Install And Configure Devnet + +`casper-devnet` is a separate development tool. It is not part of this +`casper-node` repository, so `cargo run -- ...` only works from a +`casper-devnet` checkout, not from this workspace. + +The devnet tool needs a custom asset named `evm` that points at the debug node +and sidecar binaries built above, plus the local chainspec and config files +from this workspace. + +From a separate `casper-devnet` checkout, register the asset with: + +```bash +cd /path/to/casper-devnet +cargo run -- assets add evm \ + --casper-node "$CASPER_NODE_WORKSPACE/target/debug/casper-node" \ + --casper-sidecar "$CASPER_SIDECAR_WORKSPACE/target/debug/casper-sidecar" \ + --chainspec "$CASPER_NODE_WORKSPACE/resources/local/chainspec.toml" \ + --node-config "$CASPER_NODE_WORKSPACE/resources/local/config.toml" \ + --sidecar-config "$CASPER_SIDECAR_WORKSPACE/resources/example_configs/default_rpc_only_config.toml" +``` + +If `casper-devnet` is already installed on `PATH`, the equivalent command is: + +```bash +casper-devnet assets add evm \ + --casper-node "$CASPER_NODE_WORKSPACE/target/debug/casper-node" \ + --casper-sidecar "$CASPER_SIDECAR_WORKSPACE/target/debug/casper-sidecar" \ + --chainspec "$CASPER_NODE_WORKSPACE/resources/local/chainspec.toml" \ + --node-config "$CASPER_NODE_WORKSPACE/resources/local/config.toml" \ + --sidecar-config "$CASPER_SIDECAR_WORKSPACE/resources/example_configs/default_rpc_only_config.toml" +``` + +Register the asset once for a given set of paths. Rebuilding the node or +sidecar binaries does not require re-adding the asset as long as the asset +points at those debug binary paths. You can inspect the installed custom asset +with: + +```bash +casper-devnet assets path evm +``` + +If the asset already exists and needs different paths, remove or recreate the +existing custom asset directory first, then run `assets add` again. + +### Start Devnet + +After the `evm` asset is registered, start the network from any directory where +the `casper-devnet` binary is available: + +```bash +casper-devnet start --custom-asset evm --force-setup \ + --chainspec-override evm.enabled=true +``` + +The `evm` custom asset uses the debug node binary from +`$CASPER_NODE_WORKSPACE` and the debug sidecar binary from +`$CASPER_SIDECAR_WORKSPACE`. + +Devnet is not yet fully aware of the new EVM variants, so it can log SSE decode +warnings after EVM transactions are accepted or included in blocks. Those +warnings do not block this JSON-RPC validation flow. + +### Devnet User + +Use the deterministic devnet `user-1` secp256k1 key: + +```text +private key: 0xb6cc5d5faa7c3c37db4bf9a1566023aaa9a1d716fe78ed1a6fb79a690b9400e8 +EVM address: 0x24790C4849cCAE43c0c1749e2C5b8d00Cc63AB80 +``` + +Confirm sidecar and account state: + +```bash +curl -s -X POST http://127.0.0.1:11101/rpc \ + -H 'content-type: application/json' \ + --data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' + +curl -s -X POST http://127.0.0.1:11101/rpc \ + -H 'content-type: application/json' \ + --data '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionCount","params":["0x24790C4849cCAE43c0c1749e2C5b8d00Cc63AB80","latest"]}' + +casper-cli account balance devnet:user-1 +``` + +Expected initial values on a fresh devnet: + +```text +eth_chainId: 0x435350ff +eth_getTransactionCount: 0x0 +``` + +### Deploy Counter + +From the node workspace: + +```bash +forge create --broadcast \ + --rpc-url http://127.0.0.1:11101/rpc \ + --private-key 0xb6cc5d5faa7c3c37db4bf9a1566023aaa9a1d716fe78ed1a6fb79a690b9400e8 \ + --legacy \ + --gas-price 1000000 \ + --gas-limit 3000000 \ + --nonce 0 \ + smart_contracts/evm_contracts/Counter.sol:Counter +``` + +The current validation uses `--legacy` because the minimum RPC surface does +not yet include gas estimation or dynamic-fee helper methods, and Casper only +accepts [EIP-1559][eip-1559] transactions when +`max_priority_fee_per_gas == 0`. Passing an explicit legacy gas price equal to +`[evm].base_fee` keeps the transaction shape simple and avoids underpriced +transaction rejection. + +Expected output: + +```text +Deployer: 0x24790C4849cCAE43c0c1749e2C5b8d00Cc63AB80 +Deployed to: 0x6c0704679CA22b83778Ef815607359cf6F5352B6 +Transaction hash: 0xa86146276e1cc132ddb750e9e053c3e7a1381222104cefb0c6c567556e5e9198 +``` + +Forge may create local `cache/` and `out/` directories. They are build +artifacts and should not be committed. + +The corresponding receipt should contain: + +```text +status 0x1 +contractAddress 0x6c0704679ca22b83778ef815607359cf6f5352b6 +gasUsed 0x262ef +effectiveGasPrice 0xf4240 +``` + +### Read Counter + +```bash +cast call 0x6c0704679CA22b83778Ef815607359cf6F5352B6 \ + 'get()(uint256)' \ + --rpc-url http://127.0.0.1:11101/rpc +``` + +Expected output: + +```text +0 +``` + +### Increment Counter + +```bash +cast send 0x6c0704679CA22b83778Ef815607359cf6F5352B6 \ + 'increment()' \ + --rpc-url http://127.0.0.1:11101/rpc \ + --private-key 0xb6cc5d5faa7c3c37db4bf9a1566023aaa9a1d716fe78ed1a6fb79a690b9400e8 \ + --legacy \ + --gas-price 1000000 \ + --gas-limit 100000 \ + --nonce 1 +``` + +Expected receipt highlights: + +```text +status 1 (success) +type 0 +effectiveGasPrice 1000000 +gasUsed 43803 +to 0x6c0704679CA22b83778Ef815607359cf6F5352B6 +transactionHash 0x042ff975ec4b8fa8012f486bb7bd930e69978782b8b3c107ca2a276a43d7f293 +``` + +### Read Counter Again + +```bash +cast call 0x6c0704679CA22b83778Ef815607359cf6F5352B6 \ + 'get()(uint256)' \ + --rpc-url http://127.0.0.1:11101/rpc +``` + +Expected output: + +```text +1 +``` + +### Confirm Fees + +The EVM account uses the same main purse as devnet `user-1`, so the Casper +balance should decrease after deployment and increment: + +```bash +casper-cli account balance devnet:user-1 +``` + +With `--gas-price 1000000`, every 1,000 gas consumed is 1 CSPR before refund +policy is applied. + +In the latest successful validation run, the post-deploy and post-increment +balance was: + +```text +999999999999999999999999074.848500000 CSPR +``` + +## Useful Checks + +Node workspace: + +```bash +cargo check -p casper-node --bin casper-node +cargo test -p casper-binary-port evm_call --lib +``` + +Sidecar workspace: + +```bash +cd "$CASPER_SIDECAR_WORKSPACE" +cargo test -p casper-rpc-sidecar eth --lib +cargo build -p casper-sidecar +``` + +## Current Caveats + +- `casper-devnet` SSE parsing is not yet updated for new EVM variants. +- `eth_getBlockByNumber` currently returns enough typed fields for Foundry + polling, but it is not a complete Ethereum block projection. +- Sidecar derives receipt fields from stored `ExecutionResult::Evm` and block + metadata; efficient historical log queries are not implemented. +- EVM call support is latest/pending only in sidecar. +- EVM support is currently a prototype path and still uses local sidecar + patches for unreleased node types. + +[eip-55]: https://eips.ethereum.org/EIPS/eip-55 +[eip-155]: https://eips.ethereum.org/EIPS/eip-155 +[eip-2718]: https://eips.ethereum.org/EIPS/eip-2718 +[eip-2930]: https://eips.ethereum.org/EIPS/eip-2930 +[eip-1559]: https://eips.ethereum.org/EIPS/eip-1559 +[eip-4844]: https://eips.ethereum.org/EIPS/eip-4844 +[eip-7702]: https://eips.ethereum.org/EIPS/eip-7702 diff --git a/binary_port/src/command.rs b/binary_port/src/command.rs index a3099028d0..966c169753 100644 --- a/binary_port/src/command.rs +++ b/binary_port/src/command.rs @@ -5,7 +5,7 @@ use casper_types::{ Transaction, }; -use crate::get_request::GetRequest; +use crate::{get_request::GetRequest, EvmCallRequest}; #[cfg(test)] use casper_types::testing::TestRng; @@ -116,6 +116,11 @@ pub enum Command { /// Transaction to execute. transaction: Transaction, }, + /// Request to execute a read-only EVM call. + EvmCall { + /// EVM call request. + request: EvmCallRequest, + }, } impl Command { @@ -125,6 +130,7 @@ impl Command { Command::Get(_) => CommandTag::Get, Command::TryAcceptTransaction { .. } => CommandTag::TryAcceptTransaction, Command::TrySpeculativeExec { .. } => CommandTag::TrySpeculativeExec, + Command::EvmCall { .. } => CommandTag::EvmCall, } } @@ -138,6 +144,16 @@ impl Command { CommandTag::TrySpeculativeExec => Self::TrySpeculativeExec { transaction: Transaction::random(rng), }, + CommandTag::EvmCall => Self::EvmCall { + request: EvmCallRequest::new( + casper_types::evm::Address::new(rng.gen()), + rng.gen::() + .then(|| casper_types::evm::Address::new(rng.gen())), + casper_types::evm::Hash::new(rng.gen()), + casper_types::bytesrepr::Bytes::from(rng.random_vec(0..64)), + rng.gen(), + ), + }, } } } @@ -154,6 +170,7 @@ impl ToBytes for Command { Command::Get(inner) => inner.write_bytes(writer), Command::TryAcceptTransaction { transaction } => transaction.write_bytes(writer), Command::TrySpeculativeExec { transaction } => transaction.write_bytes(writer), + Command::EvmCall { request } => request.write_bytes(writer), } } @@ -162,6 +179,7 @@ impl ToBytes for Command { Command::Get(inner) => inner.serialized_length(), Command::TryAcceptTransaction { transaction } => transaction.serialized_length(), Command::TrySpeculativeExec { transaction } => transaction.serialized_length(), + Command::EvmCall { request } => request.serialized_length(), } } } @@ -183,6 +201,10 @@ impl TryFrom<(CommandTag, &[u8])> for Command { let (transaction, remainder) = FromBytes::from_bytes(bytes)?; (Command::TrySpeculativeExec { transaction }, remainder) } + CommandTag::EvmCall => { + let (request, remainder) = FromBytes::from_bytes(bytes)?; + (Command::EvmCall { request }, remainder) + } }; if !remainder.is_empty() { return Err(bytesrepr::Error::LeftOverBytes); @@ -201,16 +223,19 @@ pub enum CommandTag { TryAcceptTransaction = 1, /// Request to execute a transaction speculatively. TrySpeculativeExec = 2, + /// Request to execute a read-only EVM call. + EvmCall = 3, } impl CommandTag { /// Creates a random `CommandTag`. #[cfg(test)] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..3) { + match rng.gen_range(0..4) { 0 => CommandTag::Get, 1 => CommandTag::TryAcceptTransaction, 2 => CommandTag::TrySpeculativeExec, + 3 => CommandTag::EvmCall, _ => unreachable!(), } } @@ -224,6 +249,7 @@ impl TryFrom for CommandTag { 0 => Ok(CommandTag::Get), 1 => Ok(CommandTag::TryAcceptTransaction), 2 => Ok(CommandTag::TrySpeculativeExec), + 3 => Ok(CommandTag::EvmCall), _ => Err(InvalidCommandTag), } } diff --git a/binary_port/src/lib.rs b/binary_port/src/lib.rs index dacd9ab7cb..521927ce87 100644 --- a/binary_port/src/lib.rs +++ b/binary_port/src/lib.rs @@ -50,6 +50,7 @@ pub use speculative_execution_result::SpeculativeExecutionResult; pub use state_request::GlobalStateRequest; pub use type_wrappers::{ AccountInformation, AddressableEntityInformation, ConsensusStatus, ConsensusValidatorChanges, - ContractInformation, DictionaryQueryResult, GetTrieFullResult, LastProgress, NetworkName, - ReactorStateName, RewardResponse, TransactionWithExecutionInfo, Uptime, ValueWithProof, + ContractInformation, DictionaryQueryResult, EvmCallRequest, EvmCallResult, GetTrieFullResult, + LastProgress, NetworkName, ReactorStateName, RewardResponse, TransactionWithExecutionInfo, + Uptime, ValueWithProof, }; diff --git a/binary_port/src/response_type.rs b/binary_port/src/response_type.rs index b9cff4caad..6e800106fd 100644 --- a/binary_port/src/response_type.rs +++ b/binary_port/src/response_type.rs @@ -20,8 +20,8 @@ use crate::{ node_status::NodeStatus, speculative_execution_result::SpeculativeExecutionResult, type_wrappers::{ - ConsensusStatus, ConsensusValidatorChanges, GetTrieFullResult, LastProgress, NetworkName, - ReactorStateName, RewardResponse, + ConsensusStatus, ConsensusValidatorChanges, EvmCallResult, GetTrieFullResult, LastProgress, + NetworkName, ReactorStateName, RewardResponse, }, AccountInformation, AddressableEntityInformation, BalanceResponse, ContractInformation, DictionaryQueryResult, RecordId, TransactionWithExecutionInfo, Uptime, ValueWithProof, @@ -119,6 +119,8 @@ pub enum ResponseType { PackageWithProof, /// Addressable entity information. AddressableEntityInformation, + /// Result of a read-only EVM call. + EvmCallResult, } impl ResponseType { @@ -145,7 +147,7 @@ impl ResponseType { #[cfg(test)] pub(crate) fn random(rng: &mut TestRng) -> Self { - Self::try_from(rng.gen_range(0..44)).unwrap() + Self::try_from(rng.gen_range(0..45)).unwrap() } } @@ -228,6 +230,7 @@ impl TryFrom for ResponseType { x if x == ResponseType::AddressableEntityInformation as u8 => { Ok(ResponseType::AddressableEntityInformation) } + x if x == ResponseType::EvmCallResult as u8 => Ok(ResponseType::EvmCallResult), _ => Err(()), } } @@ -290,6 +293,7 @@ impl fmt::Display for ResponseType { ResponseType::AddressableEntityInformation => { write!(f, "AddressableEntityInformation") } + ResponseType::EvmCallResult => write!(f, "EvmCallResult"), } } } @@ -452,6 +456,10 @@ impl PayloadEntity for AddressableEntityInformation { const RESPONSE_TYPE: ResponseType = ResponseType::AddressableEntityInformation; } +impl PayloadEntity for EvmCallResult { + const RESPONSE_TYPE: ResponseType = ResponseType::EvmCallResult; +} + impl PayloadEntity for Box where T: PayloadEntity, diff --git a/binary_port/src/type_wrappers.rs b/binary_port/src/type_wrappers.rs index 89a14cbf78..9836366662 100644 --- a/binary_port/src/type_wrappers.rs +++ b/binary_port/src/type_wrappers.rs @@ -4,6 +4,7 @@ use std::collections::BTreeMap; use casper_types::{ bytesrepr::{self, Bytes, FromBytes, ToBytes}, contracts::ContractHash, + evm, global_state::TrieMerkleProof, system::auction::DelegationRate, Account, AddressableEntity, BlockHash, ByteCode, Contract, ContractWasm, EntityAddr, EraId, @@ -41,6 +42,174 @@ macro_rules! impl_bytesrepr_for_type_wrapper { }; } +/// Request for a read-only EVM call against the latest complete block. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct EvmCallRequest { + from: evm::Address, + to: Option, + value: evm::Hash, + input: Bytes, + gas_limit: u64, +} + +impl EvmCallRequest { + /// Constructs a new EVM call request. + pub fn new( + from: evm::Address, + to: Option, + value: evm::Hash, + input: Bytes, + gas_limit: u64, + ) -> Self { + Self { + from, + to, + value, + input, + gas_limit, + } + } + + /// Returns the caller address. + pub fn from(&self) -> evm::Address { + self.from + } + + /// Returns the optional target address. + pub fn to(&self) -> Option { + self.to + } + + /// Returns the call value. + pub fn value(&self) -> evm::Hash { + self.value + } + + /// Returns the call input bytes. + pub fn input(&self) -> &[u8] { + self.input.as_ref() + } + + /// Returns the gas limit. + pub fn gas_limit(&self) -> u64 { + self.gas_limit + } +} + +impl ToBytes for EvmCallRequest { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.from.serialized_length() + + self.to.serialized_length() + + self.value.serialized_length() + + self.input.serialized_length() + + self.gas_limit.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.from.write_bytes(writer)?; + self.to.write_bytes(writer)?; + self.value.write_bytes(writer)?; + self.input.write_bytes(writer)?; + self.gas_limit.write_bytes(writer) + } +} + +impl FromBytes for EvmCallRequest { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (from, remainder) = evm::Address::from_bytes(bytes)?; + let (to, remainder) = Option::::from_bytes(remainder)?; + let (value, remainder) = evm::Hash::from_bytes(remainder)?; + let (input, remainder) = Bytes::from_bytes(remainder)?; + let (gas_limit, remainder) = u64::from_bytes(remainder)?; + Ok(( + EvmCallRequest { + from, + to, + value, + input, + gas_limit, + }, + remainder, + )) + } +} + +/// Result of a read-only EVM call. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct EvmCallResult { + status: evm::ReceiptStatus, + output: Bytes, + gas_used: u64, +} + +impl EvmCallResult { + /// Constructs a new EVM call result. + pub fn new(status: evm::ReceiptStatus, output: Bytes, gas_used: u64) -> Self { + Self { + status, + output, + gas_used, + } + } + + /// Returns the call status. + pub fn status(&self) -> evm::ReceiptStatus { + self.status + } + + /// Returns the call output bytes. + pub fn output(&self) -> &[u8] { + self.output.as_ref() + } + + /// Returns gas used by the call. + pub fn gas_used(&self) -> u64 { + self.gas_used + } +} + +impl ToBytes for EvmCallResult { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.status.serialized_length() + + self.output.serialized_length() + + self.gas_used.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.status.write_bytes(writer)?; + self.output.write_bytes(writer)?; + self.gas_used.write_bytes(writer) + } +} + +impl FromBytes for EvmCallResult { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (status, remainder) = evm::ReceiptStatus::from_bytes(bytes)?; + let (output, remainder) = Bytes::from_bytes(remainder)?; + let (gas_used, remainder) = u64::from_bytes(remainder)?; + Ok(( + EvmCallResult { + status, + output, + gas_used, + }, + remainder, + )) + } +} + /// Type representing uptime. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub struct Uptime(u64); @@ -780,6 +949,28 @@ mod tests { )); } + #[test] + fn evm_call_request_roundtrip() { + let rng = &mut TestRng::new(); + bytesrepr::test_serialization_roundtrip(&EvmCallRequest::new( + evm::Address::new(rng.gen()), + rng.gen::().then(|| evm::Address::new(rng.gen())), + evm::Hash::new(rng.gen()), + Bytes::from(rng.random_vec(0..64)), + rng.gen(), + )); + } + + #[test] + fn evm_call_result_roundtrip() { + let rng = &mut TestRng::new(); + bytesrepr::test_serialization_roundtrip(&EvmCallResult::new( + evm::Receipt::random(rng).status, + Bytes::from(rng.random_vec(0..64)), + rng.gen(), + )); + } + #[test] fn dictionary_query_result_roundtrip() { let rng = &mut TestRng::new(); diff --git a/executor/evm/Cargo.toml b/executor/evm/Cargo.toml index f60f8e6d8a..033758d08d 100644 --- a/executor/evm/Cargo.toml +++ b/executor/evm/Cargo.toml @@ -11,10 +11,10 @@ license = "Apache-2.0" [dependencies] casper-storage = { version = "5.0.0", path = "../../storage" } casper-types = { version = "7.0.0", path = "../../types", features = ["std"] } -revm = { version = "27", features = ["dev"] } +revm = { version = "38", features = ["dev", "optional_fee_charge"] } thiserror = "2" [dev-dependencies] -alloy-consensus = { version = "=1.0.22", default-features = false, features = ["k256"] } -alloy-eips = { version = "=1.0.22", default-features = false, features = ["k256"] } -alloy-primitives = { version = "=1.2.0", default-features = false, features = ["rlp", "sha3-keccak"] } +alloy-consensus = { version = "=1.0.41", default-features = false, features = ["k256"] } +alloy-eips = { version = "=1.4.2", default-features = false, features = ["k256"] } +alloy-primitives = { version = "=1.5.7", default-features = false, features = ["rlp", "sha3-keccak"] } diff --git a/executor/evm/src/block_hash.rs b/executor/evm/src/block_hash.rs index 03b8d00e66..e565e190e0 100644 --- a/executor/evm/src/block_hash.rs +++ b/executor/evm/src/block_hash.rs @@ -6,7 +6,7 @@ use casper_storage::block_store::{ lmdb::IndexedLmdbBlockStore, types::BlockHeight, BlockStoreError, BlockStoreProvider, DataReader, }; -use casper_types::{evm, BlockHeader}; +use casper_types::{BlockHash, BlockHeader}; /// Result type returned by block hash providers. pub type BlockHashProviderResult = core::result::Result; @@ -26,7 +26,7 @@ pub enum BlockHashProviderError { /// the zero hash. Providers only need to answer canonical historical heights. pub trait BlockHashProvider { /// Returns the block hash for `block_height`, or `None` when unavailable. - fn block_hash(&self, block_height: u64) -> BlockHashProviderResult>; + fn block_hash(&self, block_height: u64) -> BlockHashProviderResult>; } /// Block hash provider that returns no historical hashes. @@ -34,7 +34,7 @@ pub trait BlockHashProvider { pub struct NoBlockHashProvider; impl BlockHashProvider for NoBlockHashProvider { - fn block_hash(&self, _block_height: u64) -> BlockHashProviderResult> { + fn block_hash(&self, _block_height: u64) -> BlockHashProviderResult> { Ok(None) } } @@ -53,16 +53,10 @@ impl IndexedLmdbBlockHashProvider { } impl BlockHashProvider for IndexedLmdbBlockHashProvider { - fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { + fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { let txn = self.block_store.checkout_ro()?; let maybe_header: Option = DataReader::::read(&txn, block_height)?; - Ok(maybe_header.map(|header| block_hash_to_evm_hash(header.block_hash()))) + Ok(maybe_header.map(|header| header.block_hash())) } } - -fn block_hash_to_evm_hash(block_hash: casper_types::BlockHash) -> evm::Hash { - let mut bytes = [0u8; evm::HASH_LENGTH]; - bytes.copy_from_slice(block_hash.as_ref()); - evm::Hash::new(bytes) -} diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs index a62ee0879d..9ba022e753 100644 --- a/executor/evm/src/db.rs +++ b/executor/evm/src/db.rs @@ -65,6 +65,7 @@ where balance, nonce: account.nonce(), code_hash: tx::to_revm_hash(account.code_hash()), + account_id: None, code: None, })) } @@ -102,7 +103,7 @@ where let slot = tx::from_revm_u256(index); let key = Key::EvmStorage(evm::StorageAddr::new(address, slot)); match self.tracking_copy.read(&key)? { - Some(StoredValue::EvmStorage(value)) => Ok(tx::to_revm_u256(value.value())), + Some(StoredValue::EvmStorage(value)) => Ok(tx::to_revm_hash_word(value.value())), Some(stored_value) => Err(DbError::TypeMismatch { key: Box::new(key), expected: "StoredValue::EvmStorage", @@ -120,7 +121,9 @@ where height: number, error, })?; - Ok(maybe_block_hash.map(tx::to_revm_hash).unwrap_or(B256::ZERO)) + Ok(maybe_block_hash + .map(tx::to_revm_block_hash) + .unwrap_or(B256::ZERO)) } } diff --git a/executor/evm/src/executor.rs b/executor/evm/src/executor.rs index 2212414da4..8f2c89a7c4 100644 --- a/executor/evm/src/executor.rs +++ b/executor/evm/src/executor.rs @@ -6,13 +6,14 @@ use casper_storage::{ }; use casper_types::{evm, Key, StoredValue}; use revm::{ - context_interface::result::EVMError, primitives::hardfork::SpecId, Context, ExecuteEvm, - MainBuilder, MainContext, + context_interface::result::{EVMError, ExecutionResult as RevmExecutionResult, ResultGas}, + primitives::{hardfork::SpecId, U256}, + Context, ExecuteEvm, MainBuilder, MainContext, }; use crate::{ db::CasperDb, state, tx, BlockHashProvider, DbError, Error, ExecuteKind, ExecuteRequest, - ExecutionOutcome, NoBlockHashProvider, Result, + ExecutionOutcome, FeeCharge, NoBlockHashProvider, Result, }; /// Executes EVM transactions and calls against a Casper tracking copy. @@ -87,6 +88,8 @@ impl EvmExecutor { ExecuteKind::Transaction(_) => false, ExecuteKind::Call(call) => call.validation.is_unchecked_simulation(), }; + let fee_charge_disabled = + matches!(request.fee_charge, FeeCharge::External) || skip_validation; let result_and_state = { let db = CasperDb::new(tracking_copy, block_hash_provider); @@ -101,6 +104,7 @@ impl EvmExecutor { cfg.disable_base_fee = skip_validation; cfg.disable_balance_check = skip_validation; cfg.disable_nonce_check = skip_validation; + cfg.disable_fee_charge = fee_charge_disabled; }) .build_mainnet(); @@ -108,11 +112,71 @@ impl EvmExecutor { }; let outcome = ExecutionOutcome::from_revm_result(&result_and_state.result); - state::apply(tracking_copy, result_and_state.state)?; + let mut state = result_and_state.state; + if fee_charge_disabled { + // revm skips the upfront fee debit but still applies the + // post-execution gas reimbursement and beneficiary reward. + let disabled_fee_transfers = + disabled_fee_transfers(&self.config, spec, &request, &result_and_state.result); + state::remove_disabled_fee_transfers(&mut state, disabled_fee_transfers)?; + } + state::apply(tracking_copy, state)?; Ok(outcome) } } +fn disabled_fee_transfers( + config: &evm::EvmConfig, + spec: SpecId, + request: &ExecuteRequest, + result: &RevmExecutionResult, +) -> state::DisabledFeeTransfers { + let gas = result_gas(result); + let base_fee = u128::from(request.block.base_fee.unwrap_or(config.base_fee)); + let (caller, gas_limit, effective_gas_price) = match &request.kind { + ExecuteKind::Transaction(transaction) => ( + tx::to_revm_address(transaction.from()), + transaction.gas_limit(), + transaction.effective_gas_price(base_fee as u64), + ), + ExecuteKind::Call(call) => ( + tx::to_revm_address(call.from), + call.gas_limit, + call.gas_price, + ), + }; + + let reimbursed_gas = gas_limit + .saturating_sub(gas.total_gas_spent()) + .saturating_add(gas.inner_refunded()); + let caller_reimbursement = U256::from(effective_gas_price) * U256::from(reimbursed_gas); + // Revm still computes Ethereum fee transfers internally before we remove + // them for Casper-owned fee accounting. For node-accepted EIP-1559 + // transactions the priority fee is zero by policy, but this stays generic + // for executor callers and pre-London specs. + let coinbase_gas_price = if spec.is_enabled_in(SpecId::LONDON) { + effective_gas_price.saturating_sub(base_fee) + } else { + effective_gas_price + }; + let beneficiary_reward = U256::from(coinbase_gas_price) * U256::from(gas.tx_gas_used()); + + state::DisabledFeeTransfers { + caller, + caller_reimbursement, + beneficiary: tx::to_revm_address(request.block.beneficiary), + beneficiary_reward, + } +} + +fn result_gas(result: &RevmExecutionResult) -> &ResultGas { + match result { + RevmExecutionResult::Success { gas, .. } + | RevmExecutionResult::Revert { gas, .. } + | RevmExecutionResult::Halt { gas, .. } => gas, + } +} + fn spec_id(spec: evm::EvmSpec) -> SpecId { match spec { evm::EvmSpec::Frontier => SpecId::FRONTIER, diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs index f030ee2367..fb44ce45be 100644 --- a/executor/evm/src/lib.rs +++ b/executor/evm/src/lib.rs @@ -19,14 +19,16 @@ pub use block_hash::{ pub use error::{DbError, Error, Result}; pub use executor::EvmExecutor; pub use outcome::{ExecutionOutcome, ExecutionStatus}; -pub use request::{BlockContext, CallRequest, CallValidation, ExecuteKind, ExecuteRequest}; +pub use request::{ + BlockContext, CallRequest, CallValidation, ExecuteKind, ExecuteRequest, FeeCharge, +}; use casper_types::evm; pub use casper_types::evm::Log; /// Keccak-256 hash of empty EVM bytecode. -pub const EMPTY_CODE_HASH: evm::Hash = evm::Hash::new([ - 0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0, - 0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70, -]); +pub const EMPTY_CODE_HASH: evm::Hash = evm::EMPTY_CODE_HASH; + +/// Number of recent block hashes available to EVM `BLOCKHASH`. +pub const BLOCK_HASH_HISTORY: u64 = revm::primitives::BLOCK_HASH_HISTORY; diff --git a/executor/evm/src/outcome.rs b/executor/evm/src/outcome.rs index 778246eba5..5c10f30e89 100644 --- a/executor/evm/src/outcome.rs +++ b/executor/evm/src/outcome.rs @@ -26,10 +26,7 @@ impl ExecutionOutcome { pub(crate) fn from_revm_result(result: &ExecutionResult) -> Self { match result { ExecutionResult::Success { - gas_used, - logs, - output, - .. + gas, logs, output, .. } => { let (output_bytes, created_contract_address) = match output { Output::Call(bytes) => (bytes.to_vec(), None), @@ -39,22 +36,22 @@ impl ExecutionOutcome { }; Self { status: ExecutionStatus::Success, - gas_used: *gas_used, + gas_used: gas.tx_gas_used(), output: output_bytes, logs: logs.iter().map(from_revm_log).collect(), created_contract_address, } } - ExecutionResult::Revert { gas_used, output } => Self { + ExecutionResult::Revert { gas, output, .. } => Self { status: ExecutionStatus::Revert, - gas_used: *gas_used, + gas_used: gas.tx_gas_used(), output: output.to_vec(), logs: Vec::new(), created_contract_address: None, }, - ExecutionResult::Halt { gas_used, reason } => Self { + ExecutionResult::Halt { gas, reason, .. } => Self { status: ExecutionStatus::Halt(from_revm_halt_reason(reason)), - gas_used: *gas_used, + gas_used: gas.tx_gas_used(), output: Vec::new(), logs: Vec::new(), created_contract_address: None, @@ -103,7 +100,9 @@ fn from_revm_halt_reason(reason: &RevmHaltReason) -> evm::HaltReason { RevmHaltReason::StackOverflow => evm::HaltReason::StackOverflow, RevmHaltReason::OutOfOffset => evm::HaltReason::OutOfOffset, RevmHaltReason::CreateCollision => evm::HaltReason::CreateCollision, - RevmHaltReason::PrecompileError => evm::HaltReason::PrecompileError, + RevmHaltReason::PrecompileError | RevmHaltReason::PrecompileErrorWithContext(_) => { + evm::HaltReason::PrecompileError + } RevmHaltReason::NonceOverflow => evm::HaltReason::NonceOverflow, RevmHaltReason::CreateContractSizeLimit => evm::HaltReason::CreateContractSizeLimit, RevmHaltReason::CreateContractStartingWithEF => { diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs index a17faeef51..a114fe5dfd 100644 --- a/executor/evm/src/request.rs +++ b/executor/evm/src/request.rs @@ -11,6 +11,8 @@ pub struct ExecuteRequest { pub block: BlockContext, /// EVM work item to execute. pub kind: ExecuteKind, + /// Component responsible for charging EVM gas fees. + pub fee_charge: FeeCharge, } /// EVM work item to execute. @@ -62,6 +64,15 @@ impl CallValidation { } } +/// Component responsible for mutating account balances for gas fees. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FeeCharge { + /// Let the EVM apply Ethereum gas fee debits and refunds. + Evm, + /// Skip EVM gas fee balance mutation so the caller can charge fees externally. + External, +} + /// Per-execution block context. #[derive(Clone, Debug, Eq, PartialEq)] pub struct BlockContext { diff --git a/executor/evm/src/state.rs b/executor/evm/src/state.rs index 1c9197fd5d..231ddd5892 100644 --- a/executor/evm/src/state.rs +++ b/executor/evm/src/state.rs @@ -22,6 +22,38 @@ where Ok(()) } +pub(crate) struct DisabledFeeTransfers { + pub caller: Address, + pub caller_reimbursement: U256, + pub beneficiary: Address, + pub beneficiary_reward: U256, +} + +pub(crate) fn remove_disabled_fee_transfers( + state: &mut EvmState, + transfers: DisabledFeeTransfers, +) -> Result<(), Error> { + subtract_balance(state, transfers.caller, transfers.caller_reimbursement)?; + subtract_balance(state, transfers.beneficiary, transfers.beneficiary_reward) +} + +fn subtract_balance(state: &mut EvmState, address: Address, amount: U256) -> Result<(), Error> { + if amount.is_zero() { + return Ok(()); + } + let account = state.get_mut(&address).ok_or_else(|| { + Error::State(format!( + "missing EVM account {address:?} while removing disabled fee transfer" + )) + })?; + account.info.balance = account.info.balance.checked_sub(amount).ok_or_else(|| { + Error::State(format!( + "EVM account {address:?} balance underflow while removing disabled fee transfer" + )) + })?; + Ok(()) +} + fn apply_account( tracking_copy: &mut TrackingCopy, address: Address, @@ -30,6 +62,7 @@ fn apply_account( where R: StateReader, { + // Check how to deal with Key::Balance after selfdestruct let address = tx::from_revm_address(address); let account_key = Key::EvmAccount(address); diff --git a/executor/evm/src/tx.rs b/executor/evm/src/tx.rs index ec3556763f..59b267717a 100644 --- a/executor/evm/src/tx.rs +++ b/executor/evm/src/tx.rs @@ -1,6 +1,6 @@ //! Translation from Casper-owned EVM requests into revm transaction environments. -use casper_types::evm; +use casper_types::{evm, BlockHash, U256 as CasperU256}; use revm::{ context::TxEnv, primitives::{Address, Bytes, TxKind, B256, U256}, @@ -25,9 +25,17 @@ pub(crate) fn build_tx_env(config: &evm::EvmConfig, kind: &ExecuteKind) -> Resul .gas_price() .unwrap_or_else(|| transaction.max_fee_per_gas()), ), - evm::TransactionKind::Eip1559 => builder - .max_fee_per_gas(transaction.max_fee_per_gas()) - .gas_priority_fee(transaction.max_priority_fee_per_gas()), + evm::TransactionKind::Eip1559 => { + // Preserve the EIP-1559 fields when translating into + // revm. Node config compliance currently only admits + // zero-priority-fee EIP-1559 transactions because Casper + // does not prioritize transactions based on transaction + // gas parameters, but the executor remains a faithful + // typed-transaction adapter. + builder + .max_fee_per_gas(transaction.max_fee_per_gas()) + .gas_priority_fee(transaction.max_priority_fee_per_gas()) + } }; builder = match transaction.to() { @@ -47,7 +55,7 @@ pub(crate) fn build_tx_env(config: &evm::EvmConfig, kind: &ExecuteKind) -> Resul Some(address) => TxKind::Call(to_revm_address(address)), None => TxKind::Create, }) - .value(to_revm_u256(call.value)) + .value(to_revm_hash_word(call.value)) .data(Bytes::from(call.input.clone())) .nonce(call.nonce) .chain_id(Some(config.chain_id)) @@ -73,7 +81,19 @@ pub(crate) fn from_revm_hash(hash: B256) -> evm::Hash { evm::Hash::new(hash.0) } -pub(crate) fn to_revm_u256(value: evm::Hash) -> U256 { +pub(crate) fn to_revm_block_hash(block_hash: BlockHash) -> B256 { + let mut bytes = [0u8; evm::HASH_LENGTH]; + bytes.copy_from_slice(block_hash.as_ref()); + B256::from(bytes) +} + +pub(crate) fn to_revm_u256(value: CasperU256) -> U256 { + let mut bytes = [0u8; 32]; + value.to_big_endian(&mut bytes); + U256::from_be_slice(&bytes) +} + +pub(crate) fn to_revm_hash_word(value: evm::Hash) -> U256 { U256::from_be_slice(value.as_bytes()) } diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index fc47bd4274..c447a71e23 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -5,7 +5,7 @@ use alloy_eips::eip2718::Encodable2718; use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, U256}; use casper_executor_evm::{ BlockContext, BlockHashProvider, BlockHashProviderResult, CallRequest, CallValidation, Error, - EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, EMPTY_CODE_HASH, + EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, FeeCharge, EMPTY_CODE_HASH, }; use casper_storage::{ data_access_layer::{GenesisRequest, GenesisResult}, @@ -17,9 +17,9 @@ use casper_storage::{ TrackingCopy, }; use casper_types::{ - evm, CLValue, ChainspecRegistry, Digest, GenesisAccount, GenesisConfig, HoldBalanceHandling, - Key, Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, StoredValue, SystemConfig, - Timestamp, WasmConfig, U512, + evm, BlockHash, CLValue, ChainspecRegistry, Digest, GenesisAccount, GenesisConfig, + HoldBalanceHandling, Key, Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, + StoredValue, SystemConfig, Timestamp, WasmConfig, U512, }; use revm::bytecode::opcode; @@ -99,15 +99,15 @@ fn block() -> BlockContext { struct HeightBlockHashProvider; impl BlockHashProvider for HeightBlockHashProvider { - fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { + fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { Ok(Some(block_hash_for_height(block_height))) } } -fn block_hash_for_height(block_height: u64) -> evm::Hash { - let mut bytes = [0u8; evm::HASH_LENGTH]; +fn block_hash_for_height(block_height: u64) -> BlockHash { + let mut bytes = [0u8; BlockHash::LENGTH]; bytes[24..].copy_from_slice(&block_height.to_be_bytes()); - evm::Hash::new(bytes) + BlockHash::new(Digest::from_raw(bytes)) } fn init_code_returning(runtime: Vec) -> Vec { @@ -172,6 +172,7 @@ fn call_request( nonce: 0, validation: CallValidation::UncheckedSimulation, }), + fee_charge: FeeCharge::Evm, } } @@ -193,6 +194,7 @@ fn checked_call_request( nonce: 0, validation: CallValidation::Checked, }), + fee_charge: FeeCharge::Evm, } } @@ -405,10 +407,7 @@ fn blockhash_uses_supplied_provider() { .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); - assert_eq!( - outcome.output.as_slice(), - block_hash_for_height(1).as_bytes() - ); + assert_eq!(outcome.output.as_slice(), block_hash_for_height(1).as_ref()); } #[test] @@ -706,6 +705,7 @@ fn signed_transactions_require_configured_chain_id() { let request = ExecuteRequest { block: block(), kind: ExecuteKind::Transaction(missing_chain_id), + fee_charge: FeeCharge::Evm, }; assert!(matches!( executor.execute(&mut tracking_copy, request), @@ -723,6 +723,7 @@ fn signed_transactions_require_configured_chain_id() { let request = ExecuteRequest { block: block(), kind: ExecuteKind::Transaction(transaction), + fee_charge: FeeCharge::Evm, }; assert!(matches!( wrong_chain_executor.execute(&mut tracking_copy, request), diff --git a/node/Cargo.toml b/node/Cargo.toml index 1bc06b2e86..9733087807 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -97,6 +97,7 @@ wheelbuf = "0.2.0" casper-executor-wasm = { version = "0.1.3", path = "../executor/wasm" } casper-executor-wasm-interface = { version = "0.1.3", path = "../executor/wasm_interface" } fs_extra = "1.3.0" +casper-executor-evm = { version = "0.1.0", path = "../executor/evm" } [dev-dependencies] casper-binary-port = { version = "1.1.1", path = "../binary_port", features = ["testing"] } @@ -111,6 +112,11 @@ proptest-derive = "0.5.1" rand_core = "0.6.2" reqwest = { version = "0.11.27", features = ["stream"] } tokio = { version = "1", features = ["test-util"] } +alloy-consensus = { version = "=1.0.41", default-features = false, features = ["k256"] } +alloy-eips = { version = "=1.4.2", default-features = false, features = ["k256"] } +alloy-primitives = { version = "=1.5.7", default-features = false, features = ["rlp", "sha3-keccak", "k256"] } +k256 = { version = "0.13.4", default-features = false, features = ["ecdsa"] } +revm = { version = "38", features = ["dev"] } [features] failpoints = [] diff --git a/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index c576f89b41..8e742deec8 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -14,7 +14,7 @@ use casper_binary_port::{ AccountInformation, AddressableEntityInformation, BalanceResponse, BinaryMessage, BinaryMessageCodec, BinaryResponse, BinaryResponseAndRequest, Command, CommandHeader, CommandTag, ContractInformation, DictionaryItemIdentifier, DictionaryQueryResult, - EntityIdentifier, EraIdentifier, ErrorCode, GetRequest, GetTrieFullResult, + EntityIdentifier, EraIdentifier, ErrorCode, EvmCallRequest, GetRequest, GetTrieFullResult, GlobalStateEntityQualifier, GlobalStateQueryResult, GlobalStateRequest, InformationRequest, InformationRequestTag, KeyPrefix, NodeStatus, PackageIdentifier, PurseIdentifier, ReactorStateName, RecordId, ResponseType, RewardResponse, TransactionWithExecutionInfo, @@ -66,7 +66,7 @@ use futures::{future::BoxFuture, FutureExt}; use self::error::Error; use crate::{ - contract_runtime::SpeculativeExecutionResult, + contract_runtime::{load_recent_evm_block_hashes, SpeculativeExecutionResult}, effect::{ requests::{ AcceptTransactionRequest, BlockSynchronizerRequest, ChainspecRawBytesRequest, @@ -182,6 +182,7 @@ impl BinaryRequestTerminationDelayValues { Command::Get(GetRequest::Trie { .. }) => self.get_trie, Command::TryAcceptTransaction { .. } => self.accept_transaction, Command::TrySpeculativeExec { .. } => self.speculative_exec, + Command::EvmCall { .. } => self.speculative_exec, } } } @@ -222,6 +223,14 @@ where } try_speculative_execution(effect_builder, transaction).await } + Command::EvmCall { request } => { + metrics.binary_port_try_speculative_exec_count.inc(); + if !config.allow_request_speculative_exec { + debug!("received an EVM call request while speculative execution is disabled"); + return BinaryResponse::new_error(ErrorCode::FunctionDisabled); + } + try_evm_call(effect_builder, request).await + } Command::Get(get_req) => { handle_get_request(get_req, effect_builder, config, metrics, protocol_version).await } @@ -1383,6 +1392,33 @@ where } } +async fn try_evm_call( + effect_builder: EffectBuilder, + request: EvmCallRequest, +) -> BinaryResponse +where + REv: From + From + From, +{ + let tip = match effect_builder + .get_highest_complete_block_header_from_storage() + .await + { + Some(tip) => tip, + None => return BinaryResponse::new_error(ErrorCode::NoCompleteBlocks), + }; + let block_hashes = load_recent_evm_block_hashes(effect_builder, tip.height()).await; + match effect_builder + .evm_call(Box::new(tip), block_hashes, Box::new(request)) + .await + { + Ok(result) => BinaryResponse::from_value(result), + Err(error) => { + debug!(%error, "EVM call failed"); + BinaryResponse::new_error(ErrorCode::InternalError) + } + } +} + async fn handle_client_loop( stream: TcpStream, effect_builder: EffectBuilder, diff --git a/node/src/components/binary_port/event.rs b/node/src/components/binary_port/event.rs index 429944dd4d..4e7d175964 100644 --- a/node/src/components/binary_port/event.rs +++ b/node/src/components/binary_port/event.rs @@ -47,6 +47,7 @@ impl Display for Event { Command::TrySpeculativeExec { transaction, .. } => { write!(f, "try speculative exec ({})", transaction.hash()) } + Command::EvmCall { request } => write!(f, "evm call ({:?})", request.to()), }, } } diff --git a/node/src/components/contract_runtime.rs b/node/src/components/contract_runtime.rs index 3c0771780b..9de7b36c35 100644 --- a/node/src/components/contract_runtime.rs +++ b/node/src/components/contract_runtime.rs @@ -76,11 +76,12 @@ use metrics::Metrics; #[cfg(test)] pub(crate) use operations::compute_execution_results_checksum; pub use operations::execute_finalized_block; -use operations::speculatively_execute; +use operations::{evm_call, speculatively_execute}; pub(crate) use types::{ BlockAndExecutionArtifacts, ExecutionArtifact, ExecutionPreState, SpeculativeExecutionResult, StepOutcome, }; +pub(crate) use utils::load_recent_evm_block_hashes; use utils::{exec_and_check_next, run_intensive_task}; const COMPONENT_NAME: &str = "contract_runtime"; @@ -742,6 +743,29 @@ impl ContractRuntime { } .ignore() } + ContractRuntimeRequest::EvmCall { + block_header, + block_hashes, + request, + responder, + } => { + let chainspec = Arc::clone(&self.chainspec); + let data_access_layer = Arc::clone(&self.data_access_layer); + async move { + let result = run_intensive_task(move || { + evm_call( + data_access_layer.as_ref(), + chainspec.as_ref(), + *block_header, + block_hashes, + *request, + ) + }) + .await; + responder.respond(result).await + } + .ignore() + } ContractRuntimeRequest::GetEraGasPrice { era_id, responder } => responder .respond(self.current_gas_price.maybe_gas_price_for_era_id(era_id)) .ignore(), diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 9f6f3237bf..6e6f2201de 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -6,9 +6,16 @@ use std::{collections::BTreeMap, convert::TryInto, sync::Arc, time::Instant}; use tracing::{debug, error, info, trace, warn}; use wasm_v2_request::{WasmV2Request, WasmV2Result}; +use casper_binary_port::{EvmCallRequest, EvmCallResult}; use casper_execution_engine::engine_state::{ BlockInfo, ExecutionEngineV1, WasmV1Request, WasmV1Result, }; +use casper_executor_evm::{ + BlockContext as EvmBlockContext, BlockHashProvider as EvmBlockHashProvider, + BlockHashProviderResult as EvmBlockHashProviderResult, EvmExecutor, + ExecuteKind as EvmExecuteKind, ExecuteRequest as EvmExecuteRequest, + ExecutionStatus as EvmExecutionStatus, FeeCharge as EvmFeeCharge, +}; use casper_storage::{ block_store::types::ApprovalsHashes, data_access_layer::{ @@ -31,6 +38,10 @@ use casper_storage::{ }; use casper_types::{ bytesrepr::{self, ToBytes, U32_SERIALIZED_LENGTH}, + evm::{ + Address as EvmAddress, HaltReason as EvmHaltReason, Receipt as EvmReceipt, + ReceiptStatus as EvmReceiptStatus, + }, execution::{Effects, ExecutionResult, TransformKindV2, TransformV2}, system::handle_payment::ARG_AMOUNT, BlockHash, BlockHeader, BlockTime, BlockV2, CLValue, Chainspec, ChecksumRegistry, Digest, @@ -51,6 +62,27 @@ use crate::{ types::{self, Chunkable, ExecutableBlock, InternalEraReport, MetaTransaction}, }; +#[derive(Default)] +struct StaticEvmBlockHashProvider { + block_hashes: BTreeMap, +} + +impl EvmBlockHashProvider for StaticEvmBlockHashProvider { + fn block_hash(&self, block_height: u64) -> EvmBlockHashProviderResult> { + Ok(self.block_hashes.get(&block_height).copied()) + } +} + +fn evm_precondition_receipt(effective_gas_price: u128) -> EvmReceipt { + EvmReceipt { + status: EvmReceiptStatus::Halt(EvmHaltReason::Unknown), + gas_used: 0, + effective_gas_price, + contract_address: None, + logs: Vec::new(), + } +} + /// Executes a finalized block. #[allow(clippy::too_many_arguments)] pub fn execute_finalized_block( @@ -60,6 +92,7 @@ pub fn execute_finalized_block( chainspec: &Chainspec, metrics: Option>, execution_pre_state: ExecutionPreState, + evm_block_hash_provider: &dyn EvmBlockHashProvider, executable_block: ExecutableBlock, key_block_height_for_activation_point: u64, current_gas_price: u8, @@ -209,6 +242,8 @@ pub fn execute_finalized_block( let transaction_config = &chainspec.transaction_config; for stored_transaction in executable_block.transactions { + let evm_transaction = stored_transaction.as_evm(); + let is_evm = evm_transaction.is_some(); let transaction = MetaTransaction::from_transaction( &stored_transaction, chainspec.core_config.pricing_handling, @@ -216,11 +251,9 @@ pub fn execute_finalized_block( ) .map_err(|err| BlockExecutionError::TransactionConversion(err.to_string()))?; - let initiator_addr = transaction.initiator_addr(); - let transaction_hash = transaction.hash(); - let transaction_args = transaction.session_args().clone(); - let entry_point = transaction.entry_point(); - let authorization_keys = transaction.signers(); + let initiator_addr = stored_transaction.initiator_addr(); + let transaction_hash = stored_transaction.hash(); + let authorization_keys = stored_transaction.authorization_keys(); /* we solve for halting state using a `gas limit` which is the maximum amount of @@ -244,42 +277,59 @@ pub fn execute_finalized_block( we check these top level concerns early so that we can skip if there is an error */ + let lane_id = transaction.transaction_lane(); + let mut artifact_builder = { // NOTE: this is the allowed computation limit (gas limit) - let gas_limit = match transaction.gas_limit(chainspec) { - Ok(gas) => gas, - Err(ite) => { - debug!(%transaction_hash, %ite, "invalid transaction (gas limit)"); - artifacts.push( - ExecutionArtifactBuilder::pre_condition_failure( - &stored_transaction, - current_gas_price, - ite, - ) - .build(), - ); - continue; + let gas_limit = if let Some(evm_transaction) = evm_transaction { + Gas::new(evm_transaction.gas_limit()) + } else { + match transaction.gas_limit(chainspec) { + Ok(gas) => gas, + Err(ite) => { + debug!(%transaction_hash, %ite, "invalid transaction (gas limit)"); + artifacts.push( + ExecutionArtifactBuilder::pre_condition_failure( + &stored_transaction, + current_gas_price, + ite, + ) + .build(), + ); + continue; + } } }; - // NOTE: this is the actual adjusted cost that we charge for (gas limit * gas price) - let cost = match stored_transaction.gas_cost( - chainspec, - transaction.transaction_lane(), - current_gas_price, - ) { - Ok(motes) => motes.value(), - Err(ite) => { - debug!(%transaction_hash, "invalid transaction (motes conversion)"); - artifacts.push( - ExecutionArtifactBuilder::pre_condition_failure( - &stored_transaction, - current_gas_price, - ite, + // NOTE: this is the actual adjusted cost that we charge for (gas limit * gas price). + // For accepted EIP-1559 transactions, config compliance has already required + // `max_priority_fee_per_gas == 0`, so the effective EVM gas price is the + // configured base fee capped by `max_fee_per_gas`; Casper does not charge an + // Ethereum-style priority premium while transaction priority is not based on + // gas parameters. + let cost = if let Some(evm_transaction) = evm_transaction { + evm_transaction + .max_fee_amount(&chainspec.evm_config) + .ok_or_else(|| { + BlockExecutionError::PaymentError( + "EVM fee amount overflowed U512".to_string(), ) - .build(), - ); - continue; + })? + } else { + match stored_transaction.gas_cost(chainspec, lane_id, current_gas_price) { + Ok(motes) => motes.value(), + Err(ite) => { + debug!(%transaction_hash, "invalid transaction (motes conversion)"); + artifacts.push( + ExecutionArtifactBuilder::pre_condition_failure( + &stored_transaction, + current_gas_price, + ite, + ) + .build(), + ); + continue; + } } }; @@ -345,6 +395,15 @@ pub fn execute_finalized_block( } trace!(%transaction_hash, "insufficient initial balance"); debug!(%transaction_hash, ?initial_balance_result, %baseline_motes_amount, "insufficient initial balance"); + if let Some(evm_transaction) = evm_transaction { + artifact_builder.with_zero_cost().with_evm_receipt( + evm_precondition_receipt( + evm_transaction.effective_gas_price(chainspec.evm_config.base_fee), + ), + U512::zero(), + Effects::new(), + ); + } artifacts.push(artifact_builder.build()); // only reads have happened so far, and we can't charge due // to insufficient balance, so move on with no effects committed @@ -353,7 +412,16 @@ pub fn execute_finalized_block( } let mut balance_identifier = { - if is_standard_payment { + if let Some(evm_transaction) = evm_transaction { + // EVM transactions intentionally do not participate in Casper custom payment + // or refund-purse setup. Ethereum payloads carry a gas limit and gas price fields, + // but this chain still owns the fee/refund policy through the same chainspec + // settings used by Deploy and V1/V2 transactions. The EVM sender's main purse is + // therefore the payer for the processing hold, refund calculation, and final fee + // handling, while revm runs with gas fee charging disabled and only mutates EVM + // nonce, code, storage, logs, creates, and value transfers. + BalanceIdentifier::Evm(evm_transaction.from()) + } else if is_standard_payment { let contract_might_pay = addressable_entity_enabled && transaction.is_contract_by_hash_invocation(); @@ -490,8 +558,6 @@ pub fn execute_finalized_block( )); artifact_builder.with_available(post_payment_balance_result.available_balance().copied()); - let lane_id = transaction.transaction_lane(); - let allow_execution = { let is_not_penalized = !balance_identifier.is_penalty(); // in the case of custom payment, we do all payment processing up front after checking @@ -500,8 +566,19 @@ pub fn execute_finalized_block( // the sad path is handled by is_penalty and the balance in the payment purse is // the penalty payment or the full amount but is 'sufficient' either way let actual_cost = artifact_builder.actual_cost(); // use actual cost here + let required_balance = if let Some(evm_transaction) = evm_transaction { + evm_transaction + .required_balance(actual_cost) + .ok_or_else(|| { + BlockExecutionError::PaymentError( + "EVM value plus fee amount overflowed U512".to_string(), + ) + })? + } else { + actual_cost + }; let is_sufficient_balance = - is_custom_payment || post_payment_balance_result.is_sufficient(actual_cost); + is_custom_payment || post_payment_balance_result.is_sufficient(required_balance); let is_allowed_by_chainspec = chainspec.is_supported(lane_id); let allow = is_not_penalized && is_sufficient_balance && is_allowed_by_chainspec; if !allow { @@ -548,6 +625,7 @@ pub fn execute_finalized_block( trace!(%transaction_hash, ?lane_id, "eligible for execution"); match lane_id { lane_id if lane_id == MINT_LANE_ID => { + let transaction_args = transaction.session_args(); let runtime_args = transaction_args .as_named() .ok_or(BlockExecutionError::InvalidTransactionArgs)?; @@ -593,9 +671,11 @@ pub fn execute_finalized_block( } } lane_id if lane_id == AUCTION_LANE_ID => { + let transaction_args = transaction.session_args(); let runtime_args = transaction_args .as_named() .ok_or(BlockExecutionError::InvalidTransactionArgs)?; + let entry_point = transaction.entry_point(); match AuctionMethod::from_parts(entry_point, runtime_args, chainspec) { Ok(auction_method) => { let bidding_result = scratch_state.bidding(BiddingRequest::new( @@ -626,6 +706,55 @@ pub fn execute_finalized_block( } }; } + _ if is_evm => { + let evm_transaction = evm_transaction.expect("EVM transaction should exist"); + let block_context = EvmBlockContext { + number: block_height, + timestamp: block_time.value() / 1000, + beneficiary: EvmAddress::from_public_key(&proposer) + .unwrap_or(EvmAddress::ZERO), + gas_limit: Some(chainspec.evm_config.block_gas_limit), + base_fee: Some(chainspec.evm_config.base_fee), + }; + let request = EvmExecuteRequest { + block: block_context, + kind: EvmExecuteKind::Transaction(evm_transaction.clone()), + fee_charge: EvmFeeCharge::External, + }; + let mut tracking_copy = scratch_state + .tracking_copy(state_root_hash)? + .ok_or(BlockExecutionError::RootNotFound(state_root_hash))?; + let outcome = EvmExecutor::new(chainspec.evm_config) + .execute_with_block_hash_provider( + &mut tracking_copy, + request, + evm_block_hash_provider, + ) + .map_err(|error| { + BlockExecutionError::TransactionConversion(error.to_string()) + })?; + let execution_effects = tracking_copy.effects(); + state_root_hash = + scratch_state.commit_effects(state_root_hash, execution_effects.clone())?; + let effective_gas_price = + evm_transaction.effective_gas_price(chainspec.evm_config.base_fee); + let consumed = if matches!(outcome.status, EvmExecutionStatus::Success) { + evm_transaction + .fee_amount(outcome.gas_used, &chainspec.evm_config) + .ok_or_else(|| { + BlockExecutionError::PaymentError( + "EVM fee amount overflowed U512".to_string(), + ) + })? + } else { + artifact_builder.cost_to_use() + }; + artifact_builder.with_evm_receipt( + outcome.to_receipt(effective_gas_price), + consumed, + execution_effects, + ); + } _ if is_v1_wasm => { let wasm_v1_start = Instant::now(); let session_input_data = transaction.to_session_input_data(); @@ -719,6 +848,19 @@ pub fn execute_finalized_block( } } + if is_evm && !allow_execution { + let effective_gas_price = evm_transaction + .expect("EVM transaction should exist") + .effective_gas_price(chainspec.evm_config.base_fee); + artifact_builder.with_zero_cost().with_evm_receipt( + evm_precondition_receipt(effective_gas_price), + U512::zero(), + Effects::new(), + ); + artifacts.push(artifact_builder.build()); + continue; + } + // clear all holds on the balance_identifier purse before payment processing { let hold_request = BalanceHoldRequest::new_clear( @@ -764,15 +906,22 @@ pub fn execute_finalized_block( None } } - RefundHandling::Burn { refund_ratio } => Some(HandleRefundMode::Burn { - limit: artifact_builder.limit(), - gas_price: current_gas_price, - cost: artifact_builder.cost_to_use(), - consumed, - source: Box::new(balance_identifier.clone()), - ratio: refund_ratio, - available, - }), + RefundHandling::Burn { refund_ratio } => { + let (limit, gas_price) = if is_evm { + (artifact_builder.cost_to_use(), 1) + } else { + (artifact_builder.limit(), current_gas_price) + }; + Some(HandleRefundMode::Burn { + limit, + gas_price, + cost: artifact_builder.cost_to_use(), + consumed, + source: Box::new(balance_identifier.clone()), + ratio: refund_ratio, + available, + }) + } RefundHandling::Refund { refund_ratio } => { let source = Box::new(balance_identifier.clone()); if is_custom_payment { @@ -809,9 +958,14 @@ pub fn execute_finalized_block( // the churn of taking the token up front via transfer (which writes // multiple permanent records) and then transfer some of it back (which // writes more permanent records). + let (limit, gas_price) = if is_evm { + (artifact_builder.cost_to_use(), 1) + } else { + (artifact_builder.limit(), current_gas_price) + }; Some(HandleRefundMode::CalculateAmount { - limit: artifact_builder.limit(), - gas_price: current_gas_price, + limit, + gas_price, consumed, cost: artifact_builder.cost_to_use(), ratio: refund_ratio, @@ -1457,6 +1611,63 @@ where } } +/// Executes a read-only EVM call against a checked-out block state. +pub(super) fn evm_call( + state_provider: &S, + chainspec: &Chainspec, + block_header: BlockHeader, + block_hashes: BTreeMap, + request: EvmCallRequest, +) -> Result +where + S: StateProvider, +{ + if !chainspec.evm_config.enabled { + return Err("EVM execution is disabled".to_string()); + } + + let state_root_hash = block_header.state_root_hash(); + let mut tracking_copy = state_provider + .tracking_copy(*state_root_hash) + .map_err(|error| format!("failed to check out EVM call state: {error}"))? + .ok_or_else(|| format!("state root {state_root_hash} not found"))?; + let block_time = block_header + .timestamp() + .saturating_add(chainspec.core_config.minimum_block_time); + let block_context = EvmBlockContext { + number: block_header.height(), + timestamp: block_time.millis() / 1000, + beneficiary: EvmAddress::ZERO, + gas_limit: Some(chainspec.evm_config.block_gas_limit), + base_fee: Some(chainspec.evm_config.base_fee), + }; + let call = casper_executor_evm::CallRequest { + from: request.from(), + to: request.to(), + value: request.value(), + input: request.input().to_vec(), + gas_limit: request.gas_limit(), + gas_price: u128::from(chainspec.evm_config.base_fee), + nonce: 0, + validation: casper_executor_evm::CallValidation::UncheckedSimulation, + }; + let execute_request = EvmExecuteRequest { + block: block_context, + kind: EvmExecuteKind::Call(call), + fee_charge: EvmFeeCharge::External, + }; + let block_hash_provider = StaticEvmBlockHashProvider { block_hashes }; + let outcome = EvmExecutor::new(chainspec.evm_config) + .execute_with_block_hash_provider(&mut tracking_copy, execute_request, &block_hash_provider) + .map_err(|error| error.to_string())?; + let receipt = outcome.to_receipt(0); + Ok(EvmCallResult::new( + receipt.status, + outcome.output.into(), + outcome.gas_used, + )) +} + fn invoked_contract_will_pay( state_provider: &ScratchGlobalState, state_root_hash: Digest, diff --git a/node/src/components/contract_runtime/types.rs b/node/src/components/contract_runtime/types.rs index 78b11725ce..5505e805c0 100644 --- a/node/src/components/contract_runtime/types.rs +++ b/node/src/components/contract_runtime/types.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, sync::Arc}; use crate::{contract_runtime::StateResultError, types::TransactionHeader}; -use casper_types::{InitiatorAddr, Transfer}; +use casper_types::{evm, InitiatorAddr, Transfer}; use datasize::DataSize; use serde::Serialize; @@ -17,7 +17,7 @@ use casper_storage::{ }; use casper_types::{ contract_messages::Messages, - execution::{Effects, ExecutionResult, ExecutionResultV2}, + execution::{Effects, EvmExecutionResult, ExecutionResult, ExecutionResultV2}, BlockHash, BlockHeaderV2, BlockV2, Digest, EraId, Gas, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, ProtocolVersion, PublicKey, Transaction, TransactionHash, U512, }; @@ -83,6 +83,7 @@ pub(crate) struct ExecutionArtifactBuilder { size_estimate: u64, min_cost: U512, available: Option, + evm_receipt: Option, } impl ExecutionArtifactBuilder { @@ -109,6 +110,7 @@ impl ExecutionArtifactBuilder { size_estimate: transaction.size_estimate() as u64, min_cost, available: None, + evm_receipt: None, } } @@ -133,6 +135,7 @@ impl ExecutionArtifactBuilder { size_estimate: transaction.size_estimate() as u64, min_cost: U512::zero(), available: None, + evm_receipt: None, } } @@ -382,6 +385,24 @@ impl ExecutionArtifactBuilder { self } + pub fn with_zero_cost(&mut self) -> &mut Self { + self.cost = U512::zero(); + self.min_cost = U512::zero(); + self + } + + pub fn with_evm_receipt( + &mut self, + receipt: evm::Receipt, + consumed: U512, + effects: Effects, + ) -> &mut Self { + self.evm_receipt = Some(receipt); + self.consumed = Gas::new(consumed); + self.with_appended_effects(effects); + self + } + pub fn with_invalid_wasm_v1_request( &mut self, invalid_request: &InvalidWasmV1Request, @@ -467,19 +488,33 @@ impl ExecutionArtifactBuilder { pub(crate) fn build(self) -> ExecutionArtifact { let actual_cost = self.cost_to_use(); - let result = ExecutionResultV2 { - effects: self.effects, - transfers: self.transfers, - initiator: self.initiator, - refund: self.refund, - limit: self.limit, - consumed: self.consumed, - cost: actual_cost, - current_price: self.current_price, - size_estimate: self.size_estimate, - error_message: self.error_message, + let execution_result = if let Some(receipt) = self.evm_receipt { + let result = EvmExecutionResult { + initiator: self.initiator, + current_price: self.current_price, + limit: self.limit, + cost: actual_cost, + refund: self.refund, + size_estimate: self.size_estimate, + effects: self.effects, + receipt, + }; + ExecutionResult::from(result) + } else { + let result = ExecutionResultV2 { + effects: self.effects, + transfers: self.transfers, + initiator: self.initiator, + refund: self.refund, + limit: self.limit, + consumed: self.consumed, + cost: actual_cost, + current_price: self.current_price, + size_estimate: self.size_estimate, + error_message: self.error_message, + }; + ExecutionResult::V2(Box::new(result)) }; - let execution_result = ExecutionResult::V2(Box::new(result)); ExecutionArtifact::new(self.hash, self.header, execution_result, self.messages) } @@ -578,7 +613,7 @@ impl SpeculativeExecutionResult { InvalidTransaction::V1(InvalidTransactionV1::UnableToCalculateGasLimit), ), Transaction::Evm(_) => SpeculativeExecutionResult::InvalidTransaction( - InvalidTransaction::Evm(casper_types::evm::TransactionError::Decode( + InvalidTransaction::Evm(evm::TransactionError::Decode( "EVM transactions are not routed through contract runtime".to_string(), )), ), diff --git a/node/src/components/contract_runtime/utils.rs b/node/src/components/contract_runtime/utils.rs index b2324d8dfd..a4789cb33b 100644 --- a/node/src/components/contract_runtime/utils.rs +++ b/node/src/components/contract_runtime/utils.rs @@ -1,3 +1,6 @@ +use casper_executor_evm::{ + BlockHashProvider as EvmBlockHashProvider, BlockHashProviderResult, BLOCK_HASH_HISTORY, +}; use casper_executor_wasm::ExecutorV2; use num_rational::Ratio; use once_cell::sync::Lazy; @@ -39,7 +42,9 @@ use casper_storage::{ }, global_state::state::{lmdb::LmdbGlobalState, CommitProvider, StateProvider}, }; -use casper_types::{BlockHash, Chainspec, Digest, EraId, Gas, Key, ProtocolUpgradeConfig}; +use casper_types::{ + BlockHash, Chainspec, Digest, EraId, Gas, Key, ProtocolUpgradeConfig, Transaction, +}; /// Maximum number of resource intensive tasks that can be run in parallel. /// @@ -49,6 +54,47 @@ const MAX_PARALLEL_INTENSIVE_TASKS: usize = 4; static INTENSIVE_TASKS_SEMAPHORE: Lazy = Lazy::new(|| tokio::sync::Semaphore::new(MAX_PARALLEL_INTENSIVE_TASKS)); +#[derive(Clone, Debug, Default)] +struct RecentBlockHashProvider { + block_hashes: BTreeMap, +} + +impl RecentBlockHashProvider { + async fn load(effect_builder: EffectBuilder, current_block_height: u64) -> Self + where + REv: From, + { + let block_hashes = load_recent_evm_block_hashes(effect_builder, current_block_height).await; + RecentBlockHashProvider { block_hashes } + } +} + +impl EvmBlockHashProvider for RecentBlockHashProvider { + fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { + Ok(self.block_hashes.get(&block_height).copied()) + } +} + +pub(crate) async fn load_recent_evm_block_hashes( + effect_builder: EffectBuilder, + current_block_height: u64, +) -> BTreeMap +where + REv: From, +{ + let earliest_block_height = current_block_height.saturating_sub(BLOCK_HASH_HISTORY); + let mut block_hashes = BTreeMap::new(); + for block_height in earliest_block_height..current_block_height { + if let Some(header) = effect_builder + .get_block_header_at_height_from_storage(block_height, true) + .await + { + block_hashes.insert(block_height, header.block_hash()); + } + } + block_hashes +} + /// Asynchronously runs a resource intensive task. /// At most `MAX_PARALLEL_INTENSIVE_TASKS` are being run in parallel at any time. /// @@ -274,6 +320,15 @@ pub(super) async fn exec_and_check_next( }; let current_gas_price = executable_block.current_gas_price; + let evm_block_hash_provider = if executable_block + .transactions + .iter() + .any(|transaction| matches!(transaction, Transaction::Evm(_))) + { + RecentBlockHashProvider::load(effect_builder, executable_block.height).await + } else { + RecentBlockHashProvider::default() + }; let contract_runtime_metrics = metrics.clone(); let task = move || { debug!("ContractRuntime: execute_finalized_block"); @@ -284,6 +339,7 @@ pub(super) async fn exec_and_check_next( chainspec.as_ref(), Some(contract_runtime_metrics), current_pre_state, + &evm_block_hash_provider, executable_block, key_block_height_for_activation_point, current_gas_price, diff --git a/node/src/components/network/tasks.rs b/node/src/components/network/tasks.rs index 9700ddbbb6..5e08c9db54 100644 --- a/node/src/components/network/tasks.rs +++ b/node/src/components/network/tasks.rs @@ -685,111 +685,110 @@ where let demands_in_flight = Arc::new(Semaphore::new(context.max_in_flight_demands)); let event_queue = context.event_queue.expect("component not initialized"); - let read_messages = async move { - while let Some(msg_result) = stream.next().await { - match msg_result { - Ok(msg) => { - trace!(%msg, "message received"); - - let effect_builder = EffectBuilder::new(event_queue); - - match msg.try_into_demand(effect_builder, peer_id) { - Ok((event, wait_for_response)) => { - // Note: For now, demands bypass the limiter, as we expect the - // backpressure to handle this instead. - - // Acquire a permit. If we are handling too many demands at this - // time, this will block, halting the processing of new message, - // thus letting the peer they have reached their maximum allowance. - let in_flight = demands_in_flight - .clone() - .acquire_owned() - .await - // Note: Since the semaphore is reference counted, it must - // explicitly be closed for acquisition to fail, which we - // never do. If this happens, there is a bug in the code; - // we exit with an error and close the connection. - .map_err(|_| { - io::Error::new( - io::ErrorKind::Other, - "demand limiter semaphore closed unexpectedly", - ) - })?; - - Metrics::record_trie_request_start(&context.net_metrics); - - let net_metrics = context.net_metrics.clone(); - // Spawn a future that will eventually send the returned message. It - // will essentially buffer the response. - tokio::spawn(async move { - if let Some(payload) = wait_for_response.await { - // Send message and await its return. `send_message` should - // only return when the message has been buffered, if the - // peer is not accepting data, we will block here until the - // send buffer has sufficient room. - effect_builder.send_message(peer_id, payload).await; - - // Note: We could short-circuit the event queue here and - // directly insert into the outgoing message queue, - // which may be potential performance improvement. - } - - // Missing else: The handler of the demand did not deem it - // worthy a response. Just drop it. - - // After we have either successfully buffered the message for - // sending, failed to do so or did not have a message to send - // out, we consider the request handled and free up the permit. - Metrics::record_trie_request_end(&net_metrics); - drop(in_flight); - }); - - // Schedule the created event. - event_queue - .schedule::(event, QueueKind::NetworkDemand) - .await; - } - Err(msg) => { - // We've received a non-demand message. Ensure we have the proper amount - // of resources, then push it to the reactor. - limiter - .request_allowance( - msg.payload_incoming_resource_estimate( + let read_messages = + async move { + while let Some(msg_result) = stream.next().await { + match msg_result { + Ok(msg) => { + trace!(%msg, "message received"); + + let effect_builder = EffectBuilder::new(event_queue); + + match msg.try_into_demand(effect_builder, peer_id) { + Ok((event, wait_for_response)) => { + // Note: For now, demands bypass the limiter, as we expect the + // backpressure to handle this instead. + + // Acquire a permit. If we are handling too many demands at this + // time, this will block, halting the processing of new message, + // thus letting the peer they have reached their maximum allowance. + let in_flight = demands_in_flight + .clone() + .acquire_owned() + .await + // Note: Since the semaphore is reference counted, it must + // explicitly be closed for acquisition to fail, which we + // never do. If this happens, there is a bug in the code; + // we exit with an error and close the connection. + .map_err(|_| { + io::Error::new( + io::ErrorKind::Other, + "demand limiter semaphore closed unexpectedly", + ) + })?; + + Metrics::record_trie_request_start(&context.net_metrics); + + let net_metrics = context.net_metrics.clone(); + // Spawn a future that will eventually send the returned message. It + // will essentially buffer the response. + tokio::spawn(async move { + if let Some(payload) = wait_for_response.await { + // Send message and await its return. `send_message` should + // only return when the message has been buffered, if the + // peer is not accepting data, we will block here until the + // send buffer has sufficient room. + effect_builder.send_message(peer_id, payload).await; + + // Note: We could short-circuit the event queue here and + // directly insert into the outgoing message queue, + // which may be potential performance improvement. + } + + // Missing else: The handler of the demand did not deem it + // worthy a response. Just drop it. + + // After we have either successfully buffered the message for + // sending, failed to do so or did not have a message to send + // out, we consider the request handled and free up the permit. + Metrics::record_trie_request_end(&net_metrics); + drop(in_flight); + }); + + // Schedule the created event. + event_queue + .schedule::(event, QueueKind::NetworkDemand) + .await; + } + Err(msg) => { + // We've received a non-demand message. Ensure we have the proper amount + // of resources, then push it to the reactor. + limiter + .request_allowance(msg.payload_incoming_resource_estimate( &context.payload_weights, - ), - ) - .await; - - let queue_kind = if msg.is_low_priority() { - QueueKind::NetworkLowPriority - } else { - QueueKind::NetworkIncoming - }; - - event_queue - .schedule( - Event::IncomingMessage { - peer_id: Box::new(peer_id), - msg, - span: span.clone(), - }, - queue_kind, - ) - .await; + )) + .await; + + let queue_kind = if msg.is_low_priority() { + QueueKind::NetworkLowPriority + } else { + QueueKind::NetworkIncoming + }; + + event_queue + .schedule( + Event::IncomingMessage { + peer_id: Box::new(peer_id), + msg, + span: span.clone(), + }, + queue_kind, + ) + .await; + } } } - } - Err(err) => { - warn!( - err = display_error(&err), - "receiving message failed, closing connection" - ); - return Err(err); + Err(err) => { + warn!( + err = display_error(&err), + "receiving message failed, closing connection" + ); + return Err(err); + } } } - } - Ok(()) - }; + Ok(()) + }; let shutdown_messages = async move { while close_incoming_receiver.changed().await.is_ok() {} }; diff --git a/node/src/components/storage.rs b/node/src/components/storage.rs index fc35d1777a..fadfb1351c 100644 --- a/node/src/components/storage.rs +++ b/node/src/components/storage.rs @@ -1723,7 +1723,10 @@ impl Storage { } (approvals_hash, finalized_approvals, transaction @ Transaction::Evm(_)) => { match ApprovalsHash::compute(&finalized_approvals) { - Ok(computed_approvals_hash) if computed_approvals_hash == approvals_hash => { + Ok(computed_approvals_hash) + if computed_approvals_hash == approvals_hash + && finalized_approvals == transaction.approvals() => + { Ok(Some(transaction)) } Ok(_computed_approvals_hash) => Ok(None), @@ -2047,11 +2050,9 @@ impl Storage { Some(Transaction::V1(transaction_v1)) => { ret.push((transaction_hash, (&transaction_v1).into(), execution_result)) } - Some(Transaction::Evm(transaction)) => ret.push(( - transaction_hash, - transaction.as_ref().into(), - execution_result, - )), + Some(Transaction::Evm(transaction)) => { + ret.push((transaction_hash, (&transaction).into(), execution_result)) + } }; } Ok(Some(ret)) diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index ef3c49cb3a..1f07fe26c5 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -13,7 +13,9 @@ use datasize::DataSize; use prometheus::Registry; use tracing::{debug, error, trace}; -use casper_storage::data_access_layer::{balance::BalanceHandling, BalanceRequest, ProofHandling}; +use casper_storage::data_access_layer::{ + balance::BalanceHandling, BalanceIdentifier, BalanceRequest, ProofHandling, +}; use casper_types::{ account::AccountHash, addressable_entity::AddressableEntity, system::auction::ARG_AMOUNT, AddressableEntityHash, AddressableEntityIdentifier, BlockHeader, Chainspec, EntityAddr, @@ -212,19 +214,27 @@ impl TransactionAcceptor { }; if event_metadata.source.is_client() { - let initiator_addr = event_metadata.transaction.initiator_addr(); - let Some(account_hash) = initiator_addr.account_hash() else { - return self.reject_transaction( - effect_builder, - *event_metadata, - Error::InvalidTransaction(InvalidTransaction::Evm( - casper_types::evm::TransactionError::Decode( - "EVM transactions are not routed through transaction acceptor" - .to_string(), - ), - )), + if let Some(evm_transaction) = event_metadata.meta_transaction.as_evm() { + let balance_request = BalanceRequest::new( + *block_header.state_root_hash(), + block_header.protocol_version(), + BalanceIdentifier::Evm(evm_transaction.from()), + BalanceHandling::Available, + ProofHandling::NoProofs, ); - }; + return effect_builder + .get_balance(balance_request) + .event(move |balance_result| Event::GetBalanceResult { + event_metadata, + block_header, + maybe_balance: balance_result.available_balance().copied(), + }); + } + + let initiator_addr = event_metadata.transaction.initiator_addr(); + let account_hash = initiator_addr + .account_hash() + .expect("non-EVM transaction initiator must be a Casper account"); let entity_addr = EntityAddr::Account(account_hash.value()); effect_builder .get_addressable_entity(*block_header.state_root_hash(), entity_addr) @@ -414,6 +424,9 @@ impl TransactionAcceptor { MetaTransaction::Deploy(_) => { self.verify_deploy_session(effect_builder, event_metadata, block_header) } + MetaTransaction::Evm(_) => { + self.validate_transaction_cryptography(effect_builder, event_metadata) + } MetaTransaction::V1(_) => { self.verify_transaction_v1_body(effect_builder, event_metadata, block_header) } @@ -428,6 +441,14 @@ impl TransactionAcceptor { ) -> Effects { let session = match &event_metadata.meta_transaction { MetaTransaction::Deploy(meta_deploy) => meta_deploy.session(), + MetaTransaction::Evm(_) => { + error!("should only handle deploys in verify_deploy_session"); + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::ExpectedDeploy, + ); + } MetaTransaction::V1(txn) => { error!(%txn, "should only handle deploys in verify_deploy_session"); return self.reject_transaction( @@ -555,6 +576,14 @@ impl TransactionAcceptor { Error::ExpectedTransactionV1, ); } + MetaTransaction::Evm(_) => { + error!("should only handle version 1 transactions in verify_transaction_v1_body"); + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::ExpectedTransactionV1, + ); + } MetaTransaction::V1(txn) => match txn.target() { TransactionTarget::Stored { id, .. } => match id { TransactionInvocationTarget::ByHash(entity_addr) => { @@ -646,6 +675,10 @@ impl TransactionAcceptor { .entry_point_name() .to_string(), ), + MetaTransaction::Evm(_) => { + error!("should not fetch a contract to validate EVM transactions"); + None + } MetaTransaction::V1(_) if is_payment => { error!("should not fetch a contract to validate payment logic for transaction v1s"); None @@ -857,6 +890,9 @@ impl TransactionAcceptor { .deploy() .is_valid() .map_err(|err| Error::InvalidTransaction(err.into())), + MetaTransaction::Evm(evm) => evm + .verify() + .map_err(|err| Error::InvalidTransaction(err.into())), MetaTransaction::V1(txn) => txn .verify() .map_err(|err| Error::InvalidTransaction(err.into())), diff --git a/node/src/effect.rs b/node/src/effect.rs index 6004d0efaa..770f7972d5 100644 --- a/node/src/effect.rs +++ b/node/src/effect.rs @@ -2322,6 +2322,28 @@ impl EffectBuilder { .await } + /// Requests a read-only EVM call, without committing its effects. + pub(crate) async fn evm_call( + self, + block_header: Box, + block_hashes: BTreeMap, + request: Box, + ) -> Result + where + REv: From, + { + self.make_request( + |responder| ContractRuntimeRequest::EvmCall { + block_header, + block_hashes, + request, + responder, + }, + QueueKind::ContractRuntime, + ) + .await + } + /// Reads block execution results (or chunk) from Storage component. pub(crate) async fn get_block_execution_results_or_chunk_from_storage( self, diff --git a/node/src/effect/requests.rs b/node/src/effect/requests.rs index cf497e00f6..d95181505c 100644 --- a/node/src/effect/requests.rs +++ b/node/src/effect/requests.rs @@ -16,7 +16,8 @@ use smallvec::SmallVec; use static_assertions::const_assert; use casper_binary_port::{ - ConsensusStatus, ConsensusValidatorChanges, LastProgress, NetworkName, RecordId, Uptime, + ConsensusStatus, ConsensusValidatorChanges, EvmCallRequest, EvmCallResult, LastProgress, + NetworkName, RecordId, Uptime, }; use casper_storage::{ block_store::types::ApprovalsHashes, @@ -883,6 +884,17 @@ pub(crate) enum ContractRuntimeRequest { /// Results responder: Responder, }, + /// Execute a read-only EVM call without committing effects. + EvmCall { + /// Pre-state. + block_header: Box, + /// Recent block hashes available to the EVM `BLOCKHASH` opcode. + block_hashes: BTreeMap, + /// EVM call request. + request: Box, + /// Result. + responder: Responder>, + }, UpdateRuntimePrice(EraId, u8), GetEraGasPrice { era_id: EraId, @@ -972,6 +984,17 @@ impl Display for ContractRuntimeRequest { block_header.state_root_hash() ) } + ContractRuntimeRequest::EvmCall { + request, + block_header, + .. + } => write!( + formatter, + "Execute EVM call from {} to {:?} on {}", + request.from(), + request.to(), + block_header.state_root_hash() + ), ContractRuntimeRequest::UpdateRuntimePrice(_, era_gas_price) => { write!(formatter, "updating price to {}", era_gas_price) } diff --git a/node/src/reactor/main_reactor/tests/configs_override.rs b/node/src/reactor/main_reactor/tests/configs_override.rs index 5734f1e776..49492cfe1b 100644 --- a/node/src/reactor/main_reactor/tests/configs_override.rs +++ b/node/src/reactor/main_reactor/tests/configs_override.rs @@ -3,8 +3,8 @@ use std::collections::BTreeSet; use num_rational::Ratio; use casper_types::{ - ConsensusProtocolName, FeeHandling, HoldBalanceHandling, PricingHandling, PublicKey, - RefundHandling, TimeDiff, TransactionV1Config, + evm::EvmConfig, ConsensusProtocolName, FeeHandling, HoldBalanceHandling, PricingHandling, + PublicKey, RefundHandling, TimeDiff, TransactionV1Config, }; use crate::types::SyncHandling; @@ -36,6 +36,7 @@ pub(crate) struct ConfigsOverride { pub chain_name: Option, pub gas_hold_balance_handling: Option, pub transaction_v1_override: Option, + pub evm_config_override: Option, pub node_config_override: NodeConfigOverride, pub minimum_delegation_rate: u8, } @@ -128,6 +129,11 @@ impl ConfigsOverride { self } + pub(crate) fn with_evm_config(mut self, evm_config: EvmConfig) -> Self { + self.evm_config_override = Some(evm_config); + self + } + pub(crate) fn with_idle_tolerance(mut self, idle_tolernace: TimeDiff) -> Self { let config = NodeConfigOverride { idle_tolerance: Some(idle_tolernace), @@ -171,6 +177,7 @@ impl Default for ConfigsOverride { chain_name: None, gas_hold_balance_handling: None, transaction_v1_override: None, + evm_config_override: None, node_config_override: NodeConfigOverride::default(), minimum_delegation_rate: 0, } diff --git a/node/src/reactor/main_reactor/tests/fixture.rs b/node/src/reactor/main_reactor/tests/fixture.rs index 7cc72f0ee1..106aaa5cee 100644 --- a/node/src/reactor/main_reactor/tests/fixture.rs +++ b/node/src/reactor/main_reactor/tests/fixture.rs @@ -179,6 +179,7 @@ impl TestFixture { chain_name, gas_hold_balance_handling, transaction_v1_override, + evm_config_override, node_config_override, minimum_delegation_rate, } = spec_override.unwrap_or_default(); @@ -233,6 +234,9 @@ impl TestFixture { if let Some(transaction_v1_config) = transaction_v1_override { chainspec.transaction_config.transaction_v1_config = transaction_v1_config } + if let Some(evm_config) = evm_config_override { + chainspec.evm_config = evm_config; + } let applied_block_gas_limit = chainspec.transaction_config.block_gas_limit; diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index 71d7c07c71..d784e10a57 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -1,21 +1,33 @@ use super::{fixture::TestFixture, *}; use crate::{ + components::contract_runtime::ExecutionPreState, testing::LARGE_WASM_LANE_ID, types::{transaction::calculate_transaction_lane_for_transaction, MetaTransaction}, }; -use casper_storage::data_access_layer::{ - AddressableEntityRequest, BalanceIdentifier, BalanceIdentifierPurseRequest, - BalanceIdentifierPurseResult, ProofHandling, QueryRequest, QueryResult, +use alloy_consensus::{SignableTransaction, TxEnvelope, TxLegacy}; +use alloy_eips::Encodable2718; +use alloy_primitives::{ + Address as AlloyAddress, Bytes as AlloyBytes, Signature as AlloySignature, TxKind, U256, +}; +use casper_executor_evm::EMPTY_CODE_HASH; +use casper_storage::{ + data_access_layer::{ + AddressableEntityRequest, BalanceIdentifier, BalanceIdentifierPurseRequest, + BalanceIdentifierPurseResult, ProofHandling, QueryRequest, QueryResult, + }, + global_state::state::CommitProvider, }; use casper_types::{ account::AccountHash, addressable_entity::NamedKeyAddr, runtime_args, system::mint::{ARG_AMOUNT, ARG_TARGET}, - AccessRights, AddressableEntity, Digest, EntityAddr, ExecutableDeployItem, ExecutionInfo, - TransactionRuntimeParams, URef, URefAddr, DEFAULT_TRANSFER_COST, + AccessRights, AddressableEntity, CLValue, Digest, EntityAddr, ExecutableDeployItem, + ExecutionInfo, InitiatorAddr, TransactionRuntimeParams, URef, URefAddr, DEFAULT_TRANSFER_COST, }; +use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey}; use once_cell::sync::Lazy; +use revm::bytecode::opcode; use std::collections::BTreeMap; use crate::reactor::main_reactor::tests::{ @@ -23,6 +35,7 @@ use crate::reactor::main_reactor::tests::{ }; use casper_types::{ bytesrepr::{Bytes, ToBytes}, + evm, execution::ExecutionResultV1, }; @@ -765,6 +778,394 @@ pub fn exec_result_is_success(exec_result: &ExecutionResult) -> bool { } } +const EVM_TEST_GAS_LIMIT: u64 = 500_000; +const EVM_TEST_GAS_PRICE: u128 = 1; +const EVM_INITIAL_BALANCE: u64 = 10_000_000_000_000; +const EVM_LOG_TOPIC: evm::Hash = evm::Hash::new([0xAB; evm::HASH_LENGTH]); + +fn evm_log_emitting_init_code() -> Vec { + const MEMORY_OFFSET: u8 = 0; + const RUNTIME_LEN: u8 = 1; + const COPY_AND_RETURN_RUNTIME_LEN: usize = 12; + + let mut init_code = Vec::new(); + + // Solidity equivalent: + // + // event Log(bytes32 indexed topic); + // emit Log(EVM_LOG_TOPIC); + init_code.push(opcode::PUSH32); + init_code.extend_from_slice(EVM_LOG_TOPIC.as_bytes()); + init_code.extend_from_slice(&[ + opcode::PUSH1, + 0, // log data size + opcode::PUSH1, + MEMORY_OFFSET, // log data offset + opcode::LOG1, + ]); + + let runtime_offset = u8::try_from(init_code.len() + COPY_AND_RETURN_RUNTIME_LEN) + .expect("runtime offset should fit in a PUSH1 immediate"); + + // Solidity equivalent: + // + // bytes memory runtime = hex"00"; + // assembly { return(add(runtime, 32), 1) } + init_code.extend_from_slice(&[ + // codecopy(memoryOffset: 0, codeOffset: runtime_offset, size: 1) + opcode::PUSH1, + RUNTIME_LEN, + opcode::PUSH1, + runtime_offset, + opcode::PUSH1, + MEMORY_OFFSET, + opcode::CODECOPY, + // return(memoryOffset: 0, size: 1) + opcode::PUSH1, + RUNTIME_LEN, + opcode::PUSH1, + MEMORY_OFFSET, + opcode::RETURN, + ]); + + // Deployed runtime equivalent: + // + // fallback() external { } + init_code.extend_from_slice(&[ + // stop() + opcode::STOP, + ]); + init_code +} + +fn signed_evm_deploy_transaction(chain_id: u64) -> evm::Transaction { + let transaction = TxLegacy { + chain_id: Some(chain_id), + nonce: 0, + gas_price: EVM_TEST_GAS_PRICE, + gas_limit: EVM_TEST_GAS_LIMIT, + to: TxKind::Create, + value: U256::ZERO, + input: AlloyBytes::from(evm_log_emitting_init_code()), + }; + signed_evm_legacy_transaction(transaction) +} + +fn signed_evm_value_transfer_transaction( + chain_id: u64, + recipient: evm::Address, + gas_limit: u64, + value: u64, +) -> evm::Transaction { + let transaction = TxLegacy { + chain_id: Some(chain_id), + nonce: 0, + gas_price: EVM_TEST_GAS_PRICE, + gas_limit, + to: TxKind::Call(AlloyAddress::from(recipient.value())), + value: U256::from(value), + input: AlloyBytes::new(), + }; + signed_evm_legacy_transaction(transaction) +} + +fn signed_evm_legacy_transaction(transaction: TxLegacy) -> evm::Transaction { + let signing_key = + SigningKey::from_slice(&[0x11; 32]).expect("test EVM private key should be valid"); + let (signature, recovery_id) = signing_key + .sign_prehash(transaction.signature_hash().as_ref()) + .expect("test EVM transaction signing should succeed"); + let signed = transaction.into_signed(AlloySignature::from((signature, recovery_id))); + let envelope = TxEnvelope::from(signed); + evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::now(), + TimeDiff::from_seconds(60), + ) + .expect("test EVM transaction should decode") +} + +fn seed_evm_account(fixture: &mut TestFixture, address: evm::Address, balance: U512) { + let main_purse = evm::deterministic_purse(address); + let values_to_write = vec![ + ( + Key::EvmAccount(address), + StoredValue::EvmAccount(evm::Account::new(0, EMPTY_CODE_HASH, main_purse)), + ), + ( + Key::Balance(main_purse.addr()), + StoredValue::CLValue(CLValue::from_t(balance).unwrap()), + ), + ]; + for runner in fixture.network.runners_mut() { + let execution_pre_state = runner + .main_reactor() + .contract_runtime() + .execution_pre_state(); + let state_root_hash = runner + .main_reactor() + .contract_runtime() + .data_access_layer() + .commit_values( + execution_pre_state.pre_state_root_hash(), + values_to_write.clone(), + Default::default(), + ) + .expect("EVM seed account should commit"); + runner + .main_reactor_as_mut() + .contract_runtime + .set_execution_pre_state(ExecutionPreState::new( + execution_pre_state.next_block_height(), + state_root_hash, + execution_pre_state.parent_hash(), + execution_pre_state.parent_seed(), + )); + } +} + +fn evm_balance(fixture: &TestFixture, address: evm::Address, block_height: u64) -> U512 { + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let protocol_version = fixture.chainspec.protocol_version(); + let block_header = runner + .main_reactor() + .storage() + .read_block_header_by_height(block_height, true) + .expect("failure to read block header") + .expect("should have header"); + let result = runner + .main_reactor() + .contract_runtime() + .data_access_layer() + .balance(BalanceRequest::new( + *block_header.state_root_hash(), + protocol_version, + BalanceIdentifier::Evm(address), + BalanceHandling::Total, + ProofHandling::NoProofs, + )); + *result + .total_balance() + .expect("EVM account should have a balance") +} + +fn evm_account_at( + fixture: &mut TestFixture, + block_height: u64, + address: evm::Address, +) -> evm::Account { + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let block_header = runner + .main_reactor() + .storage() + .read_block_header_by_height(block_height, true) + .expect("failure to read block header") + .expect("should have header"); + let state_root_hash = *block_header.state_root_hash(); + match query_global_state(fixture, state_root_hash, Key::EvmAccount(address)) { + Some(value) => match *value { + StoredValue::EvmAccount(account) => account, + value => panic!("expected EVM account, got {value:?}"), + }, + value => panic!("expected EVM account, got {value:?}"), + } +} + +fn alloy_address_to_evm_address(address: AlloyAddress) -> evm::Address { + let mut bytes = [0; evm::ADDRESS_LENGTH]; + bytes.copy_from_slice(address.as_slice()); + evm::Address::new(bytes) +} + +#[tokio::test] +async fn should_execute_evm_transaction_and_store_receipt() { + let evm_config = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let evm_transaction = signed_evm_deploy_transaction(evm_config.chain_id); + let sender = evm_transaction.from(); + let expected_sender = alloy_address_to_evm_address(AlloyAddress::from_private_key( + &SigningKey::from_slice(&[0x11; 32]).unwrap(), + )); + assert_eq!(sender, expected_sender); + + let highest_block = test.fixture.highest_complete_block(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + let initial_balance = U512::from(EVM_INITIAL_BALANCE); + + let (_txn_hash, block_height, execution_result) = test + .send_transaction(Transaction::from(evm_transaction.clone())) + .await; + let ExecutionResult::Evm(execution_result) = execution_result else { + panic!("expected EVM execution result"); + }; + + assert_eq!( + execution_result.initiator, + InitiatorAddr::EvmAddress(sender) + ); + assert_eq!(execution_result.receipt.status, evm::ReceiptStatus::Success); + assert_eq!( + execution_result.receipt.effective_gas_price, + evm_transaction.effective_gas_price(evm_config.base_fee) + ); + assert!(execution_result.receipt.gas_used > 0); + let max_fee_amount = U512::from(evm_transaction.gas_limit()) * U512::from(EVM_TEST_GAS_PRICE); + assert_eq!(execution_result.cost, max_fee_amount); + assert_eq!(execution_result.refund, U512::zero()); + assert!(execution_result.receipt.contract_address.is_some()); + assert_eq!(execution_result.receipt.logs.len(), 1); + assert_eq!(execution_result.receipt.logs[0].topics, vec![EVM_LOG_TOPIC]); + assert!(execution_result.receipt.logs[0].data.is_empty()); + + let final_balance = evm_balance(&test.fixture, sender, block_height); + assert_eq!(final_balance, initial_balance - execution_result.cost); + let account = evm_account_at(&mut test.fixture, block_height, sender); + assert_eq!(account.nonce(), 1); + assert!( + block_height > highest_block.height(), + "EVM transaction should be included in a later block" + ); +} + +#[tokio::test] +async fn should_apply_casper_refund_handling_to_evm_transaction() { + let evm_config = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_refund_handling(RefundHandling::Refund { + refund_ratio: Ratio::new(1, 1), + }) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let evm_transaction = signed_evm_deploy_transaction(evm_config.chain_id); + let sender = evm_transaction.from(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + let initial_balance = U512::from(EVM_INITIAL_BALANCE); + + let (_txn_hash, block_height, execution_result) = test + .send_transaction(Transaction::from(evm_transaction.clone())) + .await; + let ExecutionResult::Evm(execution_result) = execution_result else { + panic!("expected EVM execution result"); + }; + + let max_fee_amount = U512::from(evm_transaction.gas_limit()) * U512::from(EVM_TEST_GAS_PRICE); + let consumed_fee_amount = + U512::from(execution_result.receipt.gas_used) * U512::from(EVM_TEST_GAS_PRICE); + + assert_eq!(execution_result.receipt.status, evm::ReceiptStatus::Success); + assert_eq!(execution_result.cost, max_fee_amount); + assert_eq!( + execution_result.refund, + max_fee_amount - consumed_fee_amount + ); + + let final_balance = evm_balance(&test.fixture, sender, block_height); + assert_eq!(final_balance, initial_balance - consumed_fee_amount); +} + +#[tokio::test] +async fn should_reject_evm_transaction_when_value_and_fee_exceed_balance() { + let evm_config = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let gas_limit = 21_000; + let value = EVM_INITIAL_BALANCE - gas_limit + 1; + let recipient = evm::Address::new([0x22; evm::ADDRESS_LENGTH]); + let evm_transaction = + signed_evm_value_transfer_transaction(evm_config.chain_id, recipient, gas_limit, value); + let sender = evm_transaction.from(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + + let (_txn_hash, block_height, execution_result) = test + .send_transaction(Transaction::from(evm_transaction)) + .await; + let ExecutionResult::Evm(execution_result) = execution_result else { + panic!("expected EVM execution result"); + }; + + assert_eq!( + execution_result.receipt.status, + evm::ReceiptStatus::Halt(evm::HaltReason::Unknown) + ); + assert_eq!(execution_result.receipt.gas_used, 0); + assert_eq!(execution_result.cost, U512::zero()); + assert_eq!(execution_result.refund, U512::zero()); + assert!(execution_result.effects.is_empty()); + + let final_balance = evm_balance(&test.fixture, sender, block_height); + assert_eq!(final_balance, U512::from(EVM_INITIAL_BALANCE)); + + let (_node_id, runner) = test.fixture.network.nodes().iter().next().unwrap(); + let block_header = runner + .main_reactor() + .storage() + .read_block_header_by_height(block_height, true) + .expect("failure to read block header") + .expect("should have header"); + assert!(query_global_state( + &mut test.fixture, + *block_header.state_root_hash(), + Key::EvmAccount(recipient) + ) + .is_none()); +} + #[tokio::test] async fn should_accept_transfer_without_id() { let initial_stakes = InitialStakes::FromVec(vec![u128::MAX, 1]); diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index c9e90b3410..0a56f36144 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -1,17 +1,20 @@ mod meta_deploy; +mod meta_evm; mod meta_transaction_v1; mod transaction_header; use casper_execution_engine::engine_state::{SessionDataDeploy, SessionDataV1, SessionInputData}; #[cfg(test)] use casper_types::InvalidTransactionV1; use casper_types::{ - account::AccountHash, bytesrepr::ToBytes, Approval, Chainspec, Digest, ExecutableDeployItem, - Gas, GasLimited, HashAddr, InitiatorAddr, InvalidTransaction, Phase, PricingHandling, - PricingMode, TimeDiff, Timestamp, Transaction, TransactionArgs, TransactionConfig, - TransactionEntryPoint, TransactionHash, TransactionTarget, INSTALL_UPGRADE_LANE_ID, + account::AccountHash, bytesrepr::ToBytes, evm, Approval, Chainspec, Digest, + ExecutableDeployItem, Gas, GasLimited, HashAddr, InitiatorAddr, InvalidTransaction, Phase, + PricingHandling, PricingMode, TimeDiff, Timestamp, Transaction, TransactionArgs, + TransactionConfig, TransactionEntryPoint, TransactionHash, TransactionTarget, + INSTALL_UPGRADE_LANE_ID, }; use core::fmt::{self, Debug, Display, Formatter}; use meta_deploy::MetaDeploy; +use meta_evm::MetaEvmTransaction; pub(crate) use meta_transaction_v1::MetaTransactionV1; use serde::Serialize; use std::{borrow::Cow, collections::BTreeSet}; @@ -23,6 +26,7 @@ use super::fields_container::{ARGS_MAP_KEY, ENTRY_POINT_MAP_KEY, TARGET_MAP_KEY} #[derive(Clone, Debug, Serialize)] pub(crate) enum MetaTransaction { Deploy(MetaDeploy), + Evm(MetaEvmTransaction), V1(MetaTransactionV1), } @@ -33,6 +37,7 @@ impl MetaTransaction { MetaTransaction::Deploy(meta_deploy) => { TransactionHash::from(*meta_deploy.deploy().hash()) } + MetaTransaction::Evm(evm) => evm.hash(), MetaTransaction::V1(txn) => TransactionHash::from(*txn.hash()), } } @@ -41,6 +46,7 @@ impl MetaTransaction { pub(crate) fn timestamp(&self) -> Timestamp { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().header().timestamp(), + MetaTransaction::Evm(evm) => evm.timestamp(), MetaTransaction::V1(v1) => v1.timestamp(), } } @@ -49,6 +55,7 @@ impl MetaTransaction { pub(crate) fn ttl(&self) -> TimeDiff { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().header().ttl(), + MetaTransaction::Evm(evm) => evm.ttl(), MetaTransaction::V1(v1) => v1.ttl(), } } @@ -57,6 +64,7 @@ impl MetaTransaction { pub(crate) fn approvals(&self) -> BTreeSet { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().approvals().clone(), + MetaTransaction::Evm(evm) => evm.approvals().clone(), MetaTransaction::V1(v1) => v1.approvals().clone(), } } @@ -65,6 +73,7 @@ impl MetaTransaction { pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.initiator_addr(), + MetaTransaction::Evm(evm) => evm.initiator_addr(), MetaTransaction::V1(txn) => txn.initiator_addr(), } } @@ -78,6 +87,11 @@ impl MetaTransaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + MetaTransaction::Evm(evm) => evm + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), MetaTransaction::V1(txn) => txn .approvals() .iter() @@ -90,6 +104,7 @@ impl MetaTransaction { pub(crate) fn is_native(&self) -> bool { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().is_transfer(), + MetaTransaction::Evm(_) => false, MetaTransaction::V1(v1_txn) => *v1_txn.target() == TransactionTarget::Native, } } @@ -108,6 +123,7 @@ impl MetaTransaction { .deploy() .payment() .is_standard_payment(Phase::Payment), + MetaTransaction::Evm(_) => true, MetaTransaction::V1(v1) => { if let PricingMode::PaymentLimited { standard_payment, .. @@ -128,6 +144,7 @@ impl MetaTransaction { .deploy() .payment() .is_standard_payment(Phase::Payment), + MetaTransaction::Evm(_) => false, MetaTransaction::V1(v1) => { if let PricingMode::PaymentLimited { standard_payment, .. @@ -150,6 +167,11 @@ impl MetaTransaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + MetaTransaction::Evm(evm) => evm + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), MetaTransaction::V1(transaction_v1) => transaction_v1 .approvals() .iter() @@ -159,11 +181,14 @@ impl MetaTransaction { } /// The session args. - pub(crate) fn session_args(&self) -> Cow { + pub(crate) fn session_args(&self) -> Cow<'_, TransactionArgs> { match self { MetaTransaction::Deploy(meta_deploy) => Cow::Owned(TransactionArgs::Named( meta_deploy.deploy().session().args().clone(), )), + MetaTransaction::Evm(_) => { + unreachable!("EVM transactions do not have Casper session args") + } MetaTransaction::V1(transaction_v1) => Cow::Borrowed(transaction_v1.args()), } } @@ -174,6 +199,9 @@ impl MetaTransaction { MetaTransaction::Deploy(meta_deploy) => { meta_deploy.deploy().session().entry_point_name().into() } + MetaTransaction::Evm(_) => { + unreachable!("EVM transactions do not have Casper entry points") + } MetaTransaction::V1(transaction_v1) => transaction_v1.entry_point().clone(), } } @@ -182,6 +210,7 @@ impl MetaTransaction { pub(crate) fn transaction_lane(&self) -> u8 { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.lane_id(), + MetaTransaction::Evm(evm) => evm.lane_id(), MetaTransaction::V1(v1) => v1.lane_id(), } } @@ -193,6 +222,7 @@ impl MetaTransaction { .deploy() .gas_price_tolerance() .map_err(InvalidTransaction::from), + MetaTransaction::Evm(evm) => Ok(evm.gas_price_tolerance()), MetaTransaction::V1(v1) => Ok(v1.gas_price_tolerance()), } } @@ -203,6 +233,7 @@ impl MetaTransaction { .deploy() .gas_limit(chainspec) .map_err(InvalidTransaction::from), + MetaTransaction::Evm(evm) => Ok(evm.gas_limit()), MetaTransaction::V1(v1) => v1.gas_limit(chainspec), } } @@ -211,6 +242,7 @@ impl MetaTransaction { pub(crate) fn is_deploy_transaction(&self) -> bool { match self { MetaTransaction::Deploy(_) => true, + MetaTransaction::Evm(_) => false, MetaTransaction::V1(_) => false, } } @@ -234,6 +266,7 @@ impl MetaTransaction { MetaTransaction::V1(v1) => { return v1.contract_direct_address(); } + MetaTransaction::Evm(_) => {} } None } @@ -256,11 +289,10 @@ impl MetaTransaction { &transaction_config.transaction_v1_config, ) .map(MetaTransaction::V1), - Transaction::Evm(_) => Err(InvalidTransaction::Evm( - casper_types::evm::TransactionError::Decode( - "EVM transactions are not routed through node transaction metadata".to_string(), - ), - )), + Transaction::Evm(evm) => { + MetaEvmTransaction::from_evm_transaction(evm, transaction_config) + .map(MetaTransaction::Evm) + } } } @@ -275,6 +307,7 @@ impl MetaTransaction { .deploy() .is_config_compliant(chainspec, timestamp_leeway, at) .map_err(InvalidTransaction::from), + MetaTransaction::Evm(evm) => evm.is_config_compliant(chainspec).map_err(Into::into), MetaTransaction::V1(v1) => v1 .is_config_compliant(chainspec, timestamp_leeway, at) .map_err(InvalidTransaction::from), @@ -284,11 +317,12 @@ impl MetaTransaction { pub(crate) fn payload_hash(&self) -> Digest { match self { MetaTransaction::Deploy(meta_deploy) => *meta_deploy.deploy().body_hash(), + MetaTransaction::Evm(evm) => evm.payload_hash(), MetaTransaction::V1(v1) => *v1.payload_hash(), } } - pub(crate) fn to_session_input_data(&self) -> SessionInputData { + pub(crate) fn to_session_input_data(&self) -> SessionInputData<'_> { let initiator_addr = self.initiator_addr(); let is_standard_payment = self.is_standard_payment(); match self { @@ -303,6 +337,9 @@ impl MetaTransaction { ); SessionInputData::DeploySessionData { data } } + MetaTransaction::Evm(_) => { + unreachable!("EVM transactions do not have Casper session input data") + } MetaTransaction::V1(v1) => { let data = SessionDataV1::new( v1.args().as_named().expect("V1 wasm args should be named and validated at the transaction acceptor level"), @@ -321,7 +358,7 @@ impl MetaTransaction { } /// Returns the `SessionInputData` for a payment code if present. - pub(crate) fn to_payment_input_data(&self) -> SessionInputData { + pub(crate) fn to_payment_input_data(&self) -> SessionInputData<'_> { match self { MetaTransaction::Deploy(meta_deploy) => { let initiator_addr = meta_deploy.initiator_addr(); @@ -336,6 +373,9 @@ impl MetaTransaction { ); SessionInputData::DeploySessionData { data } } + MetaTransaction::Evm(_) => { + unreachable!("EVM transactions do not have Casper payment input data") + } MetaTransaction::V1(v1) => { let initiator_addr = v1.initiator_addr(); @@ -371,6 +411,7 @@ impl MetaTransaction { pub(crate) fn size_estimate(&self) -> usize { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().serialized_length(), + MetaTransaction::Evm(evm) => evm.serialized_length(), MetaTransaction::V1(v1) => v1.serialized_length(), } } @@ -378,6 +419,7 @@ impl MetaTransaction { pub(crate) fn is_v1_wasm(&self) -> bool { match self { MetaTransaction::Deploy(_) => true, + MetaTransaction::Evm(_) => false, MetaTransaction::V1(v1) => v1.is_v1_wasm(), } } @@ -385,6 +427,7 @@ impl MetaTransaction { pub(crate) fn is_v2_wasm(&self) -> bool { match self { MetaTransaction::Deploy(_) => false, + MetaTransaction::Evm(_) => false, MetaTransaction::V1(v1) => v1.is_v2_wasm(), } } @@ -392,6 +435,7 @@ impl MetaTransaction { pub(crate) fn seed(&self) -> Option<[u8; 32]> { match self { MetaTransaction::Deploy(_) => None, + MetaTransaction::Evm(_) => None, MetaTransaction::V1(v1) => v1.seed(), } } @@ -399,6 +443,7 @@ impl MetaTransaction { pub(crate) fn is_install_or_upgrade(&self) -> bool { match self { MetaTransaction::Deploy(_) => false, + MetaTransaction::Evm(_) => false, MetaTransaction::V1(meta_transaction_v1) => { meta_transaction_v1.lane_id() == INSTALL_UPGRADE_LANE_ID } @@ -408,6 +453,7 @@ impl MetaTransaction { pub(crate) fn transferred_value(&self) -> Option { match self { MetaTransaction::Deploy(_) => None, + MetaTransaction::Evm(_) => None, MetaTransaction::V1(v1) => Some(v1.transferred_value()), } } @@ -415,15 +461,24 @@ impl MetaTransaction { pub(crate) fn target(&self) -> Option { match self { MetaTransaction::Deploy(_) => None, + MetaTransaction::Evm(_) => None, MetaTransaction::V1(v1) => Some(v1.target().clone()), } } + + pub(crate) fn as_evm(&self) -> Option<&evm::Transaction> { + match self { + MetaTransaction::Evm(evm) => Some(evm.transaction()), + _ => None, + } + } } impl Display for MetaTransaction { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { match self { MetaTransaction::Deploy(meta_deploy) => Display::fmt(meta_deploy.deploy(), formatter), + MetaTransaction::Evm(evm) => Display::fmt(evm, formatter), MetaTransaction::V1(txn) => Display::fmt(txn, formatter), } } @@ -446,6 +501,14 @@ pub(crate) fn calculate_transaction_lane_for_transaction( )?; Ok(meta.transaction_lane()) } + Transaction::Evm(_) => { + let meta = MetaTransaction::from_transaction( + transaction, + chainspec.core_config.pricing_handling, + &chainspec.transaction_config, + )?; + Ok(meta.transaction_lane()) + } Transaction::V1(v1) => { let args_binary_len = v1 .payload() @@ -473,11 +536,277 @@ pub(crate) fn calculate_transaction_lane_for_transaction( ) .map_err(InvalidTransaction::V1) } - Transaction::Evm(_) => Err(InvalidTransaction::Evm( - casper_types::evm::TransactionError::Decode( - "EVM transactions do not use Casper transaction lanes".to_string(), - ), - )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope, TxLegacy}; + use alloy_eips::eip2718::Encodable2718; + use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, U256}; + use casper_types::TransactionLaneDefinition; + + const CHAIN_ID: u64 = 7; + const BASE_FEE: u64 = 1_000_000; + const EVM_LANE: u8 = 4; + + #[test] + fn evm_from_transaction_exposes_metadata() { + let chainspec = chainspec(); + let evm_transaction = legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000); + let transaction = Transaction::Evm(evm_transaction.clone()); + let meta = MetaTransaction::from_transaction( + &transaction, + chainspec.core_config.pricing_handling, + &chainspec.transaction_config, + ) + .expect("EVM transaction metadata should be created"); + + assert_eq!(meta.hash(), transaction.hash()); + assert_eq!(meta.timestamp(), evm_transaction.timestamp()); + assert_eq!(meta.ttl(), evm_transaction.ttl()); + assert_eq!(meta.approvals(), evm_transaction.approvals().clone()); + assert_eq!( + meta.initiator_addr(), + &InitiatorAddr::EvmAddress(evm_transaction.from()) + ); + assert_eq!(meta.transaction_lane(), EVM_LANE); + assert_eq!(meta.gas_limit(&chainspec).unwrap(), Gas::new(21_000)); + assert_eq!(meta.gas_price_tolerance().unwrap(), u8::MAX); + assert_eq!(meta.size_estimate(), evm_transaction.serialized_length()); + assert!(meta.is_standard_payment()); + assert!(!meta.is_custom_payment()); + assert!(!meta.is_v1_wasm()); + assert!(!meta.is_v2_wasm()); + assert!(meta.seed().is_none()); + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()) + .expect("valid EVM transaction should be config compliant"); + } + + #[test] + fn evm_from_transaction_requires_lane() { + let mut chainspec = chainspec(); + chainspec + .transaction_config + .transaction_v1_config + .set_wasm_lanes(vec![]); + let transaction = + Transaction::Evm(legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000)); + let error = MetaTransaction::from_transaction( + &transaction, + chainspec.core_config.pricing_handling, + &chainspec.transaction_config, + ) + .expect_err("EVM transaction should need a lane"); + assert!(matches!( + error, + InvalidTransaction::Evm(evm::TransactionError::MissingTransactionLane) + )); + } + + #[test] + fn evm_config_compliance_rejects_disabled_evm() { + let mut chainspec = chainspec(); + chainspec.evm_config.enabled = false; + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::Disabled)) + )); + } + + #[test] + fn evm_config_compliance_rejects_missing_chain_id() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + legacy_transaction(None, BASE_FEE.into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + evm::TransactionError::MissingChainId + )) + )); + } + + #[test] + fn evm_config_compliance_rejects_mismatched_chain_id() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID + 1), BASE_FEE.into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::ChainIdMismatch { + expected: CHAIN_ID, + actual + })) if actual == CHAIN_ID + 1 + )); + } + + #[test] + fn evm_config_compliance_rejects_gas_price_below_base_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID), (BASE_FEE - 1).into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee + })) if gas_price == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + + #[test] + fn evm_config_compliance_rejects_max_fee_below_base_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip1559_transaction(u128::from(BASE_FEE - 1), 0, 60_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee + })) if max_fee_per_gas == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + + #[test] + fn evm_config_compliance_rejects_non_zero_priority_fee() { + let chainspec = chainspec(); + let meta = evm_meta(&chainspec, eip1559_transaction(BASE_FEE.into(), 1, 60_000)); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + evm::TransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas: 1 + } + )) + )); + } + + #[test] + fn evm_config_compliance_rejects_gas_limit_above_block_limit() { + let chainspec = chainspec(); + let gas_limit = chainspec.evm_config.block_gas_limit + 1; + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), gas_limit), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit: actual_gas_limit, + block_gas_limit + })) if actual_gas_limit == gas_limit && block_gas_limit == chainspec.evm_config.block_gas_limit + )); + } + + #[test] + fn evm_config_compliance_rejects_invalid_approval() { + let chainspec = chainspec(); + let approvals = BTreeSet::new(); + let evm_transaction = + legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000).with_approvals(approvals); + let meta = evm_meta(&chainspec, evm_transaction); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + evm::TransactionError::MissingApproval + )) + )); + } + + fn chainspec() -> Chainspec { + let mut chainspec = Chainspec::default(); + chainspec.evm_config.enabled = true; + chainspec.evm_config.chain_id = CHAIN_ID; + chainspec.evm_config.base_fee = BASE_FEE; + chainspec.evm_config.block_gas_limit = 30_000_000; + chainspec + .transaction_config + .transaction_v1_config + .set_wasm_lanes(vec![TransactionLaneDefinition::new( + EVM_LANE, + u64::MAX, + 10_000, + u64::MAX, + 10, + )]); + chainspec + } + + fn evm_meta(chainspec: &Chainspec, evm_transaction: evm::Transaction) -> MetaTransaction { + MetaTransaction::from_transaction( + &Transaction::Evm(evm_transaction), + chainspec.core_config.pricing_handling, + &chainspec.transaction_config, + ) + .expect("EVM transaction metadata should be created") + } + + fn legacy_transaction( + chain_id: Option, + gas_price: u128, + gas_limit: u64, + ) -> evm::Transaction { + // Ethereum legacy transactions are the original, untyped transaction + // envelope. With EIP-155 replay protection they include a chain ID, + // but they still use a single fixed `gas_price` instead of separate + // base-fee and priority-fee fields. + let tx = TxLegacy { + chain_id, + nonce: 0, + gas_price, + gas_limit, + to: TxKind::Call(AlloyAddress::from([1u8; 20])), + value: U256::ZERO, + input: Default::default(), + }; + signed_transaction(tx.into_signed(Signature::test_signature()).into()) + } + + fn eip1559_transaction( + max_fee_per_gas: u128, + max_priority_fee_per_gas: u128, + gas_limit: u64, + ) -> evm::Transaction { + // EIP-1559 transactions are typed dynamic-fee transactions. Casper + // currently accepts this envelope for tooling compatibility, but + // requires `max_priority_fee_per_gas == 0` because transactions are + // not packed by priority fee. + let tx = TxEip1559 { + chain_id: CHAIN_ID, + nonce: 0, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to: TxKind::Call(AlloyAddress::from([1u8; 20])), + value: U256::ZERO, + access_list: Default::default(), + input: Default::default(), + }; + signed_transaction(tx.into_signed(Signature::test_signature()).into()) + } + + fn signed_transaction(envelope: TxEnvelope) -> evm::Transaction { + evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ) + .expect("EVM transaction should decode") } } diff --git a/node/src/types/transaction/meta_transaction/meta_evm.rs b/node/src/types/transaction/meta_transaction/meta_evm.rs new file mode 100644 index 0000000000..2e416a2b44 --- /dev/null +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -0,0 +1,168 @@ +use std::{ + collections::BTreeSet, + fmt::{self, Display, Formatter}, +}; + +use casper_types::{ + bytesrepr::ToBytes, evm, Approval, Chainspec, Digest, Gas, InitiatorAddr, InvalidTransaction, + TimeDiff, Timestamp, TransactionConfig, TransactionHash, +}; +use serde::Serialize; + +/// Metadata extracted from a Casper EVM transaction. +#[derive(Clone, Debug, Serialize)] +pub(crate) struct MetaEvmTransaction { + transaction: evm::Transaction, + initiator_addr: InitiatorAddr, + lane_id: u8, + payload_hash: Digest, +} + +impl MetaEvmTransaction { + pub(crate) fn from_evm_transaction( + transaction: &evm::Transaction, + transaction_config: &TransactionConfig, + ) -> Result { + let lane_id = transaction_config + .transaction_v1_config + .wasm_lanes() + .iter() + .last() + .map(|lane| lane.id()) + .ok_or(evm::TransactionError::MissingTransactionLane)?; + let payload_hash = Digest::hash(transaction.signing_payload()?); + Ok(MetaEvmTransaction { + transaction: transaction.clone(), + initiator_addr: InitiatorAddr::EvmAddress(transaction.from()), + lane_id, + payload_hash, + }) + } + + pub(crate) fn transaction(&self) -> &evm::Transaction { + &self.transaction + } + + pub(crate) fn hash(&self) -> TransactionHash { + TransactionHash::from(self.transaction.hash()) + } + + pub(crate) fn timestamp(&self) -> Timestamp { + self.transaction.timestamp() + } + + pub(crate) fn ttl(&self) -> TimeDiff { + self.transaction.ttl() + } + + pub(crate) fn approvals(&self) -> &BTreeSet { + self.transaction.approvals() + } + + pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { + &self.initiator_addr + } + + pub(crate) fn lane_id(&self) -> u8 { + self.lane_id + } + + pub(crate) fn gas_limit(&self) -> Gas { + Gas::new(self.transaction.gas_limit()) + } + + pub(crate) fn gas_price_tolerance(&self) -> u8 { + u8::MAX + } + + pub(crate) fn serialized_length(&self) -> usize { + self.transaction.serialized_length() + } + + pub(crate) fn payload_hash(&self) -> Digest { + self.payload_hash + } + + pub(crate) fn verify(&self) -> Result<(), evm::TransactionError> { + self.transaction.verify() + } + + pub(crate) fn is_config_compliant( + &self, + chainspec: &Chainspec, + ) -> Result<(), evm::TransactionError> { + let transaction = &self.transaction; + let evm_config = &chainspec.evm_config; + if !evm_config.enabled { + return Err(evm::TransactionError::Disabled); + } + + transaction.verify()?; + + let expected = evm_config.chain_id; + let actual = transaction + .chain_id() + .ok_or(evm::TransactionError::MissingChainId)?; + if actual != expected { + return Err(evm::TransactionError::ChainIdMismatch { expected, actual }); + } + + let gas_limit = transaction.gas_limit(); + let block_gas_limit = evm_config.block_gas_limit; + if gas_limit > block_gas_limit { + return Err(evm::TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit, + block_gas_limit, + }); + } + + let base_fee = u128::from(evm_config.base_fee); + match transaction.kind() { + evm::TransactionKind::Legacy | evm::TransactionKind::Eip2930 => { + let gas_price = transaction + .gas_price() + .ok_or(evm::TransactionError::MissingGasPrice)?; + if gas_price < base_fee { + return Err(evm::TransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee, + }); + } + } + evm::TransactionKind::Eip1559 => { + // `max_fee_per_gas` is still meaningful on Casper as the user's + // EIP-1559 total price cap. It must at least cover the configured + // EVM base fee; with the priority fee forced to zero below, this + // cap is what lets Ethereum tooling submit type-2 transactions + // without implying transaction priority based on gas parameters. + let max_fee_per_gas = transaction.max_fee_per_gas(); + if max_fee_per_gas < base_fee { + return Err(evm::TransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee, + }); + } + let max_priority_fee_per_gas = transaction.max_priority_fee_per_gas().unwrap_or(0); + if max_priority_fee_per_gas != 0 { + // Casper does not currently prioritize transactions based + // on transaction gas parameters. Accepting a non-zero + // EIP-1559 priority fee would charge users for a priority + // signal that the node does not honor, so this prototype + // only accepts EIP-1559 as a max-fee compatibility + // envelope with zero priority fee. + return Err(evm::TransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas, + }); + } + } + } + + Ok(()) + } +} + +impl Display for MetaEvmTransaction { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.transaction, formatter) + } +} diff --git a/node/src/types/transaction/meta_transaction/transaction_header.rs b/node/src/types/transaction/meta_transaction/transaction_header.rs index 5d6a3e2b51..71be97b7a7 100644 --- a/node/src/types/transaction/meta_transaction/transaction_header.rs +++ b/node/src/types/transaction/meta_transaction/transaction_header.rs @@ -77,7 +77,7 @@ impl From<&Transaction> for TransactionHeader { match transaction { Transaction::Deploy(deploy) => deploy.header().clone().into(), Transaction::V1(v1) => v1.into(), - Transaction::Evm(evm) => evm.as_ref().into(), + Transaction::Evm(evm) => evm.into(), } } } diff --git a/resources/integration-test/chainspec.toml b/resources/integration-test/chainspec.toml index 9d2bebfbb6..b21d9624c9 100644 --- a/resources/integration-test/chainspec.toml +++ b/resources/integration-test/chainspec.toml @@ -517,4 +517,8 @@ enabled = false chain_id = 1_129_533_444 spec = 'prague' block_gas_limit = 30_000_000 -base_fee = 0 +# Base fee is denominated in motes per EVM gas. At 30,000,000 gas this +# makes a full EVM block cost 30,000 CSPR, which keeps EVM execution +# cheap enough for testing but expensive enough to discourage fill-block +# spam with low-value logs or memory-heavy execution. +base_fee = 1_000_000 diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index 681e2b71d7..8cd8f5f6c4 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -504,9 +504,13 @@ min_gas_price = 1 [evm] enabled = false # EVM chain IDs use 0x435350NN ("CSP" + network namespace) as EIP-155 replay domains. -# Namespaces: mainnet=0x01, testnet=0x02, integration=0x04, devnet=0x05, local=0xFF. +# Namespaces: mainnet=0x01, testnet=0x02, integration=0x03, devnet=0x04, local=0xFF. # All local chainspecs use namespace 0xFF; decimal 1129533695. chain_id = 1_129_533_695 spec = 'prague' block_gas_limit = 30_000_000 -base_fee = 0 +# Base fee is denominated in motes per EVM gas. At 30,000,000 gas this +# makes a full EVM block cost 30,000 CSPR, which keeps EVM execution +# cheap enough for testing but expensive enough to discourage fill-block +# spam with low-value logs or memory-heavy execution. +base_fee = 1_000_000 diff --git a/resources/mainnet/chainspec.toml b/resources/mainnet/chainspec.toml index 7e22a6f1f0..d9ac82c9c6 100644 --- a/resources/mainnet/chainspec.toml +++ b/resources/mainnet/chainspec.toml @@ -517,4 +517,8 @@ enabled = false chain_id = 1_129_533_441 spec = 'prague' block_gas_limit = 30_000_000 -base_fee = 0 +# Base fee is denominated in motes per EVM gas. At 30,000,000 gas this +# makes a full EVM block cost 30,000 CSPR, which keeps EVM execution +# cheap enough for testing but expensive enough to discourage fill-block +# spam with low-value logs or memory-heavy execution. +base_fee = 1_000_000 diff --git a/resources/production/chainspec.toml b/resources/production/chainspec.toml index fe98a0ea34..ff0bda27d1 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -516,4 +516,8 @@ enabled = false chain_id = 1_129_533_441 spec = 'prague' block_gas_limit = 30_000_000 -base_fee = 0 +# Base fee is denominated in motes per EVM gas. At 30,000,000 gas this +# makes a full EVM block cost 30,000 CSPR, which keeps EVM execution +# cheap enough for testing but expensive enough to discourage fill-block +# spam with low-value logs or memory-heavy execution. +base_fee = 1_000_000 diff --git a/resources/testnet/chainspec.toml b/resources/testnet/chainspec.toml index 01148de81a..78daf1613e 100644 --- a/resources/testnet/chainspec.toml +++ b/resources/testnet/chainspec.toml @@ -519,4 +519,8 @@ enabled = false chain_id = 1_129_533_442 spec = 'prague' block_gas_limit = 30_000_000 -base_fee = 0 +# Base fee is denominated in motes per EVM gas. At 30,000,000 gas this +# makes a full EVM block cost 30,000 CSPR, which keeps EVM execution +# cheap enough for testing but expensive enough to discourage fill-block +# spam with low-value logs or memory-heavy execution. +base_fee = 1_000_000 diff --git a/smart_contracts/contract/src/no_std_handlers.rs b/smart_contracts/contract/src/no_std_handlers.rs index e1298375b8..1798593b92 100644 --- a/smart_contracts/contract/src/no_std_handlers.rs +++ b/smart_contracts/contract/src/no_std_handlers.rs @@ -2,7 +2,6 @@ /// A panic handler for use in a `no_std` environment which simply aborts the process. #[panic_handler] -#[no_mangle] pub fn panic(_info: &core::panic::PanicInfo) -> ! { #[cfg(feature = "test-support")] crate::contract_api::runtime::print(&alloc::format!("{_info}")); @@ -12,7 +11,6 @@ pub fn panic(_info: &core::panic::PanicInfo) -> ! { /// An out-of-memory allocation error handler for use in a `no_std` environment which simply aborts /// the process. #[alloc_error_handler] -#[no_mangle] pub fn oom(_: core::alloc::Layout) -> ! { core::intrinsics::abort(); } diff --git a/smart_contracts/rust-toolchain b/smart_contracts/rust-toolchain index dacd12aede..a733a6e85d 100644 --- a/smart_contracts/rust-toolchain +++ b/smart_contracts/rust-toolchain @@ -1 +1 @@ -nightly-2025-02-16 \ No newline at end of file +nightly-2025-08-28 diff --git a/storage/src/system/genesis/account_contract_installer.rs b/storage/src/system/genesis/account_contract_installer.rs index b2fc8afafc..22f81309a0 100644 --- a/storage/src/system/genesis/account_contract_installer.rs +++ b/storage/src/system/genesis/account_contract_installer.rs @@ -27,6 +27,7 @@ use casper_types::{ ContractHash, ContractPackage, ContractPackageHash, ContractPackageStatus, ContractVersions, DisabledVersions, NamedKeys, }, + evm, execution::Effects, system::{ auction::{ @@ -625,6 +626,7 @@ where )); self.tracking_copy.borrow_mut().write(key, stored_value); + self.maybe_create_evm_account(&account, main_purse); total_supply += account.balance().value(); } @@ -640,6 +642,19 @@ where Ok(sustain_purse) } + fn maybe_create_evm_account(&self, account: &GenesisAccount, main_purse: URef) { + if !self.config.enable_evm() { + return; + } + let Some(address) = evm::Address::from_public_key(&account.public_key()) else { + return; + }; + self.tracking_copy.borrow_mut().write( + Key::EvmAccount(address), + StoredValue::EvmAccount(evm::Account::new(0, evm::EMPTY_CODE_HASH, main_purse)), + ); + } + fn initial_seigniorage_recipients( &self, staked: &Staking, diff --git a/storage/src/system/genesis/entity_installer.rs b/storage/src/system/genesis/entity_installer.rs index db6db59976..018ef5e5df 100644 --- a/storage/src/system/genesis/entity_installer.rs +++ b/storage/src/system/genesis/entity_installer.rs @@ -18,6 +18,7 @@ use casper_types::{ ActionThresholds, EntityKindTag, MessageTopics, NamedKeyAddr, NamedKeyValue, }, contracts::NamedKeys, + evm, execution::Effects, system::{ auction, @@ -586,6 +587,7 @@ where None, main_purse, )?; + self.maybe_create_evm_account(&account, main_purse); total_supply += account_starting_balance; } @@ -601,6 +603,19 @@ where Ok(()) } + fn maybe_create_evm_account(&self, account: &GenesisAccount, main_purse: URef) { + if !self.config.enable_evm() { + return; + } + let Some(address) = evm::Address::from_public_key(&account.public_key()) else { + return; + }; + self.tracking_copy.borrow_mut().write( + Key::EvmAccount(address), + StoredValue::EvmAccount(evm::Account::new(0, evm::EMPTY_CODE_HASH, main_purse)), + ); + } + fn initial_seigniorage_recipients( &self, staked: &Staking, diff --git a/storage/src/system/transfer.rs b/storage/src/system/transfer.rs index ce2634e9c6..2b172138b9 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -419,24 +419,23 @@ impl TransferRuntimeArgsBuilder { where R: StateReader, { - let (to, target) = match self - .resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))? - { - TransferTargetMode::ExistingAccount { - main_purse: purse_uref, - target_account_hash: target_account, - } => (Some(target_account), purse_uref), - TransferTargetMode::PurseExists { - target_account_hash, - purse_uref, - } => (target_account_hash, purse_uref), - TransferTargetMode::CreateAccount(_) => { - // Method "build()" is called after `resolve_transfer_target_mode` is first called - // and handled by creating a new account. Calling `resolve_transfer_target_mode` - // for the second time should never return `CreateAccount` variant. - return Err(TransferError::InvalidOperation); - } - }; + let (to, target) = + match self.resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))? { + TransferTargetMode::ExistingAccount { + main_purse: purse_uref, + target_account_hash: target_account, + } => (Some(target_account), purse_uref), + TransferTargetMode::PurseExists { + target_account_hash, + purse_uref, + } => (target_account_hash, purse_uref), + TransferTargetMode::CreateAccount(_) => { + // Method "build()" is called after `resolve_transfer_target_mode` is first called + // and handled by creating a new account. Calling `resolve_transfer_target_mode` + // for the second time should never return `CreateAccount` variant. + return Err(TransferError::InvalidOperation); + } + }; let source = self.resolve_source_uref(from, Rc::clone(&tracking_copy))?; diff --git a/types/Cargo.toml b/types/Cargo.toml index 47b5563a53..3b0cc8f1ba 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -49,10 +49,9 @@ uint = { version = "0.9.0", default-features = false } untrusted = { version = "0.7.1", optional = true } derive_more = "0.99.17" version-sync = { version = "0.9", optional = true } -alloy-consensus = { version = "=1.0.22", default-features = false, features = ["k256"] } -alloy-primitives = { version = "=1.2.0", default-features = false, features = ["rlp", "sha3-keccak"] } -alloy-eips = { version = "=1.0.22", default-features = false, features = ["k256"] } -alloy-tx-macros = { version = "=1.0.22", default-features = false } +alloy-consensus = { version = "=1.0.41", default-features = false, features = ["k256"] } +alloy-primitives = { version = "=1.5.7", default-features = false, features = ["rlp", "sha3-keccak"] } +alloy-eips = { version = "=1.4.2", default-features = false, features = ["k256"] } [dev-dependencies] base16 = { version = "0.2.1", features = ["std"] } diff --git a/types/src/chainspec/genesis_config.rs b/types/src/chainspec/genesis_config.rs index 9721ace5dd..8573f36e8c 100644 --- a/types/src/chainspec/genesis_config.rs +++ b/types/src/chainspec/genesis_config.rs @@ -34,6 +34,7 @@ pub struct GenesisConfig { gas_hold_balance_handling: HoldBalanceHandling, gas_hold_interval_millis: u64, enable_addressable_entity: bool, + enable_evm: bool, rewards_ratio: Option>, storage_costs: StorageCosts, minimum_delegation_rate: DelegationRate, @@ -72,6 +73,7 @@ impl GenesisConfig { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity, + enable_evm: false, rewards_ratio: rewards_handling, storage_costs, minimum_delegation_rate, @@ -170,6 +172,11 @@ impl GenesisConfig { self.enable_addressable_entity } + /// Returns whether EVM genesis account records should be installed. + pub fn enable_evm(&self) -> bool { + self.enable_evm + } + /// Set enable entity. pub fn set_enable_entity(&mut self, enable: bool) { self.enable_addressable_entity = enable @@ -245,6 +252,7 @@ impl Distribution for Standard { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity: false, + enable_evm: false, rewards_ratio: None, storage_costs, minimum_delegation_rate, @@ -279,6 +287,7 @@ impl From<&Chainspec> for GenesisConfig { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity: chainspec.core_config.enable_addressable_entity, + enable_evm: chainspec.evm_config.enabled, rewards_ratio, storage_costs, minimum_delegation_rate: chainspec.core_config.minimum_delegation_rate, diff --git a/types/src/evm.rs b/types/src/evm.rs index d6ac08b7cc..c706c17a91 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -12,7 +12,9 @@ mod hash; mod receipt; mod transaction; -pub use account::{deterministic_purse, Account, ByteCode, StorageAddr, StorageValue}; +pub use account::{ + deterministic_purse, Account, ByteCode, StorageAddr, StorageValue, EMPTY_CODE_HASH, +}; pub use address::{Address, ADDRESS_LENGTH}; pub use config::{EvmConfig, EvmSpec}; pub use hash::{Hash, HASH_LENGTH}; diff --git a/types/src/evm/account.rs b/types/src/evm/account.rs index 51e8be1564..d103782eeb 100644 --- a/types/src/evm/account.rs +++ b/types/src/evm/account.rs @@ -12,6 +12,12 @@ use crate::{ Digest, URef, }; +/// Keccak-256 hash of empty EVM bytecode. +pub const EMPTY_CODE_HASH: Hash = Hash::new([ + 0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0, + 0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70, +]); + /// EVM account metadata stored in global state. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] diff --git a/types/src/evm/address.rs b/types/src/evm/address.rs index 96cf507059..514951c715 100644 --- a/types/src/evm/address.rs +++ b/types/src/evm/address.rs @@ -10,7 +10,12 @@ use datasize::DataSize; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::bytesrepr::{self, FromBytes, ToBytes}; +use alloy_primitives::keccak256; + +use crate::{ + bytesrepr::{self, FromBytes, ToBytes}, + PublicKey, +}; /// The number of bytes in an EVM address. pub const ADDRESS_LENGTH: usize = 20; @@ -48,6 +53,23 @@ impl Address { pub fn to_hex_string(self) -> String { base16::encode_lower(&self.0) } + + /// Returns the Ethereum address for a secp256k1 public key. + /// + /// Ethereum addresses are the low 20 bytes of the Keccak-256 hash of the + /// uncompressed secp256k1 public key without its SEC1 prefix byte. + /// Non-secp256k1 Casper keys do not have an EVM-native address. + pub fn from_public_key(public_key: &PublicKey) -> Option { + let PublicKey::Secp256k1(public_key) = public_key else { + return None; + }; + let encoded = public_key.to_encoded_point(false); + let bytes = encoded.as_bytes(); + let digest = keccak256(&bytes[1..]); + let mut address = [0u8; ADDRESS_LENGTH]; + address.copy_from_slice(&digest.as_slice()[digest.len() - ADDRESS_LENGTH..]); + Some(Address::new(address)) + } } impl AsRef<[u8]> for Address { diff --git a/types/src/evm/hash.rs b/types/src/evm/hash.rs index d58707a024..1fbc58b68f 100644 --- a/types/src/evm/hash.rs +++ b/types/src/evm/hash.rs @@ -10,43 +10,44 @@ use datasize::DataSize; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::bytesrepr::{self, FromBytes, ToBytes}; +use crate::{ + bytesrepr::{self, FromBytes, ToBytes}, + Digest, +}; /// The number of bytes in an EVM 256-bit hash or word. pub const HASH_LENGTH: usize = 32; -const HASH_SERIALIZED_LENGTH: usize = HASH_LENGTH; - /// A 32-byte EVM hash or storage word. #[derive( Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, )] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -pub struct Hash([u8; HASH_LENGTH]); +pub struct Hash(Digest); impl Hash { /// The zero hash. - pub const ZERO: Hash = Hash([0; HASH_LENGTH]); + pub const ZERO: Hash = Hash(Digest::from_raw([0; HASH_LENGTH])); /// Creates a hash from raw bytes. pub const fn new(bytes: [u8; HASH_LENGTH]) -> Self { - Hash(bytes) + Hash(Digest::from_raw(bytes)) } /// Returns the raw bytes backing this hash. - pub const fn value(self) -> [u8; HASH_LENGTH] { - self.0 + pub fn value(self) -> [u8; HASH_LENGTH] { + self.0.value() } /// Returns the raw bytes backing this hash by reference. - pub const fn as_bytes(&self) -> &[u8; HASH_LENGTH] { - &self.0 + pub fn as_bytes(&self) -> &[u8; HASH_LENGTH] { + <&[u8; HASH_LENGTH]>::try_from(self.0.as_ref()).expect("digest length is 32 bytes") } /// Returns `true` when all bytes are zero. pub fn is_zero(&self) -> bool { - self.0.iter().all(|byte| *byte == 0) + self.0.as_ref().iter().all(|byte| *byte == 0) } /// Returns a lower-case hexadecimal string without a `0x` prefix. @@ -57,7 +58,7 @@ impl Hash { impl AsRef<[u8]> for Hash { fn as_ref(&self) -> &[u8] { - &self.0 + self.0.as_ref() } } @@ -69,26 +70,20 @@ impl Display for Hash { impl ToBytes for Hash { fn to_bytes(&self) -> Result, bytesrepr::Error> { - Ok(self.0.to_vec()) + self.0.to_bytes() } fn serialized_length(&self) -> usize { - HASH_SERIALIZED_LENGTH + self.0.serialized_length() } fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - writer.extend_from_slice(&self.0); - Ok(()) + self.0.write_bytes(writer) } } impl FromBytes for Hash { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - if bytes.len() < HASH_LENGTH { - return Err(bytesrepr::Error::EarlyEndOfStream); - } - let (hash, remainder) = bytes.split_at(HASH_LENGTH); - let hash = <[u8; HASH_LENGTH]>::try_from(hash).map_err(|_| bytesrepr::Error::Formatting)?; - Ok((Hash(hash), remainder)) + Digest::from_bytes(bytes).map(|(digest, remainder)| (Hash(digest), remainder)) } } diff --git a/types/src/evm/receipt.rs b/types/src/evm/receipt.rs index 735d6c2fe8..ffad0e7684 100644 --- a/types/src/evm/receipt.rs +++ b/types/src/evm/receipt.rs @@ -389,8 +389,39 @@ pub struct Log { /// Contract address that emitted the log. pub address: Address, /// Indexed log topics. + /// + /// The EVM supports at most four topics per log because bytecode emits + /// logs with the `LOG0` through `LOG4` opcodes. For a non-anonymous + /// Solidity event, `topics[0]` is the full 32-byte Keccak-256 hash of + /// the canonical event signature such as `Transfer(address,address,uint256)`. + /// Indexed event arguments are ABI-encoded into the following topics. + /// Anonymous Solidity events omit the signature topic, allowing all four + /// topics to hold indexed arguments. pub topics: Vec, - /// Unindexed log data. + /// ABI-encoded unindexed log data. + /// + /// This contains the event arguments that are not marked `indexed`, + /// including ABI offsets and lengths for dynamic values. The type does + /// not impose a fixed per-log byte limit; effective size is bounded by + /// transaction gas, block gas, EVM memory expansion, and the EVM log-data + /// gas cost. With the current Casper EVM `block_gas_limit` of 30,000,000 + /// and revm's Ethereum gas schedule, the artificial best-case bound is: + /// + /// ```text + /// log_gas = 375 + 375 * topics + 8 * bytes + memory_gas(bytes) + /// memory_gas(bytes) = 3 * words + floor(words * words / 512) + /// words = ceil(bytes / 32) + /// ``` + /// + /// There is no separate configured EVM memory cap here; memory is bounded + /// by gas. If one `LOG0` spent the whole 30,000,000 gas budget expanding + /// memory from zero and emitting data, the largest data payload would be + /// 2,376,064 bytes, or 74,252 32-byte memory words, costing 29,999,923 + /// gas. For `LOG4`, the same calculation gives 2,375,968 bytes, or + /// 74,249 words, costing 29,999,776 gas. Both are about 2.27 MiB. Real + /// contracts have lower practical limits because they also spend gas on + /// transaction intrinsic cost, code, stack setup, memory writes, control + /// flow, and any surrounding state changes. pub data: Bytes, } @@ -440,7 +471,13 @@ pub struct Receipt { pub status: ReceiptStatus, /// Gas consumed by EVM execution. pub gas_used: u64, - /// Effective gas price used for Ethereum receipt projection. + /// Effective gas price used for Ethereum receipt projection and Casper + /// EVM fee accounting. + /// + /// For accepted EIP-1559 transactions this is the configured EVM base fee + /// capped by `max_fee_per_gas`, since non-zero priority fees are rejected + /// while Casper does not prioritize transactions based on transaction gas + /// parameters. pub effective_gas_price: u128, /// Contract address created by the transaction, if any. pub contract_address: Option
, diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index 398855254b..51e6c3aa4c 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -1,4 +1,5 @@ use alloc::{ + collections::BTreeSet, format, string::{String, ToString}, vec::Vec, @@ -6,12 +7,22 @@ use alloc::{ use core::fmt::{self, Display, Formatter}; use alloy_consensus::{ - transaction::SignerRecoverable, Transaction as AlloyTransaction, TxEnvelope, + constants::{EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID}, + transaction::SignerRecoverable, + SignableTransaction, Transaction as AlloyTransaction, TxEip1559, TxEip2930, TxEnvelope, + TxLegacy, TypedTransaction, +}; +use alloy_eips::{ + eip2718::{Decodable2718, Encodable2718}, + eip2930::AccessList, +}; +use alloy_primitives::{ + keccak256, Address as AlloyAddress, Bytes as AlloyBytes, Signature as AlloySignature, + TxKind as AlloyTxKind, B256, U256 as AlloyU256, }; -use alloy_eips::eip2718::{Decodable2718, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID}; -use alloy_primitives::{Address as AlloyAddress, TxKind as AlloyTxKind, B256}; #[cfg(feature = "datasize")] use datasize::DataSize; +use k256::ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey}; #[cfg(any(feature = "testing", test))] use rand::Rng; #[cfg(feature = "json-schema")] @@ -20,12 +31,13 @@ use schemars::JsonSchema; use serde::{de, Deserializer, Serializer}; use serde::{Deserialize, Serialize}; -use super::{Address, Hash, HASH_LENGTH}; +use super::{Address, EvmConfig, Hash, HASH_LENGTH}; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; use crate::{ - bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - TimeDiff, Timestamp, + bytesrepr::{self, Bytes, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, + Approval, AsymmetricType, Digest, PublicKey, SecretKey, Signature, TimeDiff, Timestamp, U256, + U512, }; const TRANSACTION_KIND_SERIALIZED_LENGTH: usize = U8_SERIALIZED_LENGTH; @@ -42,33 +54,43 @@ pub const EIP7702_TRANSACTION_TYPE_ID: u8 = EIP7702_TX_TYPE_ID; )] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -pub struct TransactionHash(Hash); +pub struct TransactionHash(Digest); impl TransactionHash { - /// Creates a transaction hash from raw bytes. - pub const fn new(bytes: [u8; HASH_LENGTH]) -> Self { - TransactionHash(Hash::new(bytes)) + /// Creates a transaction hash from a raw digest. + pub const fn new(hash: Digest) -> Self { + TransactionHash(hash) + } + + /// Returns a new `TransactionHash` directly initialized with the provided bytes. + pub const fn from_raw(raw_digest: [u8; HASH_LENGTH]) -> Self { + TransactionHash(Digest::from_raw(raw_digest)) + } + + /// Returns the wrapped inner digest. + pub fn inner(&self) -> &Digest { + &self.0 } /// Returns the wrapped hash. - pub const fn hash(self) -> Hash { - self.0 + pub fn hash(self) -> Hash { + Hash::new(self.0.value()) } /// Returns the raw bytes backing this hash. - pub const fn value(self) -> [u8; HASH_LENGTH] { + pub fn value(self) -> [u8; HASH_LENGTH] { self.0.value() } /// Returns a lower-case hexadecimal string without a `0x` prefix. pub fn to_hex_string(self) -> String { - self.0.to_hex_string() + base16::encode_lower(&self.0) } /// Returns a random EVM transaction hash. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - TransactionHash(Hash::new(rng.gen())) + TransactionHash(Digest::from(rng.gen::<[u8; HASH_LENGTH]>())) } } @@ -80,7 +102,7 @@ impl AsRef<[u8]> for TransactionHash { impl Display for TransactionHash { fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(&self.0, formatter) + write!(formatter, "0x{}", base16::encode_lower(&self.0)) } } @@ -100,11 +122,23 @@ impl ToBytes for TransactionHash { impl FromBytes for TransactionHash { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - Hash::from_bytes(bytes).map(|(hash, remainder)| (TransactionHash(hash), remainder)) + Digest::from_bytes(bytes).map(|(hash, remainder)| (TransactionHash(hash), remainder)) + } +} + +impl From for TransactionHash { + fn from(digest: Digest) -> Self { + TransactionHash(digest) + } +} + +impl From for Digest { + fn from(transaction_hash: TransactionHash) -> Self { + transaction_hash.0 } } -/// The supported Ethereum signed transaction envelope kinds. +/// The supported Ethereum transaction envelope kinds. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] @@ -165,20 +199,74 @@ impl FromBytes for TransactionKind { } } -/// Errors returned while decoding or validating signed EVM transactions. +/// Errors returned while decoding or validating EVM transactions. #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub enum TransactionError { - /// The signed RLP was malformed or not a supported Ethereum envelope. + /// The RLP was malformed or not a supported Ethereum envelope. Decode(String), + /// EVM transactions are disabled in the active chainspec. + Disabled, /// The transaction envelope type is not supported by this first-pass executor. UnsupportedTransactionType(u8), /// The transaction contains an access list, which this first-pass executor does not model. UnsupportedAccessList, + /// A chain ID was required by the transaction envelope but was missing. + MissingChainId, + /// A gas price was required by the transaction envelope but was missing. + MissingGasPrice, + /// No transaction lane is available for packing EVM transactions. + MissingTransactionLane, + /// The transaction chain ID does not match the active chainspec EVM chain ID. + ChainIdMismatch { + /// Expected chainspec EVM chain ID. + expected: u64, + /// Actual transaction chain ID. + actual: u64, + }, + /// The legacy or EIP-2930 gas price is lower than the active block base fee. + GasPriceBelowBaseFee { + /// Transaction gas price. + gas_price: u128, + /// Active block base fee. + base_fee: u128, + }, + /// The EIP-1559 maximum fee per gas is lower than the active block base fee. + MaxFeePerGasBelowBaseFee { + /// Transaction maximum fee per gas. + max_fee_per_gas: u128, + /// Active block base fee. + base_fee: u128, + }, + /// The EIP-1559 maximum priority fee per gas must be zero because Casper + /// does not prioritize transactions based on transaction gas parameters. + NonZeroMaxPriorityFeePerGas { + /// Transaction maximum priority fee per gas. + max_priority_fee_per_gas: u128, + }, + /// The transaction gas limit exceeds the configured EVM block gas limit. + GasLimitExceedsBlockGasLimit { + /// Transaction gas limit. + gas_limit: u64, + /// Configured EVM block gas limit. + block_gas_limit: u64, + }, + /// The transaction does not contain an EVM approval. + MissingApproval, + /// The transaction contains more than one approval. + MultipleApprovals, + /// The approval is not a secp256k1 signature and public key. + NonSecp256k1Approval, + /// The approval signature could not be recovered against the stored payload. + InvalidApprovalSignature, + /// The recovered signer address does not match the stored EVM sender. + SenderMismatch, + /// The reconstructed Ethereum transaction hash does not match the stored hash. + HashMismatch, /// The sender address could not be recovered from the signature. SenderRecovery(String), - /// Re-decoding the raw RLP produced metadata different from this transaction. + /// Reconstructing the signed envelope produced metadata different from this transaction. InconsistentEnvelope, } @@ -188,17 +276,78 @@ impl Display for TransactionError { TransactionError::Decode(error) => { write!(formatter, "EVM transaction decode error: {error}") } + TransactionError::Disabled => formatter.write_str("EVM transactions are disabled"), TransactionError::UnsupportedTransactionType(kind) => { write!(formatter, "unsupported EVM transaction type: {kind}") } TransactionError::UnsupportedAccessList => { formatter.write_str("unsupported EVM transaction access list") } + TransactionError::MissingChainId => formatter.write_str("missing EVM chain ID"), + TransactionError::MissingGasPrice => formatter.write_str("missing EVM gas price"), + TransactionError::MissingTransactionLane => { + formatter.write_str("missing EVM transaction lane") + } + TransactionError::ChainIdMismatch { expected, actual } => { + write!( + formatter, + "EVM chain ID mismatch: expected {expected}, got {actual}" + ) + } + TransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee, + } => { + write!( + formatter, + "EVM gas price {gas_price} is below base fee {base_fee}" + ) + } + TransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee, + } => { + write!( + formatter, + "EVM max fee per gas {max_fee_per_gas} is below base fee {base_fee}" + ) + } + TransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas, + } => { + write!( + formatter, + "EVM max priority fee per gas {max_priority_fee_per_gas} must be zero" + ) + } + TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit, + block_gas_limit, + } => { + write!( + formatter, + "EVM gas limit {gas_limit} exceeds block gas limit {block_gas_limit}" + ) + } + TransactionError::MissingApproval => formatter.write_str("missing EVM approval"), + TransactionError::MultipleApprovals => formatter.write_str("multiple EVM approvals"), + TransactionError::NonSecp256k1Approval => { + formatter.write_str("EVM approval must use secp256k1") + } + TransactionError::InvalidApprovalSignature => { + formatter.write_str("invalid EVM approval signature") + } + TransactionError::SenderMismatch => { + formatter.write_str("EVM approval signer does not match transaction sender") + } + TransactionError::HashMismatch => { + formatter.write_str("EVM transaction hash does not match approval") + } TransactionError::SenderRecovery(error) => { write!(formatter, "EVM transaction sender recovery error: {error}") } TransactionError::InconsistentEnvelope => { - formatter.write_str("EVM transaction fields do not match signed RLP") + formatter.write_str("EVM transaction fields do not match signed envelope") } } } @@ -207,12 +356,11 @@ impl Display for TransactionError { #[cfg(feature = "std")] impl std::error::Error for TransactionError {} -/// A decoded signed Ethereum transaction plus Casper envelope metadata. +/// An unsigned Ethereum transaction payload plus one Ethereum-style Casper approval. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub struct Transaction { - raw_signed_rlp: Vec, timestamp: Timestamp, ttl: TimeDiff, hash: TransactionHash, @@ -221,37 +369,81 @@ pub struct Transaction { to: Option
, nonce: u64, gas_limit: u64, + // Legacy and EIP-2930 transactions use this fixed gas price. gas_price: Option, + // EIP-1559 maximum total price per gas. Under current node rules this + // remains useful as a sender cap, but accepted EIP-1559 transactions must + // set `max_priority_fee_per_gas` to zero. max_fee_per_gas: u128, + // EIP-1559 maximum proposer tip per gas. Casper currently does not + // prioritize transactions based on transaction gas parameters, so node + // config compliance rejects non-zero values. max_priority_fee_per_gas: Option, - value: Hash, + value: U256, input: Vec, chain_id: Option, + approvals: BTreeSet, } #[cfg(any(feature = "std", test))] #[derive(Serialize)] struct TransactionSerHelper<'a> { - raw_signed_rlp: &'a Vec, timestamp: Timestamp, ttl: TimeDiff, + hash: TransactionHash, + from: Address, + kind: TransactionKind, + to: Option
, + nonce: u64, + gas_limit: u64, + gas_price: Option, + max_fee_per_gas: u128, + max_priority_fee_per_gas: Option, + value: U256, + input: &'a Vec, + chain_id: Option, + approvals: &'a BTreeSet, } #[cfg(any(feature = "std", test))] #[derive(Deserialize)] struct TransactionDeserHelper { - raw_signed_rlp: Vec, timestamp: Timestamp, ttl: TimeDiff, + hash: TransactionHash, + from: Address, + kind: TransactionKind, + to: Option
, + nonce: u64, + gas_limit: u64, + gas_price: Option, + max_fee_per_gas: u128, + max_priority_fee_per_gas: Option, + value: U256, + input: Vec, + chain_id: Option, + approvals: BTreeSet, } #[cfg(any(feature = "std", test))] impl Serialize for Transaction { fn serialize(&self, serializer: S) -> Result { TransactionSerHelper { - raw_signed_rlp: &self.raw_signed_rlp, timestamp: self.timestamp, ttl: self.ttl, + hash: self.hash, + from: self.from, + kind: self.kind, + to: self.to, + nonce: self.nonce, + gas_limit: self.gas_limit, + gas_price: self.gas_price, + max_fee_per_gas: self.max_fee_per_gas, + max_priority_fee_per_gas: self.max_priority_fee_per_gas, + value: self.value, + input: &self.input, + chain_id: self.chain_id, + approvals: &self.approvals, } .serialize(serializer) } @@ -261,13 +453,30 @@ impl Serialize for Transaction { impl<'de> Deserialize<'de> for Transaction { fn deserialize>(deserializer: D) -> Result { let helper = TransactionDeserHelper::deserialize(deserializer)?; - Transaction::from_signed_rlp(helper.raw_signed_rlp, helper.timestamp, helper.ttl) - .map_err(de::Error::custom) + let transaction = Transaction { + timestamp: helper.timestamp, + ttl: helper.ttl, + hash: helper.hash, + from: helper.from, + kind: helper.kind, + to: helper.to, + nonce: helper.nonce, + gas_limit: helper.gas_limit, + gas_price: helper.gas_price, + max_fee_per_gas: helper.max_fee_per_gas, + max_priority_fee_per_gas: helper.max_priority_fee_per_gas, + value: helper.value, + input: helper.input, + chain_id: helper.chain_id, + approvals: helper.approvals, + }; + transaction.verify().map_err(de::Error::custom)?; + Ok(transaction) } } impl Transaction { - /// Decodes a signed Ethereum RLP transaction and attaches Casper envelope metadata. + /// Decodes a signed Ethereum RLP transaction into an unsigned payload plus approval. pub fn from_signed_rlp( raw_signed_rlp: Vec, timestamp: Timestamp, @@ -315,10 +524,7 @@ impl Transaction { { return Err(TransactionError::UnsupportedAccessList); } - let from = envelope - .recover_signer() - .map_err(|error| TransactionError::SenderRecovery(format!("{error:?}")))?; - let hash = b256_to_hash(*envelope.tx_hash()); + let kind = if envelope.is_legacy() { TransactionKind::Legacy } else if envelope.is_eip2930() { @@ -334,12 +540,18 @@ impl Transaction { AlloyTxKind::Call(address) => Some(alloy_address_to_address(address)), AlloyTxKind::Create => None, }; + let signature_hash = envelope.signature_hash(); + let approval = approval_from_alloy_signature(envelope.signature(), &signature_hash)?; + let mut approvals = BTreeSet::new(); + approvals.insert(approval); + let from = envelope + .recover_signer() + .map_err(|error| TransactionError::SenderRecovery(format!("{error:?}")))?; Ok(Transaction { - raw_signed_rlp, timestamp, ttl, - hash: TransactionHash(hash), + hash: b256_to_transaction_hash(*envelope.tx_hash()), from: alloy_address_to_address(from), kind, to, @@ -348,26 +560,100 @@ impl Transaction { gas_price: envelope.gas_price(), max_fee_per_gas: envelope.max_fee_per_gas(), max_priority_fee_per_gas: envelope.max_priority_fee_per_gas(), - value: Hash::new(envelope.value().to_be_bytes()), + value: alloy_u256_to_casper(envelope.value()), input: envelope.input().to_vec(), chain_id: envelope.chain_id(), + approvals, }) } - /// Re-decodes the signed RLP and checks that all derived fields still match. + /// Reconstructs the signed Ethereum envelope and validates sender/hash consistency. pub fn verify(&self) -> Result<(), TransactionError> { - let decoded = - Transaction::from_signed_rlp(self.raw_signed_rlp.clone(), self.timestamp, self.ttl)?; - if decoded == *self { - Ok(()) - } else { - Err(TransactionError::InconsistentEnvelope) + let signed = self.signed_envelope()?; + if b256_to_transaction_hash(*signed.tx_hash()) != self.hash { + return Err(TransactionError::HashMismatch); } + let recovered = signed + .recover_signer() + .map_err(|error| TransactionError::SenderRecovery(format!("{error:?}")))?; + if alloy_address_to_address(recovered) != self.from { + return Err(TransactionError::SenderMismatch); + } + Ok(()) + } + + /// Signs the unsigned Ethereum payload with one secp256k1 approval. + /// + /// This recomputes the recovered EVM sender and Ethereum signed + /// transaction hash from the new signature. + pub fn sign(&mut self, secret_key: &SecretKey) { + self.try_sign(secret_key) + .expect("EVM transactions must be signed with a valid secp256k1 key") + } + + /// Attempts to sign the unsigned Ethereum payload with one secp256k1 approval. + pub fn try_sign(&mut self, secret_key: &SecretKey) -> Result<(), TransactionError> { + let SecretKey::Secp256k1(signing_key) = secret_key else { + return Err(TransactionError::NonSecp256k1Approval); + }; + let unsigned = self.unsigned_transaction()?; + let signature_hash = unsigned.signature_hash(); + let (signature, recovery_id) = signing_key + .sign_prehash_recoverable(signature_hash.as_slice()) + .map_err(|_| TransactionError::InvalidApprovalSignature)?; + let mut signature_bytes = [0u8; Signature::SECP256K1_LENGTH]; + signature_bytes.copy_from_slice(signature.to_bytes().as_slice()); + let signature = Signature::secp256k1(signature_bytes) + .map_err(|_| TransactionError::InvalidApprovalSignature)?; + let signer = PublicKey::from(secret_key); + let mut approvals = BTreeSet::new(); + approvals.insert(Approval::new(signer, signature)); + + let recovered_key = + recover_verifying_key(&signature_hash, &signature_bytes, recovery_id.is_y_odd())?; + let alloy_signature = + AlloySignature::from_bytes_and_parity(&signature_bytes, recovery_id.is_y_odd()); + let signed = unsigned.into_envelope(alloy_signature); + + self.approvals = approvals; + self.from = evm_address_from_verifying_key(&recovered_key); + self.hash = b256_to_transaction_hash(*signed.tx_hash()); + self.verify() + } + + /// Returns the raw signed Ethereum RLP bytes reconstructed from the approval. + pub fn signed_rlp(&self) -> Result, TransactionError> { + Ok(self.signed_envelope()?.encoded_2718()) + } + + /// Returns the raw signed Ethereum RLP bytes reconstructed from the approval. + pub fn raw_signed_rlp(&self) -> Result, TransactionError> { + self.signed_rlp() + } + + /// Returns the Ethereum signing hash of the unsigned payload. + pub fn signature_hash(&self) -> Result { + Ok(b256_to_hash(self.unsigned_transaction()?.signature_hash())) } - /// Returns the raw signed Ethereum RLP bytes. - pub fn raw_signed_rlp(&self) -> &[u8] { - &self.raw_signed_rlp + /// Returns the bytes Ethereum signs for this unsigned payload. + pub fn signing_payload(&self) -> Result, TransactionError> { + Ok(self.unsigned_transaction()?.encoded_for_signing()) + } + + /// Returns the approvals attached to this transaction. + pub fn approvals(&self) -> &BTreeSet { + &self.approvals + } + + /// Returns this transaction with a replacement approval set. + /// + /// The stored Ethereum transaction hash is intentionally left unchanged; + /// [`Transaction::verify`] rejects replacement approvals that do not + /// reconstruct the same signed Ethereum transaction. + pub fn with_approvals(mut self, approvals: BTreeSet) -> Self { + self.approvals = approvals; + self } /// Returns the Casper envelope timestamp. @@ -390,7 +676,7 @@ impl Transaction { self.from } - /// Returns the signed transaction envelope kind. + /// Returns the transaction envelope kind. pub fn kind(&self) -> TransactionKind { self.kind } @@ -416,17 +702,29 @@ impl Transaction { } /// Returns the maximum fee per gas. + /// + /// For EIP-1559 transactions, this is the sender's cap on the total gas + /// price. Casper accepts EIP-1559 envelopes for tooling compatibility, + /// but currently requires the priority fee to be zero because Casper does + /// not prioritize transactions based on transaction gas parameters. Under + /// those rules, accepted EIP-1559 transactions effectively pay the + /// configured EVM base fee, capped by this value. pub fn max_fee_per_gas(&self) -> u128 { self.max_fee_per_gas } /// Returns the maximum priority fee per gas, if available. + /// + /// This is the EIP-1559 proposer-tip cap. Casper currently rejects + /// non-zero priority fees during node config compliance because EVM + /// transactions are packed using Casper's current transaction ordering + /// policy, not Ethereum-style priority-fee bidding. pub fn max_priority_fee_per_gas(&self) -> Option { self.max_priority_fee_per_gas } - /// Returns the transferred value as a 32-byte big-endian word. - pub fn value(&self) -> Hash { + /// Returns the amount of wei transferred by this transaction. + pub fn value(&self) -> U256 { self.value } @@ -440,6 +738,50 @@ impl Transaction { self.chain_id } + /// Returns the effective gas price at the supplied block base fee. + /// + /// Legacy and EIP-2930 transactions use their signed gas price directly. + /// For EIP-1559, the calculation follows Ethereum's effective price + /// formula: the lower of `max_fee_per_gas` and block base fee plus + /// `max_priority_fee_per_gas`. + /// + /// The node execution path currently rejects non-zero EIP-1559 priority + /// fees during chainspec compliance checks because Casper does not + /// prioritize transactions based on transaction gas parameters. For + /// accepted node transactions, the EIP-1559 effective gas price is + /// therefore the block base fee capped by `max_fee_per_gas`. + pub fn effective_gas_price(&self, base_fee: u64) -> u128 { + match self.kind { + TransactionKind::Legacy | TransactionKind::Eip2930 => { + self.gas_price.unwrap_or(self.max_fee_per_gas) + } + TransactionKind::Eip1559 => { + let max_priority_fee_per_gas = self.max_priority_fee_per_gas.unwrap_or(0); + let priority_fee = self.max_fee_per_gas.saturating_sub(u128::from(base_fee)); + if priority_fee > max_priority_fee_per_gas { + u128::from(base_fee).saturating_add(max_priority_fee_per_gas) + } else { + self.max_fee_per_gas + } + } + } + } + + /// Returns the fee amount for `gas_used` under the supplied EVM config. + pub fn fee_amount(&self, gas_used: u64, evm_config: &EvmConfig) -> Option { + U512::from(gas_used).checked_mul(U512::from(self.effective_gas_price(evm_config.base_fee))) + } + + /// Returns the maximum fee amount this transaction can consume. + pub fn max_fee_amount(&self, evm_config: &EvmConfig) -> Option { + self.fee_amount(self.gas_limit, evm_config) + } + + /// Returns the balance needed for value transfer plus the supplied fee amount. + pub fn required_balance(&self, fee_amount: U512) -> Option { + fee_amount.checked_add(U512::from(self.value)) + } + /// Returns `true` if the transaction has expired at the given timestamp. pub fn expired(&self, current_instant: Timestamp) -> bool { current_instant > self.expires() @@ -449,6 +791,87 @@ impl Transaction { pub fn expires(&self) -> Timestamp { self.timestamp + self.ttl } + + fn signed_envelope(&self) -> Result { + let unsigned = self.unsigned_transaction()?; + let signature_hash = unsigned.signature_hash(); + let (signature, recovered_from) = self.approval_signature(&signature_hash)?; + if recovered_from != self.from { + return Err(TransactionError::SenderMismatch); + } + Ok(unsigned.into_envelope(signature)) + } + + fn unsigned_transaction(&self) -> Result { + let to = match self.to { + Some(address) => AlloyTxKind::Call(to_alloy_address(address)), + None => AlloyTxKind::Create, + }; + let value = casper_u256_to_alloy(self.value); + let input = AlloyBytes::from(self.input.clone()); + match self.kind { + TransactionKind::Legacy => Ok(TypedTransaction::Legacy(TxLegacy { + chain_id: self.chain_id, + nonce: self.nonce, + gas_price: self.gas_price.ok_or(TransactionError::MissingGasPrice)?, + gas_limit: self.gas_limit, + to, + value, + input, + })), + TransactionKind::Eip2930 => Ok(TypedTransaction::Eip2930(TxEip2930 { + chain_id: self.chain_id.ok_or(TransactionError::MissingChainId)?, + nonce: self.nonce, + gas_price: self.gas_price.ok_or(TransactionError::MissingGasPrice)?, + gas_limit: self.gas_limit, + to, + value, + access_list: AccessList::default(), + input, + })), + TransactionKind::Eip1559 => Ok(TypedTransaction::Eip1559(TxEip1559 { + chain_id: self.chain_id.ok_or(TransactionError::MissingChainId)?, + nonce: self.nonce, + gas_limit: self.gas_limit, + max_fee_per_gas: self.max_fee_per_gas, + max_priority_fee_per_gas: self.max_priority_fee_per_gas.unwrap_or(0), + to, + value, + access_list: AccessList::default(), + input, + })), + } + } + + fn approval_signature( + &self, + signature_hash: &B256, + ) -> Result<(AlloySignature, Address), TransactionError> { + let approval = self.single_approval()?; + let raw_signature = secp256k1_signature_bytes(approval)?; + let expected_signer = approval.signer(); + for y_parity in [false, true] { + let alloy_signature = AlloySignature::from_bytes_and_parity(&raw_signature, y_parity); + let recovered_key = recover_verifying_key(signature_hash, &raw_signature, y_parity)?; + let recovered_public_key = public_key_from_verifying_key(&recovered_key)?; + if &recovered_public_key == expected_signer { + return Ok(( + alloy_signature, + evm_address_from_verifying_key(&recovered_key), + )); + } + } + Err(TransactionError::InvalidApprovalSignature) + } + + fn single_approval(&self) -> Result<&Approval, TransactionError> { + let mut approvals = self.approvals.iter(); + let approval = approvals.next().ok_or(TransactionError::MissingApproval)?; + if approvals.next().is_some() { + return Err(TransactionError::MultipleApprovals); + } + Ok(approval) + } } impl Display for Transaction { @@ -470,29 +893,139 @@ impl ToBytes for Transaction { } fn serialized_length(&self) -> usize { - self.raw_signed_rlp.serialized_length() - + self.timestamp.serialized_length() + self.timestamp.serialized_length() + self.ttl.serialized_length() + + self.hash.serialized_length() + + self.from.serialized_length() + + self.kind.serialized_length() + + self.to.serialized_length() + + self.nonce.serialized_length() + + self.gas_limit.serialized_length() + + self.gas_price.serialized_length() + + self.max_fee_per_gas.serialized_length() + + self.max_priority_fee_per_gas.serialized_length() + + self.value.serialized_length() + + Bytes::from(self.input.clone()).serialized_length() + + self.chain_id.serialized_length() + + self.approvals.serialized_length() } fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - self.raw_signed_rlp.write_bytes(writer)?; self.timestamp.write_bytes(writer)?; - self.ttl.write_bytes(writer) + self.ttl.write_bytes(writer)?; + self.hash.write_bytes(writer)?; + self.from.write_bytes(writer)?; + self.kind.write_bytes(writer)?; + self.to.write_bytes(writer)?; + self.nonce.write_bytes(writer)?; + self.gas_limit.write_bytes(writer)?; + self.gas_price.write_bytes(writer)?; + self.max_fee_per_gas.write_bytes(writer)?; + self.max_priority_fee_per_gas.write_bytes(writer)?; + self.value.write_bytes(writer)?; + Bytes::from(self.input.clone()).write_bytes(writer)?; + self.chain_id.write_bytes(writer)?; + self.approvals.write_bytes(writer) } } impl FromBytes for Transaction { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (raw_signed_rlp, remainder) = Vec::::from_bytes(bytes)?; - let (timestamp, remainder) = Timestamp::from_bytes(remainder)?; + let (timestamp, remainder) = Timestamp::from_bytes(bytes)?; let (ttl, remainder) = TimeDiff::from_bytes(remainder)?; - let transaction = Transaction::from_signed_rlp(raw_signed_rlp, timestamp, ttl) + let (hash, remainder) = TransactionHash::from_bytes(remainder)?; + let (from, remainder) = Address::from_bytes(remainder)?; + let (kind, remainder) = TransactionKind::from_bytes(remainder)?; + let (to, remainder) = Option::
::from_bytes(remainder)?; + let (nonce, remainder) = u64::from_bytes(remainder)?; + let (gas_limit, remainder) = u64::from_bytes(remainder)?; + let (gas_price, remainder) = Option::::from_bytes(remainder)?; + let (max_fee_per_gas, remainder) = u128::from_bytes(remainder)?; + let (max_priority_fee_per_gas, remainder) = Option::::from_bytes(remainder)?; + let (value, remainder) = U256::from_bytes(remainder)?; + let (input, remainder) = Bytes::from_bytes(remainder)?; + let (chain_id, remainder) = Option::::from_bytes(remainder)?; + let (approvals, remainder) = BTreeSet::::from_bytes(remainder)?; + let transaction = Transaction { + timestamp, + ttl, + hash, + from, + kind, + to, + nonce, + gas_limit, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + value, + input: input.into(), + chain_id, + approvals, + }; + transaction + .verify() .map_err(|_| bytesrepr::Error::Formatting)?; Ok((transaction, remainder)) } } +fn approval_from_alloy_signature( + signature: &AlloySignature, + signature_hash: &B256, +) -> Result { + let raw_signature = signature.as_rsy(); + let mut signature_bytes = [0u8; Signature::SECP256K1_LENGTH]; + signature_bytes.copy_from_slice(&raw_signature[..Signature::SECP256K1_LENGTH]); + let recovered_key = recover_verifying_key(signature_hash, &signature_bytes, signature.v())?; + let signer = public_key_from_verifying_key(&recovered_key)?; + let signature = Signature::secp256k1(signature_bytes) + .map_err(|_| TransactionError::InvalidApprovalSignature)?; + Ok(Approval::new(signer, signature)) +} + +fn secp256k1_signature_bytes(approval: &Approval) -> Result<[u8; 64], TransactionError> { + if !matches!(approval.signer(), PublicKey::Secp256k1(_)) + || !matches!(approval.signature(), Signature::Secp256k1(_)) + { + return Err(TransactionError::NonSecp256k1Approval); + } + let signature_bytes = Vec::::from(approval.signature()); + signature_bytes + .try_into() + .map_err(|_| TransactionError::InvalidApprovalSignature) +} + +fn recover_verifying_key( + signature_hash: &B256, + signature_bytes: &[u8; 64], + y_parity: bool, +) -> Result { + let signature = K256Signature::try_from(signature_bytes.as_slice()) + .map_err(|_| TransactionError::InvalidApprovalSignature)?; + let recovery_id = RecoveryId::new(y_parity, false); + VerifyingKey::recover_from_prehash(signature_hash.as_slice(), &signature, recovery_id) + .map_err(|_| TransactionError::InvalidApprovalSignature) +} + +fn public_key_from_verifying_key(key: &VerifyingKey) -> Result { + PublicKey::secp256k1_from_bytes(key.to_encoded_point(true).as_ref()) + .map_err(|_| TransactionError::InvalidApprovalSignature) +} + +fn evm_address_from_verifying_key(key: &VerifyingKey) -> Address { + let encoded = key.to_encoded_point(false); + let bytes = encoded.as_bytes(); + let digest = keccak256(&bytes[1..]); + let mut address = [0u8; super::ADDRESS_LENGTH]; + address.copy_from_slice(&digest.as_slice()[HASH_LENGTH - super::ADDRESS_LENGTH..]); + Address::new(address) +} + +fn to_alloy_address(address: Address) -> AlloyAddress { + AlloyAddress::from(address.value()) +} + fn alloy_address_to_address(address: AlloyAddress) -> Address { let mut bytes = [0u8; super::ADDRESS_LENGTH]; bytes.copy_from_slice(address.as_slice()); @@ -502,3 +1035,17 @@ fn alloy_address_to_address(address: AlloyAddress) -> Address { fn b256_to_hash(hash: B256) -> Hash { Hash::new(hash.0) } + +fn b256_to_transaction_hash(hash: B256) -> TransactionHash { + TransactionHash::from_raw(hash.0) +} + +fn alloy_u256_to_casper(value: AlloyU256) -> U256 { + U256::from_big_endian(&value.to_be_bytes::<32>()) +} + +fn casper_u256_to_alloy(value: U256) -> AlloyU256 { + let mut bytes = [0u8; 32]; + value.to_big_endian(&mut bytes); + AlloyU256::from_be_slice(&bytes) +} diff --git a/types/src/transaction.rs b/types/src/transaction.rs index 36a96e10a3..9cbd802b45 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -23,7 +23,6 @@ mod transfer_target; #[cfg(feature = "json-schema")] use crate::URef; use alloc::{ - boxed::Box, collections::BTreeSet, string::{String, ToString}, vec::Vec, @@ -150,8 +149,8 @@ pub enum Transaction { schemars(with = "TransactionV1Json") )] V1(TransactionV1), - /// An EVM signed RLP transaction. - Evm(Box), + /// An EVM transaction. + Evm(evm::Transaction), } impl Transaction { @@ -167,7 +166,7 @@ impl Transaction { /// EVM variant ctor. pub fn from_evm(evm: evm::Transaction) -> Self { - Transaction::Evm(Box::new(evm)) + Transaction::Evm(evm) } /// Returns the `TransactionHash` identifying this transaction. @@ -175,7 +174,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => TransactionHash::from(*deploy.hash()), Transaction::V1(txn) => TransactionHash::from(*txn.hash()), - Transaction::Evm(txn) => TransactionHash::from(txn.as_ref().hash()), + Transaction::Evm(txn) => TransactionHash::from(txn.hash()), } } @@ -221,7 +220,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.sign(secret_key), Transaction::V1(v1) => v1.sign(secret_key), - Transaction::Evm(_) => {} + Transaction::Evm(txn) => txn.sign(secret_key), } } @@ -230,7 +229,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.approvals().clone(), Transaction::V1(v1) => v1.approvals().clone(), - Transaction::Evm(_) => BTreeSet::new(), + Transaction::Evm(txn) => txn.approvals().clone(), } } @@ -239,7 +238,7 @@ impl Transaction { let approvals_hash = match self { Transaction::Deploy(deploy) => deploy.compute_approvals_hash()?, Transaction::V1(txn) => txn.compute_approvals_hash()?, - Transaction::Evm(_) => ApprovalsHash::compute(&BTreeSet::new())?, + Transaction::Evm(txn) => ApprovalsHash::compute(txn.approvals())?, }; Ok(approvals_hash) } @@ -296,11 +295,11 @@ impl Transaction { } Transaction::Evm(txn) => { let approvals_hash = - ApprovalsHash::compute(&BTreeSet::new()).unwrap_or_else(|error| { - error!(%error, "failed to serialize empty EVM approvals"); + ApprovalsHash::compute(txn.approvals()).unwrap_or_else(|error| { + error!(%error, "failed to serialize EVM approvals"); ApprovalsHash::from(Digest::default()) }); - TransactionId::new(TransactionHash::Evm(txn.as_ref().hash()), approvals_hash) + TransactionId::new(TransactionHash::Evm(txn.hash()), approvals_hash) } } } @@ -325,7 +324,7 @@ impl Transaction { /// Returns the native EVM transaction hash for an EVM transaction. pub fn evm_hash(&self) -> Option { match self { - Transaction::Evm(txn) => Some(txn.as_ref().hash()), + Transaction::Evm(txn) => Some(txn.hash()), _ => None, } } @@ -361,7 +360,11 @@ impl Transaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), - Transaction::Evm(_) => BTreeSet::new(), + Transaction::Evm(txn) => txn + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), } } @@ -410,7 +413,11 @@ impl Transaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), - Transaction::Evm(_) => BTreeSet::new(), + Transaction::Evm(txn) => txn + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), } } @@ -506,10 +513,15 @@ impl Transaction { .gas_cost(chainspec, lane_id, gas_price) .map_err(InvalidTransaction::from) } - Transaction::Evm(txn) => Ok(Motes::new( - txn.gas_limit() - .saturating_mul(txn.max_fee_per_gas().min(u64::MAX as u128) as u64), - )), + Transaction::Evm(txn) => { + // Use the EIP-1559 max-fee cap for generic upper-bound balance + // checks. Node config compliance separately rejects non-zero + // priority fees, so accepted type-2 transactions do not imply + // transaction priority based on gas parameters. + Ok(Motes::new(txn.gas_limit().saturating_mul( + txn.max_fee_per_gas().min(u64::MAX as u128) as u64, + ))) + } } } @@ -573,8 +585,8 @@ enum TransactionJson { /// A version 1 transaction. #[serde(rename = "Version1")] V1(Box), - /// An EVM signed RLP transaction. - Evm(Box), + /// An EVM transaction. + Evm(evm::Transaction), } #[cfg(any(feature = "std", test))] @@ -658,7 +670,7 @@ impl From for Transaction { impl From for Transaction { fn from(txn: evm::Transaction) -> Self { - Self::Evm(Box::new(txn)) + Self::Evm(txn) } } @@ -710,7 +722,7 @@ impl FromBytes for Transaction { } EVM_TAG => { let (txn, remainder) = evm::Transaction::from_bytes(remainder)?; - Ok((Transaction::Evm(Box::new(txn)), remainder)) + Ok((Transaction::Evm(txn), remainder)) } _ => Err(bytesrepr::Error::Formatting), } diff --git a/types/src/transaction/transaction_hash.rs b/types/src/transaction/transaction_hash.rs index 583c8bbc3b..ac30ce1ca0 100644 --- a/types/src/transaction/transaction_hash.rs +++ b/types/src/transaction/transaction_hash.rs @@ -45,7 +45,7 @@ impl TransactionHash { match self { TransactionHash::Deploy(deploy_hash) => *deploy_hash.inner(), TransactionHash::V1(transaction_hash) => *transaction_hash.inner(), - TransactionHash::Evm(transaction_hash) => Digest::from_raw(transaction_hash.value()), + TransactionHash::Evm(transaction_hash) => *transaction_hash.inner(), } } diff --git a/types/src/uint.rs b/types/src/uint.rs index ca8d7ab280..26741c1f20 100644 --- a/types/src/uint.rs +++ b/types/src/uint.rs @@ -640,6 +640,14 @@ impl AsPrimitive for U512 { } } +impl From for U512 { + fn from(value: U256) -> Self { + let mut result = U512::zero(); + result.0[..4].clone_from_slice(&value.0[..4]); + result + } +} + #[cfg(test)] mod tests { use std::fmt::Debug; diff --git a/types/tests/evm_transaction.rs b/types/tests/evm_transaction.rs index 89c4296080..ac8204140b 100644 --- a/types/tests/evm_transaction.rs +++ b/types/tests/evm_transaction.rs @@ -1,58 +1,61 @@ -use alloy_consensus::{SignableTransaction, TxEip2930, TxEnvelope}; +use std::collections::BTreeSet; + +use alloy_consensus::{ + crypto::secp256k1, transaction::SignerRecoverable, SignableTransaction, TxEip1559, TxEip2930, + TxEnvelope, TxLegacy, +}; use alloy_eips::{ eip2718::Encodable2718, eip2930::{AccessList, AccessListItem}, }; -use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, B256, U256}; +use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, B256, U256 as AlloyU256}; use casper_types::{ + bytesrepr::{FromBytes, ToBytes}, evm::{ - Address, Hash, Transaction, TransactionError, TransactionKind, EIP4844_TRANSACTION_TYPE_ID, - EIP7702_TRANSACTION_TYPE_ID, + self, Address, Hash, Transaction, TransactionError, TransactionKind, + EIP4844_TRANSACTION_TYPE_ID, EIP7702_TRANSACTION_TYPE_ID, }, - TimeDiff, Timestamp, + Approval, ApprovalsHash, Digest, PublicKey, SecretKey, TimeDiff, Timestamp, + Transaction as CasperTransaction, TransactionHash, U256, }; -use hex_literal::hex; -const SENDER: Address = Address::new(hex!("dceea13df2f85e3a1de99a2f1c119fa6b2296a1e")); +const SIGNING_SECRET: [u8; 32] = [7; 32]; #[test] fn decodes_legacy_signed_rlp() { - let transaction = decode(hex!("f86380843b9aca008252089400000000000000000000000000000000000000017b8031a0f9f5275265b6eb94b3c40777a78b78a6f2271bee070f22baf95cc373241ad424a0455f2ffb56667d638e4c5a3e859de7f0094996cffdfbe0e6b8b5025a600002c9")); + let signed_transaction = signed_legacy_transaction(); + let transaction = decode(signed_transaction.raw_rlp.clone()); assert_eq!(transaction.kind(), TransactionKind::Legacy); - assert_eq!(transaction.from(), SENDER); - assert_eq!( - transaction.to(), - Some(Address::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 - ])) - ); + assert_eq!(transaction.from(), signed_transaction.sender); + assert_eq!(transaction.to(), Some(address(1))); assert_eq!(transaction.nonce(), 0); assert_eq!(transaction.gas_limit(), 21_000); assert_eq!(transaction.gas_price(), Some(1_000_000_000)); - assert_eq!(transaction.value(), word(123)); + assert_eq!(transaction.value(), U256::from(123u64)); assert_eq!(transaction.chain_id(), Some(7)); transaction .verify() .expect("legacy transaction should verify"); + assert_eq!(transaction.approvals().len(), 1); + assert_eq!( + transaction.signed_rlp().unwrap(), + signed_transaction.raw_rlp + ); } #[test] fn decodes_eip2930_signed_rlp() { - let transaction = decode(hex!("01f8690701843b9aca0082c3509400000000000000000000000000000000000000028201c8821234c080a0ae81543cd30ddc7a55203a0df0d0d1182a448754e2569c32c9758db31e324cdfa0422e1fa2e2c7bf80261ccd8b4f9ac8b6c422cf0b09505d406256855156213e92")); + let signed_transaction = signed_eip2930_transaction(); + let transaction = decode(signed_transaction.raw_rlp); assert_eq!(transaction.kind(), TransactionKind::Eip2930); - assert_eq!(transaction.from(), SENDER); - assert_eq!( - transaction.to(), - Some(Address::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 - ])) - ); + assert_eq!(transaction.from(), signed_transaction.sender); + assert_eq!(transaction.to(), Some(address(2))); assert_eq!(transaction.nonce(), 1); assert_eq!(transaction.gas_limit(), 50_000); assert_eq!(transaction.gas_price(), Some(1_000_000_000)); - assert_eq!(transaction.value(), word(456)); + assert_eq!(transaction.value(), U256::from(456u64)); assert_eq!(transaction.input(), &[0x12, 0x34]); assert_eq!(transaction.chain_id(), Some(7)); transaction @@ -62,21 +65,17 @@ fn decodes_eip2930_signed_rlp() { #[test] fn decodes_eip1559_signed_rlp() { - let transaction = decode(hex!("02f86d07028405f5e100847735940082ea6094000000000000000000000000000000000000000382031582abcdc080a0cc943bbac7dcda95bc138f08375a3be9543d34f7aecde0b386dbc0575c9cd5bc9fdfdb2712cdea574dfc83b372c490f705c2ba59421af0a84614d63024787fee")); + let signed_transaction = signed_eip1559_transaction(); + let transaction = decode(signed_transaction.raw_rlp); assert_eq!(transaction.kind(), TransactionKind::Eip1559); - assert_eq!(transaction.from(), SENDER); - assert_eq!( - transaction.to(), - Some(Address::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3 - ])) - ); + assert_eq!(transaction.from(), signed_transaction.sender); + assert_eq!(transaction.to(), Some(address(3))); assert_eq!(transaction.nonce(), 2); assert_eq!(transaction.gas_limit(), 60_000); assert_eq!(transaction.max_fee_per_gas(), 2_000_000_000); assert_eq!(transaction.max_priority_fee_per_gas(), Some(100_000_000)); - assert_eq!(transaction.value(), word(789)); + assert_eq!(transaction.value(), U256::from(789u64)); assert_eq!(transaction.input(), &[0xab, 0xcd]); assert_eq!(transaction.chain_id(), Some(7)); transaction @@ -114,19 +113,233 @@ fn non_empty_access_lists_are_rejected() { ); } -fn decode(bytes: [u8; N]) -> Transaction { - Transaction::from_signed_rlp( - bytes.to_vec(), - Timestamp::zero(), - TimeDiff::from_seconds(60), - ) - .expect("transaction should decode") +#[test] +fn approval_backed_transaction_identity_uses_evm_approval() { + let evm_transaction = decode(signed_eip1559_transaction().raw_rlp); + let transaction = CasperTransaction::from(evm_transaction.clone()); + let approvals_hash = ApprovalsHash::compute(evm_transaction.approvals()).unwrap(); + + assert_eq!(transaction.approvals(), evm_transaction.approvals().clone()); + assert_eq!( + transaction.compute_approvals_hash().unwrap(), + approvals_hash + ); + assert_eq!(transaction.compute_id().approvals_hash(), approvals_hash); + assert_eq!( + transaction.compute_id().transaction_hash(), + TransactionHash::from(evm_transaction.hash()) + ); +} + +#[test] +fn evm_approvals_are_not_replaced_by_finalized_approvals() { + let evm_transaction = decode(signed_eip1559_transaction().raw_rlp); + let transaction = CasperTransaction::from(evm_transaction.clone()); + let secret_key = SecretKey::ed25519_from_bytes([42; SecretKey::ED25519_LENGTH]).unwrap(); + let replacement_approval = + Approval::create(&TransactionHash::from(evm_transaction.hash()), &secret_key); + + assert_eq!( + transaction + .with_approvals(BTreeSet::from([replacement_approval])) + .approvals(), + evm_transaction.approvals().clone() + ); +} + +#[test] +fn evm_transaction_sign_replaces_approval_and_recomputes_identity() { + let mut transaction = CasperTransaction::from(decode(signed_legacy_transaction().raw_rlp)); + let old_hash = transaction.hash(); + let new_secret_key = secp_secret_key([1; SecretKey::SECP256K1_LENGTH]); + let expected_signer = PublicKey::from(&new_secret_key); + + transaction.sign(&new_secret_key); + + let CasperTransaction::Evm(evm_transaction) = transaction else { + panic!("expected EVM transaction"); + }; + assert_eq!(evm_transaction.approvals().len(), 1); + assert_eq!( + evm_transaction.approvals().iter().next().unwrap().signer(), + &expected_signer + ); + assert_ne!(TransactionHash::from(evm_transaction.hash()), old_hash); + evm_transaction + .verify() + .expect("signed transaction should verify"); + + let decoded = decode(evm_transaction.signed_rlp().unwrap()); + assert_eq!(decoded.hash(), evm_transaction.hash()); + assert_eq!(decoded.from(), evm_transaction.from()); + assert_eq!(decoded.approvals(), evm_transaction.approvals()); +} + +#[test] +#[should_panic(expected = "EVM transactions must be signed with a valid secp256k1 key")] +fn evm_transaction_sign_rejects_non_secp256k1_keys() { + let mut transaction = CasperTransaction::from(decode(signed_legacy_transaction().raw_rlp)); + transaction.sign(&ed_secret_key([42; SecretKey::ED25519_LENGTH])); +} + +#[test] +fn evm_approval_verification_rejects_bad_approval_sets() { + let transaction = decode(signed_legacy_transaction().raw_rlp); + assert_eq!( + transaction.clone().with_approvals(BTreeSet::new()).verify(), + Err(TransactionError::MissingApproval) + ); + + let mut multiple_approvals = transaction.approvals().clone(); + multiple_approvals.insert(Approval::create( + &TransactionHash::from(transaction.hash()), + &secp_secret_key([1; SecretKey::SECP256K1_LENGTH]), + )); + assert_eq!( + transaction + .clone() + .with_approvals(multiple_approvals) + .verify(), + Err(TransactionError::MultipleApprovals) + ); + + let non_secp_approval = Approval::create( + &TransactionHash::from(transaction.hash()), + &ed_secret_key([42; SecretKey::ED25519_LENGTH]), + ); + assert_eq!( + transaction + .clone() + .with_approvals(BTreeSet::from([non_secp_approval])) + .verify(), + Err(TransactionError::NonSecp256k1Approval) + ); +} + +#[test] +fn evm_hashes_round_trip_raw_digest_bytes() { + let raw = [0x42; 32]; + let hash = Hash::new(raw); + assert_eq!(hash.value(), raw); + assert_eq!(hash.as_bytes(), &raw); + bytesrepr_roundtrip(&hash); + let serialized = serde_json::to_string(&hash).unwrap(); + let deserialized = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(deserialized, hash); + + let digest = Digest::from_raw(raw); + let transaction_hash = evm::TransactionHash::new(digest); + assert_eq!(transaction_hash.inner(), &digest); + assert_eq!(transaction_hash.value(), raw); + assert_eq!(Digest::from(transaction_hash), digest); + bytesrepr_roundtrip(&transaction_hash); + let serialized = serde_json::to_string(&transaction_hash).unwrap(); + let deserialized = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(deserialized, transaction_hash); +} + +struct SignedTransaction { + raw_rlp: Vec, + sender: Address, +} + +fn decode(bytes: Vec) -> Transaction { + Transaction::from_signed_rlp(bytes, Timestamp::zero(), TimeDiff::from_seconds(60)) + .expect("transaction should decode") +} + +fn bytesrepr_roundtrip(value: &T) +where + T: ToBytes + FromBytes + PartialEq + std::fmt::Debug, +{ + let bytes = value.to_bytes().expect("value should serialize"); + let (decoded, remainder) = T::from_bytes(&bytes).expect("value should deserialize"); + assert!(remainder.is_empty()); + assert_eq!(&decoded, value); +} + +fn signed_legacy_transaction() -> SignedTransaction { + let tx = TxLegacy { + chain_id: Some(7), + nonce: 0, + gas_price: 1_000_000_000, + gas_limit: 21_000, + to: TxKind::Call(alloy_address(1)), + value: AlloyU256::from(123u64), + input: Vec::new().into(), + }; + let signature = sign_transaction(&tx); + signed_transaction(tx.into_signed(signature).into()) +} + +fn signed_eip2930_transaction() -> SignedTransaction { + let tx = TxEip2930 { + chain_id: 7, + nonce: 1, + gas_price: 1_000_000_000, + gas_limit: 50_000, + to: TxKind::Call(alloy_address(2)), + value: AlloyU256::from(456u64), + input: vec![0x12, 0x34].into(), + access_list: AccessList::default(), + }; + let signature = sign_transaction(&tx); + signed_transaction(tx.into_signed(signature).into()) +} + +fn signed_eip1559_transaction() -> SignedTransaction { + let tx = TxEip1559 { + chain_id: 7, + nonce: 2, + gas_limit: 60_000, + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 100_000_000, + to: TxKind::Call(alloy_address(3)), + value: AlloyU256::from(789u64), + access_list: AccessList::default(), + input: vec![0xab, 0xcd].into(), + }; + let signature = sign_transaction(&tx); + signed_transaction(tx.into_signed(signature).into()) +} + +fn signed_transaction(envelope: TxEnvelope) -> SignedTransaction { + let sender = alloy_address_to_address( + envelope + .recover_signer() + .expect("signed transaction should recover sender"), + ); + SignedTransaction { + raw_rlp: envelope.encoded_2718(), + sender, + } +} + +fn sign_transaction>(tx: &T) -> Signature { + secp256k1::sign_message(B256::from(SIGNING_SECRET), tx.signature_hash()) + .expect("transaction signing should succeed") +} + +fn secp_secret_key(bytes: [u8; SecretKey::SECP256K1_LENGTH]) -> SecretKey { + SecretKey::secp256k1_from_bytes(bytes).expect("secp256k1 secret key should be valid") +} + +fn ed_secret_key(bytes: [u8; SecretKey::ED25519_LENGTH]) -> SecretKey { + SecretKey::ed25519_from_bytes(bytes).expect("ed25519 secret key should be valid") +} + +fn address(value: u8) -> Address { + let mut bytes = [0; 20]; + bytes[19] = value; + Address::new(bytes) +} + +fn alloy_address(value: u8) -> AlloyAddress { + AlloyAddress::from(address(value).value()) } -fn word(value: u64) -> Hash { - let mut bytes = [0u8; 32]; - bytes[24..].copy_from_slice(&value.to_be_bytes()); - Hash::new(bytes) +fn alloy_address_to_address(address: AlloyAddress) -> Address { + Address::new(address.into_array()) } fn signed_eip2930_with_access_list() -> Vec { @@ -135,11 +348,11 @@ fn signed_eip2930_with_access_list() -> Vec { nonce: 0, gas_price: 1_000_000_000, gas_limit: 50_000, - to: TxKind::Call(AlloyAddress::from([2u8; 20])), - value: U256::from(456u64), + to: TxKind::Call(alloy_address(2)), + value: AlloyU256::from(456u64), input: vec![0x12, 0x34].into(), access_list: AccessList(vec![AccessListItem { - address: AlloyAddress::from([8u8; 20]), + address: alloy_address(8), storage_keys: vec![B256::from([9u8; 32])], }]), }; From c46145e0dec76959531b32b518fe66a30cf325a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Mon, 11 May 2026 15:28:50 +0200 Subject: [PATCH 06/17] Support native transfers to EVM addresses Allow native transfer targets to be encoded as 20-byte EVM addresses when EVM support is enabled. Resolve those targets to existing EVM account purses or create deterministic EVM account records on first funding, while preserving the native transfer record schema. Stop seeding EVM accounts at genesis and require explicit funding through native transfers. Reject EVM address transfer targets when EVM is disabled, add the binary-port error code, and cover the genesis and transfer behavior with tests. Document the explicit EVM funding flow and add CL typing/schema support for EVM addresses. --- EVM.md | 49 ++++-- binary_port/src/command.rs | 2 +- binary_port/src/error_code.rs | 3 + binary_port/src/type_wrappers.rs | 12 +- .../src/transfer_request_builder.rs | 1 + node/src/components/transaction_acceptor.rs | 14 +- .../components/transaction_acceptor/error.rs | 6 + .../main_reactor/tests/transactions.rs | 162 ++++++++++++++++++ node/src/types/transaction/arg_handling.rs | 66 +++++-- .../meta_transaction/meta_transaction_v1.rs | 1 + storage/src/global_state/state/mod.rs | 20 ++- .../genesis/account_contract_installer.rs | 15 -- .../src/system/genesis/entity_installer.rs | 15 -- storage/src/system/transfer.rs | 36 +++- types/src/chainspec.rs | 4 +- types/src/chainspec/genesis_config.rs | 9 - types/src/chainspec/network_config.rs | 4 +- types/src/evm.rs | 7 +- types/src/evm/address.rs | 47 ++++- types/src/evm/hash.rs | 75 +++++++- types/src/evm/transaction.rs | 23 ++- .../deploy/executable_deploy_item.rs | 3 + .../transaction_v1/arg_handling.rs | 28 +++ types/src/transaction/transfer_target.rs | 10 +- 24 files changed, 516 insertions(+), 96 deletions(-) diff --git a/EVM.md b/EVM.md index 19a37a983e..045b605f49 100644 --- a/EVM.md +++ b/EVM.md @@ -39,8 +39,8 @@ Implemented in this workspace: - Contract runtime execution for finalized `Transaction::Evm` values. - Casper fee and refund handling for EVM transactions. - Binary-port `EvmCall` for read-only `eth_call` support. -- EVM genesis account seeding for secp256k1 genesis accounts when - `[evm].enabled = true`. +- Native Casper transfers to 20-byte EVM addresses when `[evm].enabled = true`, + creating or funding the corresponding EVM account record. Implemented in the sidecar workspace for validation: @@ -338,11 +338,16 @@ An EVM account record contains: Balances are Casper purse balances. EVM balance reads and writes reconcile through the account main purse and `Key::Balance(main_purse.addr())`. -When `[evm].enabled = true`, genesis creates an EVM account for each genesis -secp256k1 account. The EVM address is derived from the public key using -Ethereum address rules, and the EVM account uses the same main purse as the -Casper account. This avoids duplicating supply while allowing Ethereum -transactions to spend from the same funded devnet user. +Genesis does not create EVM account records for Casper genesis accounts. +Funding an EVM identity is explicit: a native Casper transfer can use a +20-byte `evm::Address` as its `target` argument when `[evm].enabled = true`. +If `Key::EvmAccount(address)` already exists, the transfer credits that +account's main purse. If it does not exist, the transfer creates +`StoredValue::EvmAccount(Account::new(0, EMPTY_CODE_HASH, +evm::deterministic_purse(address)))`, initializes that deterministic purse +with a zero balance, then transfers the requested motes into it. Transfer +records keep the Casper transfer schema unchanged: `to` is `None`, and +`target` is the EVM account's backing purse. ## Receipts @@ -564,6 +569,24 @@ eth_chainId: 0x435350ff eth_getTransactionCount: 0x0 ``` +### Fund EVM Identity + +Create and fund the EVM identity explicitly with a native Casper transfer: + +```bash +casper-cli transaction transfer \ + --from devnet:user-1 \ + --to 0x24790C4849cCAE43c0c1749e2C5b8d00Cc63AB80 \ + --amount 10000 \ + --raw \ + --no-interactive +``` + +The transfer target is encoded as `byte-array[20]`. A successful transfer +creates `Key::EvmAccount(0x24790c...)`, initializes its deterministic backing +purse, and credits it with the transferred motes. The EVM nonce remains `0x0` +until the first EVM transaction is executed. + ### Deploy Counter From the node workspace: @@ -660,8 +683,9 @@ Expected output: ### Confirm Fees -The EVM account uses the same main purse as devnet `user-1`, so the Casper -balance should decrease after deployment and increment: +The native transfer debits devnet `user-1` and credits the EVM identity's +deterministic backing purse. EVM transaction fees are charged from that EVM +backing purse, not from `user-1`'s Casper account purse: ```bash casper-cli account balance devnet:user-1 @@ -670,13 +694,6 @@ casper-cli account balance devnet:user-1 With `--gas-price 1000000`, every 1,000 gas consumed is 1 CSPR before refund policy is applied. -In the latest successful validation run, the post-deploy and post-increment -balance was: - -```text -999999999999999999999999074.848500000 CSPR -``` - ## Useful Checks Node workspace: diff --git a/binary_port/src/command.rs b/binary_port/src/command.rs index 966c169753..4caeb4e989 100644 --- a/binary_port/src/command.rs +++ b/binary_port/src/command.rs @@ -149,7 +149,7 @@ impl Command { casper_types::evm::Address::new(rng.gen()), rng.gen::() .then(|| casper_types::evm::Address::new(rng.gen())), - casper_types::evm::Hash::new(rng.gen()), + casper_types::U256::from_big_endian(&rng.gen::<[u8; 32]>()), casper_types::bytesrepr::Bytes::from(rng.random_vec(0..64)), rng.gen(), ), diff --git a/binary_port/src/error_code.rs b/binary_port/src/error_code.rs index 6bcb8ee8a1..74056dfd18 100644 --- a/binary_port/src/error_code.rs +++ b/binary_port/src/error_code.rs @@ -370,6 +370,9 @@ pub enum ErrorCode { InvalidDelegationAmount = 116, #[error("the transaction invocation target is unsupported under V2 runtime")] UnsupportedInvocationTarget = 117, + /// EVM address transfer target is disabled for this deploy. + #[error("EVM address transfer target is disabled for this deploy")] + DeployEvmAddressTransferDisabled = 118, } impl TryFrom for ErrorCode { diff --git a/binary_port/src/type_wrappers.rs b/binary_port/src/type_wrappers.rs index 9836366662..bbba222f6c 100644 --- a/binary_port/src/type_wrappers.rs +++ b/binary_port/src/type_wrappers.rs @@ -9,7 +9,7 @@ use casper_types::{ system::auction::DelegationRate, Account, AddressableEntity, BlockHash, ByteCode, Contract, ContractWasm, EntityAddr, EraId, ExecutionInfo, Key, PublicKey, StoredValue, TimeDiff, Timestamp, Transaction, ValidatorChange, - U512, + U256, U512, }; use serde::Serialize; @@ -47,7 +47,7 @@ macro_rules! impl_bytesrepr_for_type_wrapper { pub struct EvmCallRequest { from: evm::Address, to: Option, - value: evm::Hash, + value: U256, input: Bytes, gas_limit: u64, } @@ -57,7 +57,7 @@ impl EvmCallRequest { pub fn new( from: evm::Address, to: Option, - value: evm::Hash, + value: U256, input: Bytes, gas_limit: u64, ) -> Self { @@ -81,7 +81,7 @@ impl EvmCallRequest { } /// Returns the call value. - pub fn value(&self) -> evm::Hash { + pub fn value(&self) -> U256 { self.value } @@ -124,7 +124,7 @@ impl FromBytes for EvmCallRequest { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { let (from, remainder) = evm::Address::from_bytes(bytes)?; let (to, remainder) = Option::::from_bytes(remainder)?; - let (value, remainder) = evm::Hash::from_bytes(remainder)?; + let (value, remainder) = U256::from_bytes(remainder)?; let (input, remainder) = Bytes::from_bytes(remainder)?; let (gas_limit, remainder) = u64::from_bytes(remainder)?; Ok(( @@ -955,7 +955,7 @@ mod tests { bytesrepr::test_serialization_roundtrip(&EvmCallRequest::new( evm::Address::new(rng.gen()), rng.gen::().then(|| evm::Address::new(rng.gen())), - evm::Hash::new(rng.gen()), + U256::from_big_endian(&rng.gen::<[u8; 32]>()), Bytes::from(rng.random_vec(0..64)), rng.gen(), )); diff --git a/execution_engine_testing/test_support/src/transfer_request_builder.rs b/execution_engine_testing/test_support/src/transfer_request_builder.rs index c48bfb7dab..1f077172ea 100644 --- a/execution_engine_testing/test_support/src/transfer_request_builder.rs +++ b/execution_engine_testing/test_support/src/transfer_request_builder.rs @@ -82,6 +82,7 @@ impl TransferRequestBuilder { let target_value = match target.into() { TransferTarget::PublicKey(public_key) => CLValue::from_t(public_key), TransferTarget::AccountHash(account_hash) => CLValue::from_t(account_hash), + TransferTarget::EvmAddress(address) => CLValue::from_t(address), TransferTarget::URef(uref) => CLValue::from_t(uref), } .unwrap(); diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index 1f07fe26c5..0bdaf4f594 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -18,7 +18,7 @@ use casper_storage::data_access_layer::{ }; use casper_types::{ account::AccountHash, addressable_entity::AddressableEntity, system::auction::ARG_AMOUNT, - AddressableEntityHash, AddressableEntityIdentifier, BlockHeader, Chainspec, EntityAddr, + AddressableEntityHash, AddressableEntityIdentifier, BlockHeader, CLType, Chainspec, EntityAddr, EntityKind, EntityVersion, EntityVersionKey, ExecutableDeployItem, ExecutableDeployItemIdentifier, Package, PackageAddr, PackageHash, PackageIdentifier, Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, TransactionTarget, @@ -463,12 +463,22 @@ impl TransactionAcceptor { ExecutableDeployItem::Transfer { args } => { // We rely on the `Deploy::is_config_compliant` to check // that the transfer amount arg is present and is a valid U512. - if args.get(ARG_TARGET).is_none() { + let Some(target) = args.get(ARG_TARGET) else { let error = Error::parameter_failure( &block_header, DeployParameterFailure::MissingTransferTarget.into(), ); return self.reject_transaction(effect_builder, *event_metadata, error); + }; + if !self.chainspec.evm_config.enabled + && target.cl_type() + == &CLType::ByteArray(casper_types::evm::ADDRESS_LENGTH as u32) + { + let error = Error::parameter_failure( + &block_header, + DeployParameterFailure::EvmAddressTransferDisabled.into(), + ); + return self.reject_transaction(effect_builder, *event_metadata, error); } } ExecutableDeployItem::ModuleBytes { module_bytes, .. } => { diff --git a/node/src/components/transaction_acceptor/error.rs b/node/src/components/transaction_acceptor/error.rs index 1ce7409fcd..89b3e2965a 100644 --- a/node/src/components/transaction_acceptor/error.rs +++ b/node/src/components/transaction_acceptor/error.rs @@ -115,6 +115,9 @@ impl From for BinaryPortErrorCode { DeployParameterFailure::MissingTransferTarget => { BinaryPortErrorCode::DeployMissingTransferTarget } + DeployParameterFailure::EvmAddressTransferDisabled => { + BinaryPortErrorCode::DeployEvmAddressTransferDisabled + } DeployParameterFailure::MissingModuleBytes => { BinaryPortErrorCode::DeployMissingModuleBytes } @@ -191,6 +194,9 @@ pub(crate) enum DeployParameterFailure { /// Missing transfer "target" runtime argument. #[error("missing transfer 'target' runtime argument")] MissingTransferTarget, + /// EVM address transfer target is disabled. + #[error("EVM address transfer target is disabled")] + EvmAddressTransferDisabled, /// Module bytes for session code cannot be empty. #[error("module bytes for session code cannot be empty")] MissingModuleBytes, diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index d784e10a57..818e931722 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -355,6 +355,50 @@ async fn transfer_to_account>( ) } +async fn transfer_to_evm_address>( + fixture: &mut TestFixture, + amount: A, + from: &SecretKey, + to: evm::Address, + pricing: PricingMode, + transfer_id: Option, +) -> (TransactionHash, u64, ExecutionResult) { + let chain_name = fixture.chainspec.network_config.name.clone(); + + let mut txn = Transaction::from( + TransactionV1Builder::new_transfer(amount, None, to, transfer_id) + .unwrap() + .with_initiator_addr(PublicKey::from(from)) + .with_pricing_mode(pricing) + .with_chain_name(chain_name) + .build() + .unwrap(), + ); + + txn.sign(from); + let txn_hash = txn.hash(); + + fixture.inject_transaction(txn).await; + fixture + .run_until_executed_transaction(&txn_hash, TEN_SECS) + .await; + + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let exec_info = runner + .main_reactor() + .storage() + .read_execution_info(txn_hash) + .expect("Expected transaction to be included in a block."); + + ( + txn_hash, + exec_info.block_height, + exec_info + .execution_result + .expect("Exec result should have been stored."), + ) +} + async fn send_add_bid>( fixture: &mut TestFixture, amount: A, @@ -1166,6 +1210,124 @@ async fn should_reject_evm_transaction_when_value_and_fee_exceed_balance() { .is_none()); } +#[tokio::test] +async fn should_not_seed_evm_accounts_at_genesis() { + let evm_config = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config().with_evm_config(evm_config); + let alice_secret_key = Arc::new( + SecretKey::secp256k1_from_bytes([0x11; SecretKey::SECP256K1_LENGTH]) + .expect("secp256k1 key should be valid"), + ); + let bob_secret_key = Arc::new( + SecretKey::secp256k1_from_bytes([0x22; SecretKey::SECP256K1_LENGTH]) + .expect("secp256k1 key should be valid"), + ); + let charlie_secret_key = Arc::new( + SecretKey::secp256k1_from_bytes([0x33; SecretKey::SECP256K1_LENGTH]) + .expect("secp256k1 key should be valid"), + ); + let alice_evm_address = evm::Address::from_public_key(&PublicKey::from(&*alice_secret_key)) + .expect("secp256k1 public key should have an EVM address"); + let mut test = SingleTransactionTestCase::new( + alice_secret_key, + bob_secret_key, + charlie_secret_key, + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let (_node_id, runner) = test.fixture.network.nodes().iter().next().unwrap(); + let block_header = runner + .main_reactor() + .storage() + .read_block_header_by_height(0, true) + .expect("failure to read block header") + .expect("should have header"); + assert!(query_global_state( + &mut test.fixture, + *block_header.state_root_hash(), + Key::EvmAccount(alice_evm_address), + ) + .is_none()); +} + +#[tokio::test] +async fn should_transfer_to_evm_address_with_native_transfer() { + let evm_config = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_pricing_handling(PricingHandling::Fixed) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::NoFee); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let recipient = evm::Address::new([0x44; evm::ADDRESS_LENGTH]); + let transfer_amount = test + .fixture + .chainspec + .transaction_config + .native_transfer_minimum_motes + + 100; + let alice_secret_key = Arc::clone(&test.fixture.node_contexts[0].secret_key); + let (_txn_hash, block_height, execution_result) = transfer_to_evm_address( + &mut test.fixture, + transfer_amount, + &alice_secret_key, + recipient, + PricingMode::Fixed { + gas_price_tolerance: 1, + additional_computation_factor: 0, + }, + Some(0xE0), + ) + .await; + + assert!( + exec_result_is_success(&execution_result), + "{execution_result:?}" + ); + let account = evm_account_at(&mut test.fixture, block_height, recipient); + let expected_purse = evm::deterministic_purse(recipient); + assert_eq!(account.main_purse(), expected_purse); + assert_eq!( + evm_balance(&test.fixture, recipient, block_height), + U512::from(transfer_amount) + ); + + let transfers = execution_result.transfers(); + assert_eq!(transfers.len(), 1, "{transfers:?}"); + let casper_types::Transfer::V2(transfer) = &transfers[0] else { + panic!("expected V2 transfer"); + }; + assert_eq!(transfer.to, None); + assert_eq!(transfer.target.addr(), expected_purse.addr()); + assert_eq!(transfer.amount, U512::from(transfer_amount)); +} + #[tokio::test] async fn should_accept_transfer_without_id() { let initial_stakes = InitialStakes::FromVec(vec![u128::MAX, 1]); diff --git a/node/src/types/transaction/arg_handling.rs b/node/src/types/transaction/arg_handling.rs index 6b347c243b..7eb1367c30 100644 --- a/node/src/types/transaction/arg_handling.rs +++ b/node/src/types/transaction/arg_handling.rs @@ -4,6 +4,7 @@ use core::marker::PhantomData; use casper_types::{ account::AccountHash, bytesrepr::FromBytes, + evm, system::auction::{DelegatorKind, Reservation, ARG_VALIDATOR}, CLType, CLTyped, CLValue, CLValueError, Chainspec, InvalidTransactionV1, PublicKey, RuntimeArgs, TransactionArgs, URef, U512, @@ -166,6 +167,7 @@ pub fn new_transfer_args, T: Into>( TransferTarget::AccountHash(account_hash) => { args.insert(TRANSFER_ARG_TARGET, account_hash)? } + TransferTarget::EvmAddress(address) => args.insert(TRANSFER_ARG_TARGET, address)?, TransferTarget::URef(uref) => args.insert(TRANSFER_ARG_TARGET, uref)?, } TRANSFER_ARG_AMOUNT.insert(&mut args, amount.into())?; @@ -179,6 +181,7 @@ pub fn new_transfer_args, T: Into>( pub fn has_valid_transfer_args( args: &TransactionArgs, native_transfer_minimum_motes: u64, + evm_enabled: bool, ) -> Result<(), InvalidTransactionV1> { let args = args .as_named() @@ -204,6 +207,13 @@ pub fn has_valid_transfer_args( arg_name: TRANSFER_ARG_TARGET.to_string(), } })?; + let expected_target_types = || { + let mut expected = vec![CLType::PublicKey, CLType::ByteArray(32), CLType::URef]; + if evm_enabled { + expected.push(CLType::ByteArray(evm::ADDRESS_LENGTH as u32)); + } + expected + }; match target_cl_value.cl_type() { CLType::PublicKey => { let _ = parse_cl_value::(target_cl_value, TRANSFER_ARG_TARGET); @@ -211,6 +221,9 @@ pub fn has_valid_transfer_args( CLType::ByteArray(32) => { let _ = parse_cl_value::(target_cl_value, TRANSFER_ARG_TARGET); } + CLType::ByteArray(length) if *length == evm::ADDRESS_LENGTH as u32 && evm_enabled => { + let _ = parse_cl_value::(target_cl_value, TRANSFER_ARG_TARGET); + } CLType::URef => { let _ = parse_cl_value::(target_cl_value, TRANSFER_ARG_TARGET); } @@ -225,7 +238,7 @@ pub fn has_valid_transfer_args( ); return Err(InvalidTransactionV1::unexpected_arg_type( TRANSFER_ARG_TARGET.to_string(), - vec![CLType::PublicKey, CLType::ByteArray(32), CLType::URef], + expected_target_types(), target_cl_value.cl_type().clone(), )); } @@ -588,7 +601,7 @@ mod tests { rng.gen::().then(|| rng.gen()), ) .unwrap(); - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes).unwrap(); + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false).unwrap(); // Check random args, AccountHash target, within motes limit. let args = new_transfer_args( @@ -598,7 +611,7 @@ mod tests { rng.gen::().then(|| rng.gen()), ) .unwrap(); - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes).unwrap(); + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false).unwrap(); // Check random args, URef target, within motes limit. let args = new_transfer_args( @@ -608,7 +621,36 @@ mod tests { rng.gen::().then(|| rng.gen()), ) .unwrap(); - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes).unwrap(); + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false).unwrap(); + + // Check random args, EVM address target, within motes limit. + let evm_address = evm::Address::new(rng.gen()); + let args = new_transfer_args( + U512::from(rng.gen_range(min_motes..=u64::MAX)), + rng.gen::().then(|| rng.gen()), + evm_address, + rng.gen::().then(|| rng.gen()), + ) + .unwrap(); + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, true).unwrap(); + + let evm_address = evm::Address::new(rng.gen()); + let args = new_transfer_args( + U512::from(rng.gen_range(min_motes..=u64::MAX)), + rng.gen::().then(|| rng.gen()), + evm_address, + rng.gen::().then(|| rng.gen()), + ) + .unwrap(); + let expected_error = InvalidTransactionV1::unexpected_arg_type( + TRANSFER_ARG_TARGET.to_string(), + vec![CLType::PublicKey, CLType::ByteArray(32), CLType::URef], + CLType::ByteArray(evm::ADDRESS_LENGTH as u32), + ); + assert_eq!( + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false), + Err(expected_error) + ); // Check at minimum motes limit. let args = new_transfer_args( @@ -618,7 +660,7 @@ mod tests { rng.gen::().then(|| rng.gen()), ) .unwrap(); - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes).unwrap(); + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false).unwrap(); // Check with extra arg. let mut args = new_transfer_args( @@ -629,7 +671,7 @@ mod tests { ) .unwrap(); args.insert("a", 1).unwrap(); - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes).unwrap(); + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false).unwrap(); } #[test] @@ -648,7 +690,7 @@ mod tests { }; assert_eq!( - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes), + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false), Err(expected_error) ); } @@ -666,7 +708,7 @@ mod tests { arg_name: TRANSFER_ARG_TARGET.to_string(), }; assert_eq!( - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes), + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false), Err(expected_error) ); @@ -678,7 +720,7 @@ mod tests { arg_name: TRANSFER_ARG_AMOUNT.name.to_string(), }; assert_eq!( - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes), + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false), Err(expected_error) ); } @@ -699,7 +741,7 @@ mod tests { CLType::String, ); assert_eq!( - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes), + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false), Err(expected_error) ); @@ -715,7 +757,7 @@ mod tests { CLType::U8, ); assert_eq!( - has_valid_transfer_args(&TransactionArgs::Named(args), min_motes), + has_valid_transfer_args(&TransactionArgs::Named(args), min_motes, false), Err(expected_error) ); } @@ -1427,7 +1469,7 @@ mod tests { let args = TransactionArgs::Bytesrepr(vec![b'a'; 100].into()); let expected_error = InvalidTransactionV1::ExpectedNamedArguments; assert_eq!( - has_valid_transfer_args(&args, 0).as_ref(), + has_valid_transfer_args(&args, 0, false).as_ref(), Err(&expected_error) ); assert_eq!(check_add_bid_args(&args).as_ref(), Err(&expected_error)); diff --git a/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs b/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs index 5f0a0f43b5..cb31e528a5 100644 --- a/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs +++ b/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs @@ -637,6 +637,7 @@ impl MetaTransactionV1 { TransactionEntryPoint::Transfer => arg_handling::has_valid_transfer_args( &self.args, config.native_transfer_minimum_motes, + chainspec.evm_config.enabled, ), TransactionEntryPoint::Burn => arg_handling::has_valid_burn_args(&self.args), TransactionEntryPoint::AddBid => { diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index d1ee10aa9a..698aa7e840 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -22,6 +22,7 @@ use casper_types::{ account::AccountHash, bytesrepr::{self, Bytes, ToBytes}, contracts::NamedKeys, + evm, execution::{Effects, TransformError, TransformInstruction, TransformKindV2, TransformV2}, global_state::TrieMerkleProof, system::{ @@ -2219,7 +2220,9 @@ pub trait StateProvider: Send + Sync + Sized { ); match transfer_target_mode { - TransferTargetMode::ExistingAccount { .. } | TransferTargetMode::PurseExists { .. } => { + TransferTargetMode::ExistingAccount { .. } + | TransferTargetMode::ExistingEvmAccount { .. } + | TransferTargetMode::PurseExists { .. } => { // Noop } TransferTargetMode::CreateAccount(account_hash) => { @@ -2238,6 +2241,21 @@ pub trait StateProvider: Send + Sync + Sized { return TransferResult::Failure(tce.into()); } } + TransferTargetMode::CreateEvmAccount(address) => { + let main_purse = evm::deterministic_purse(address); + let balance = match CLValue::from_t(U512::zero()) { + Ok(balance) => balance, + Err(error) => return TransferResult::Failure(TransferError::CLValue(error)), + }; + tc.borrow_mut().write( + Key::EvmAccount(address), + StoredValue::EvmAccount(evm::Account::new(0, evm::EMPTY_CODE_HASH, main_purse)), + ); + tc.borrow_mut().write( + Key::Balance(main_purse.addr()), + StoredValue::CLValue(balance), + ); + } } let transfer_args = match runtime_args_builder.build( &runtime_footprint, diff --git a/storage/src/system/genesis/account_contract_installer.rs b/storage/src/system/genesis/account_contract_installer.rs index 22f81309a0..b2fc8afafc 100644 --- a/storage/src/system/genesis/account_contract_installer.rs +++ b/storage/src/system/genesis/account_contract_installer.rs @@ -27,7 +27,6 @@ use casper_types::{ ContractHash, ContractPackage, ContractPackageHash, ContractPackageStatus, ContractVersions, DisabledVersions, NamedKeys, }, - evm, execution::Effects, system::{ auction::{ @@ -626,7 +625,6 @@ where )); self.tracking_copy.borrow_mut().write(key, stored_value); - self.maybe_create_evm_account(&account, main_purse); total_supply += account.balance().value(); } @@ -642,19 +640,6 @@ where Ok(sustain_purse) } - fn maybe_create_evm_account(&self, account: &GenesisAccount, main_purse: URef) { - if !self.config.enable_evm() { - return; - } - let Some(address) = evm::Address::from_public_key(&account.public_key()) else { - return; - }; - self.tracking_copy.borrow_mut().write( - Key::EvmAccount(address), - StoredValue::EvmAccount(evm::Account::new(0, evm::EMPTY_CODE_HASH, main_purse)), - ); - } - fn initial_seigniorage_recipients( &self, staked: &Staking, diff --git a/storage/src/system/genesis/entity_installer.rs b/storage/src/system/genesis/entity_installer.rs index 018ef5e5df..db6db59976 100644 --- a/storage/src/system/genesis/entity_installer.rs +++ b/storage/src/system/genesis/entity_installer.rs @@ -18,7 +18,6 @@ use casper_types::{ ActionThresholds, EntityKindTag, MessageTopics, NamedKeyAddr, NamedKeyValue, }, contracts::NamedKeys, - evm, execution::Effects, system::{ auction, @@ -587,7 +586,6 @@ where None, main_purse, )?; - self.maybe_create_evm_account(&account, main_purse); total_supply += account_starting_balance; } @@ -603,19 +601,6 @@ where Ok(()) } - fn maybe_create_evm_account(&self, account: &GenesisAccount, main_purse: URef) { - if !self.config.enable_evm() { - return; - } - let Some(address) = evm::Address::from_public_key(&account.public_key()) else { - return; - }; - self.tracking_copy.borrow_mut().write( - Key::EvmAccount(address), - StoredValue::EvmAccount(evm::Account::new(0, evm::EMPTY_CODE_HASH, main_purse)), - ); - } - fn initial_seigniorage_recipients( &self, staked: &Staking, diff --git a/storage/src/system/transfer.rs b/storage/src/system/transfer.rs index 2b172138b9..1dcc6d5a1d 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -4,6 +4,7 @@ use thiserror::Error; use casper_types::{ account::AccountHash, bytesrepr::FromBytes, + evm, system::{mint, mint::Error as MintError}, AccessRights, CLType, CLTyped, CLValue, CLValueError, Key, ProtocolVersion, RuntimeArgs, RuntimeFootprint, StoredValue, StoredValueTypeMismatch, URef, U512, @@ -87,6 +88,11 @@ pub enum TransferTargetMode { /// Main purse of a resolved account. main_purse: URef, }, + /// Native transfer arguments resolved into a transfer to an existing EVM account. + ExistingEvmAccount { + /// Main purse of a resolved EVM account. + main_purse: URef, + }, /// Native transfer arguments resolved into a transfer to a purse. PurseExists { /// Target account hash (if known). @@ -96,6 +102,8 @@ pub enum TransferTargetMode { }, /// Native transfer arguments resolved into a transfer to a new account. CreateAccount(AccountHash), + /// Native transfer arguments resolved into a transfer to a new EVM account. + CreateEvmAccount(evm::Address), } impl TransferTargetMode { @@ -111,6 +119,8 @@ impl TransferTargetMode { .. } => Some(*target_account_hash), TransferTargetMode::CreateAccount(target_account_hash) => Some(*target_account_hash), + TransferTargetMode::ExistingEvmAccount { .. } + | TransferTargetMode::CreateEvmAccount(_) => None, } } } @@ -342,6 +352,26 @@ impl TransferRuntimeArgsBuilder { Some(cl_value) if *cl_value.cl_type() == CLType::ByteArray(32) => { self.map_cl_value(cl_value)? } + Some(cl_value) + if *cl_value.cl_type() == CLType::ByteArray(evm::ADDRESS_LENGTH as u32) => + { + let address: evm::Address = self.map_cl_value(cl_value)?; + let key = Key::EvmAccount(address); + return match tracking_copy.borrow_mut().read(&key)? { + Some(StoredValue::EvmAccount(account)) => { + Ok(TransferTargetMode::ExistingEvmAccount { + main_purse: account.main_purse().with_access_rights(AccessRights::ADD), + }) + } + Some(stored_value) => { + Err(TransferError::TypeMismatch(StoredValueTypeMismatch::new( + "StoredValue::EvmAccount".to_string(), + stored_value.type_name(), + ))) + } + None => Ok(TransferTargetMode::CreateEvmAccount(address)), + }; + } Some(cl_value) if *cl_value.cl_type() == CLType::Key => { let account_key: Key = self.map_cl_value(cl_value)?; let account_hash: AccountHash = account_key @@ -425,11 +455,15 @@ impl TransferRuntimeArgsBuilder { main_purse: purse_uref, target_account_hash: target_account, } => (Some(target_account), purse_uref), + TransferTargetMode::ExistingEvmAccount { + main_purse: purse_uref, + .. + } => (None, purse_uref), TransferTargetMode::PurseExists { target_account_hash, purse_uref, } => (target_account_hash, purse_uref), - TransferTargetMode::CreateAccount(_) => { + TransferTargetMode::CreateAccount(_) | TransferTargetMode::CreateEvmAccount(_) => { // Method "build()" is called after `resolve_transfer_target_mode` is first called // and handled by creating a new account. Calling `resolve_transfer_target_mode` // for the second time should never return `CreateAccount` variant. diff --git a/types/src/chainspec.rs b/types/src/chainspec.rs index a1fde64a45..57078cb50a 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -28,7 +28,7 @@ use std::{fmt::Debug, sync::Arc}; use datasize::DataSize; #[cfg(any(feature = "testing", test))] use rand::Rng; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tracing::error; #[cfg(any(feature = "testing", test))] @@ -98,7 +98,7 @@ pub use vm_config::{ /// A collection of configuration settings describing the state of the system at genesis and after /// upgrades to basic system functionality occurring after genesis. -#[derive(Clone, PartialEq, Eq, Serialize, Debug, Default)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Default)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[serde(deny_unknown_fields)] pub struct Chainspec { diff --git a/types/src/chainspec/genesis_config.rs b/types/src/chainspec/genesis_config.rs index 8573f36e8c..9721ace5dd 100644 --- a/types/src/chainspec/genesis_config.rs +++ b/types/src/chainspec/genesis_config.rs @@ -34,7 +34,6 @@ pub struct GenesisConfig { gas_hold_balance_handling: HoldBalanceHandling, gas_hold_interval_millis: u64, enable_addressable_entity: bool, - enable_evm: bool, rewards_ratio: Option>, storage_costs: StorageCosts, minimum_delegation_rate: DelegationRate, @@ -73,7 +72,6 @@ impl GenesisConfig { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity, - enable_evm: false, rewards_ratio: rewards_handling, storage_costs, minimum_delegation_rate, @@ -172,11 +170,6 @@ impl GenesisConfig { self.enable_addressable_entity } - /// Returns whether EVM genesis account records should be installed. - pub fn enable_evm(&self) -> bool { - self.enable_evm - } - /// Set enable entity. pub fn set_enable_entity(&mut self, enable: bool) { self.enable_addressable_entity = enable @@ -252,7 +245,6 @@ impl Distribution for Standard { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity: false, - enable_evm: false, rewards_ratio: None, storage_costs, minimum_delegation_rate, @@ -287,7 +279,6 @@ impl From<&Chainspec> for GenesisConfig { gas_hold_balance_handling, gas_hold_interval_millis, enable_addressable_entity: chainspec.core_config.enable_addressable_entity, - enable_evm: chainspec.evm_config.enabled, rewards_ratio, storage_costs, minimum_delegation_rate: chainspec.core_config.minimum_delegation_rate, diff --git a/types/src/chainspec/network_config.rs b/types/src/chainspec/network_config.rs index 4ea2c88112..979ac54c7d 100644 --- a/types/src/chainspec/network_config.rs +++ b/types/src/chainspec/network_config.rs @@ -3,7 +3,7 @@ use datasize::DataSize; #[cfg(any(feature = "testing", test))] use rand::Rng; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use crate::bytesrepr::{self, FromBytes, ToBytes}; #[cfg(any(feature = "testing", test))] @@ -12,7 +12,7 @@ use crate::testing::TestRng; use super::AccountsConfig; /// Configuration values associated with the network. -#[derive(Clone, PartialEq, Eq, Serialize, Debug, Default)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Default)] #[cfg_attr(feature = "datasize", derive(DataSize))] pub struct NetworkConfig { /// The network name. diff --git a/types/src/evm.rs b/types/src/evm.rs index c706c17a91..699f2179c0 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -8,6 +8,7 @@ mod account; mod address; mod config; +mod eth_u256; mod hash; mod receipt; mod transaction; @@ -17,9 +18,11 @@ pub use account::{ }; pub use address::{Address, ADDRESS_LENGTH}; pub use config::{EvmConfig, EvmSpec}; +pub use eth_u256::EthU256; pub use hash::{Hash, HASH_LENGTH}; pub use receipt::{HaltReason, Log, OutOfGasError, Receipt, ReceiptStatus}; pub use transaction::{ - Transaction, TransactionError, TransactionHash, TransactionKind, EIP4844_TRANSACTION_TYPE_ID, - EIP7702_TRANSACTION_TYPE_ID, + Transaction, TransactionError, TransactionHash, TransactionKind, EIP1559_TRANSACTION_TYPE_ID, + EIP2930_TRANSACTION_TYPE_ID, EIP4844_TRANSACTION_TYPE_ID, EIP7702_TRANSACTION_TYPE_ID, + LEGACY_TRANSACTION_TYPE_ID, }; diff --git a/types/src/evm/address.rs b/types/src/evm/address.rs index 514951c715..a0d93750a4 100644 --- a/types/src/evm/address.rs +++ b/types/src/evm/address.rs @@ -14,7 +14,7 @@ use alloy_primitives::keccak256; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, - PublicKey, + CLType, CLTyped, PublicKey, }; /// The number of bytes in an EVM address. @@ -27,7 +27,6 @@ const ADDRESS_SERIALIZED_LENGTH: usize = ADDRESS_LENGTH; Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, )] #[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub struct Address([u8; ADDRESS_LENGTH]); impl Address { @@ -84,6 +83,27 @@ impl Display for Address { } } +#[cfg(feature = "json-schema")] +impl JsonSchema for Address { + fn schema_name() -> String { + String::from("Address") + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + let schema = gen.subschema_for::(); + let mut schema_object = schema.into_object(); + schema_object.metadata().description = + Some("A 20-byte Ethereum account or contract address encoded as hexadecimal.".into()); + schema_object.into() + } +} + +impl CLTyped for Address { + fn cl_type() -> CLType { + CLType::ByteArray(ADDRESS_LENGTH as u32) + } +} + impl ToBytes for Address { fn to_bytes(&self) -> Result, bytesrepr::Error> { Ok(self.0.to_vec()) @@ -110,3 +130,26 @@ impl FromBytes for Address { Ok((Address(address), remainder)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::CLValue; + + #[test] + fn evm_address_cl_value_roundtrip() { + let address = Address::new([0x11; ADDRESS_LENGTH]); + let cl_value = CLValue::from_t(address).expect("address should serialize"); + + assert_eq!( + cl_value.cl_type(), + &CLType::ByteArray(ADDRESS_LENGTH as u32) + ); + assert_eq!( + cl_value + .to_t::
() + .expect("address should deserialize"), + address + ); + } +} diff --git a/types/src/evm/hash.rs b/types/src/evm/hash.rs index 1fbc58b68f..de19895cbf 100644 --- a/types/src/evm/hash.rs +++ b/types/src/evm/hash.rs @@ -8,7 +8,7 @@ use core::{ use datasize::DataSize; #[cfg(feature = "json-schema")] use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, @@ -19,11 +19,8 @@ use crate::{ pub const HASH_LENGTH: usize = 32; /// A 32-byte EVM hash or storage word. -#[derive( - Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, -)] +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub struct Hash(Digest); impl Hash { @@ -68,6 +65,48 @@ impl Display for Hash { } } +#[cfg(feature = "json-schema")] +impl JsonSchema for Hash { + fn schema_name() -> String { + String::from("EvmHash") + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + let schema = gen.subschema_for::(); + let mut schema_object = schema.into_object(); + schema_object.metadata().description = + Some("A 32-byte EVM hash or storage word encoded as 0x-prefixed hexadecimal.".into()); + schema_object.into() + } +} + +impl Serialize for Hash { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + serializer.collect_str(self) + } else { + self.0.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for Hash { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let value = String::deserialize(deserializer)?; + let hex = value + .strip_prefix("0x") + .ok_or_else(|| D::Error::custom("hash must start with 0x"))?; + let bytes = base16::decode(hex.as_bytes()).map_err(SerdeError::custom)?; + let bytes = + <[u8; HASH_LENGTH]>::try_from(bytes.as_ref()).map_err(SerdeError::custom)?; + Ok(Hash::new(bytes)) + } else { + Digest::deserialize(deserializer).map(Hash) + } + } +} + impl ToBytes for Hash { fn to_bytes(&self) -> Result, bytesrepr::Error> { self.0.to_bytes() @@ -87,3 +126,29 @@ impl FromBytes for Hash { Digest::from_bytes(bytes).map(|(digest, remainder)| (Hash(digest), remainder)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn human_readable_serde_uses_0x_prefixed_hex() { + let hash = Hash::new([0xab; HASH_LENGTH]); + let expected_hex = "ab".repeat(HASH_LENGTH); + + let encoded = serde_json::to_string(&hash).expect("hash should serialize"); + assert_eq!(encoded, format!("\"0x{expected_hex}\"")); + + let decoded: Hash = serde_json::from_str(&encoded).expect("hash should deserialize"); + assert_eq!(decoded, hash); + } + + #[test] + fn non_human_readable_serde_roundtrip() { + let hash = Hash::new([0xcd; HASH_LENGTH]); + let encoded = bincode::serialize(&hash).expect("hash should serialize"); + let decoded: Hash = bincode::deserialize(&encoded).expect("hash should deserialize"); + + assert_eq!(decoded, hash); + } +} diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index 51e6c3aa4c..ccad44e2da 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -42,6 +42,15 @@ use crate::{ const TRANSACTION_KIND_SERIALIZED_LENGTH: usize = U8_SERIALIZED_LENGTH; +/// Ethereum transaction type ID for legacy transactions. +pub const LEGACY_TRANSACTION_TYPE_ID: u8 = 0; + +/// Ethereum transaction type ID for EIP-2930 access-list transactions. +pub const EIP2930_TRANSACTION_TYPE_ID: u8 = 1; + +/// Ethereum transaction type ID for EIP-1559 dynamic-fee transactions. +pub const EIP1559_TRANSACTION_TYPE_ID: u8 = 2; + /// Ethereum transaction type ID for EIP-4844 blob transactions. pub const EIP4844_TRANSACTION_TYPE_ID: u8 = EIP4844_TX_TYPE_ID; @@ -54,6 +63,7 @@ pub const EIP7702_TRANSACTION_TYPE_ID: u8 = EIP7702_TX_TYPE_ID; )] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[cfg_attr(feature = "json-schema", schemars(rename = "EvmTransactionHash"))] pub struct TransactionHash(Digest); impl TransactionHash { @@ -152,13 +162,18 @@ pub enum TransactionKind { } impl TransactionKind { - fn tag(self) -> u8 { + /// Returns the Ethereum transaction type ID for this transaction kind. + pub const fn type_id(self) -> u8 { match self { - TransactionKind::Legacy => 0, - TransactionKind::Eip2930 => 1, - TransactionKind::Eip1559 => 2, + TransactionKind::Legacy => LEGACY_TRANSACTION_TYPE_ID, + TransactionKind::Eip2930 => EIP2930_TRANSACTION_TYPE_ID, + TransactionKind::Eip1559 => EIP1559_TRANSACTION_TYPE_ID, } } + + fn tag(self) -> u8 { + self.type_id() + } } impl Display for TransactionKind { diff --git a/types/src/transaction/deploy/executable_deploy_item.rs b/types/src/transaction/deploy/executable_deploy_item.rs index c684c35120..71f31b3d13 100644 --- a/types/src/transaction/deploy/executable_deploy_item.rs +++ b/types/src/transaction/deploy/executable_deploy_item.rs @@ -241,6 +241,9 @@ impl ExecutableDeployItem { TransferTarget::AccountHash(account_hash) => args .insert(TRANSFER_ARG_TARGET, account_hash) .expect("should serialize account hash target arg"), + TransferTarget::EvmAddress(address) => args + .insert(TRANSFER_ARG_TARGET, address) + .expect("should serialize EVM address target arg"), TransferTarget::URef(uref) => args .insert(TRANSFER_ARG_TARGET, uref) .expect("should serialize uref target arg"), diff --git a/types/src/transaction/transaction_v1/arg_handling.rs b/types/src/transaction/transaction_v1/arg_handling.rs index b1c003ef90..87b5f15cce 100644 --- a/types/src/transaction/transaction_v1/arg_handling.rs +++ b/types/src/transaction/transaction_v1/arg_handling.rs @@ -97,6 +97,7 @@ pub(crate) fn new_transfer_args, T: Into>( TransferTarget::AccountHash(account_hash) => { args.insert(TRANSFER_ARG_TARGET, account_hash)? } + TransferTarget::EvmAddress(address) => args.insert(TRANSFER_ARG_TARGET, address)?, TransferTarget::URef(uref) => args.insert(TRANSFER_ARG_TARGET, uref)?, } TRANSFER_ARG_AMOUNT.insert(&mut args, amount.into())?; @@ -182,3 +183,30 @@ pub(crate) fn new_redelegate_args>( REDELEGATE_ARG_NEW_VALIDATOR.insert(&mut args, new_validator)?; Ok(args) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{evm, CLType}; + + #[test] + fn new_transfer_args_accepts_evm_address_target() { + let address = evm::Address::new([0x44; evm::ADDRESS_LENGTH]); + let args = new_transfer_args(U512::from(1), None, address, None) + .expect("EVM address transfer args should serialize"); + let target = args + .get(TRANSFER_ARG_TARGET) + .expect("target argument should exist"); + + assert_eq!( + target.cl_type(), + &CLType::ByteArray(evm::ADDRESS_LENGTH as u32) + ); + assert_eq!( + target + .to_t::() + .expect("EVM address should deserialize"), + address + ); + } +} diff --git a/types/src/transaction/transfer_target.rs b/types/src/transaction/transfer_target.rs index e1500a5384..d306a11619 100644 --- a/types/src/transaction/transfer_target.rs +++ b/types/src/transaction/transfer_target.rs @@ -3,7 +3,7 @@ use rand::Rng; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; -use crate::{account::AccountHash, PublicKey, URef}; +use crate::{account::AccountHash, evm, PublicKey, URef}; /// The various types which can be used as the `target` runtime argument of a native transfer. #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] @@ -12,6 +12,8 @@ pub enum TransferTarget { PublicKey(PublicKey), /// An account hash. AccountHash(AccountHash), + /// An EVM address. + EvmAddress(evm::Address), /// A URef. URef(URef), } @@ -41,6 +43,12 @@ impl From for TransferTarget { } } +impl From for TransferTarget { + fn from(address: evm::Address) -> Self { + Self::EvmAddress(address) + } +} + impl From for TransferTarget { fn from(uref: URef) -> Self { Self::URef(uref) From d5f99ecc1d27ea55bfc9077c6fd45aa1a24ac41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Mon, 11 May 2026 15:52:03 +0200 Subject: [PATCH 07/17] Replace EvmCall with generic Simulate --- EVM.md | 4 +- binary_port/src/command.rs | 57 +++-- binary_port/src/lib.rs | 4 +- binary_port/src/response_type.rs | 27 ++- binary_port/src/type_wrappers.rs | 176 +++++++++++++- executor/evm/src/request.rs | 4 +- executor/evm/src/tx.rs | 2 +- executor/evm/tests/executor.rs | 18 +- node/BINARY_PORT_PROTOCOL.md | 3 +- node/src/components/binary_port.rs | 28 ++- node/src/components/binary_port/config.rs | 13 ++ node/src/components/binary_port/event.rs | 11 +- node/src/components/binary_port/metrics.rs | 13 ++ node/src/components/binary_port/tests.rs | 79 ++++++- .../src/reactor/main_reactor/tests/fixture.rs | 1 + .../integration-test/config-example.toml | 7 + resources/local/config.toml | 7 + resources/mainnet/config-example.toml | 7 + resources/production/config-example.toml | 7 + resources/testnet/config-example.toml | 7 + types/src/evm/eth_u256.rs | 216 ++++++++++++++++++ 21 files changed, 627 insertions(+), 64 deletions(-) create mode 100644 types/src/evm/eth_u256.rs diff --git a/EVM.md b/EVM.md index 045b605f49..007fde3b62 100644 --- a/EVM.md +++ b/EVM.md @@ -38,7 +38,7 @@ Implemented in this workspace: - `casper-executor-evm`, backed by `revm`, with Casper-owned public types. - Contract runtime execution for finalized `Transaction::Evm` values. - Casper fee and refund handling for EVM transactions. -- Binary-port `EvmCall` for read-only `eth_call` support. +- Binary-port `Simulate` for read-only `eth_call` support. - Native Casper transfers to 20-byte EVM addresses when `[evm].enabled = true`, creating or funding the corresponding EVM account record. @@ -384,7 +384,7 @@ Sidecar derives those fields from execution info and block transaction order: ## Read-only EVM Calls -`eth_call` uses a node binary-port command, not transaction submission. +`eth_call` uses the node binary-port `Simulate` command, not transaction submission. The binary-port request carries: diff --git a/binary_port/src/command.rs b/binary_port/src/command.rs index 4caeb4e989..f34e286c2f 100644 --- a/binary_port/src/command.rs +++ b/binary_port/src/command.rs @@ -5,7 +5,7 @@ use casper_types::{ Transaction, }; -use crate::{get_request::GetRequest, EvmCallRequest}; +use crate::{get_request::GetRequest, SimulationRequest}; #[cfg(test)] use casper_types::testing::TestRng; @@ -116,10 +116,10 @@ pub enum Command { /// Transaction to execute. transaction: Transaction, }, - /// Request to execute a read-only EVM call. - EvmCall { - /// EVM call request. - request: EvmCallRequest, + /// Request to run a simulation. + Simulate { + /// Simulation request. + request: SimulationRequest, }, } @@ -130,7 +130,7 @@ impl Command { Command::Get(_) => CommandTag::Get, Command::TryAcceptTransaction { .. } => CommandTag::TryAcceptTransaction, Command::TrySpeculativeExec { .. } => CommandTag::TrySpeculativeExec, - Command::EvmCall { .. } => CommandTag::EvmCall, + Command::Simulate { .. } => CommandTag::Simulate, } } @@ -144,15 +144,8 @@ impl Command { CommandTag::TrySpeculativeExec => Self::TrySpeculativeExec { transaction: Transaction::random(rng), }, - CommandTag::EvmCall => Self::EvmCall { - request: EvmCallRequest::new( - casper_types::evm::Address::new(rng.gen()), - rng.gen::() - .then(|| casper_types::evm::Address::new(rng.gen())), - casper_types::U256::from_big_endian(&rng.gen::<[u8; 32]>()), - casper_types::bytesrepr::Bytes::from(rng.random_vec(0..64)), - rng.gen(), - ), + CommandTag::Simulate => Self::Simulate { + request: SimulationRequest::random(rng), }, } } @@ -170,7 +163,7 @@ impl ToBytes for Command { Command::Get(inner) => inner.write_bytes(writer), Command::TryAcceptTransaction { transaction } => transaction.write_bytes(writer), Command::TrySpeculativeExec { transaction } => transaction.write_bytes(writer), - Command::EvmCall { request } => request.write_bytes(writer), + Command::Simulate { request } => request.write_bytes(writer), } } @@ -179,7 +172,7 @@ impl ToBytes for Command { Command::Get(inner) => inner.serialized_length(), Command::TryAcceptTransaction { transaction } => transaction.serialized_length(), Command::TrySpeculativeExec { transaction } => transaction.serialized_length(), - Command::EvmCall { request } => request.serialized_length(), + Command::Simulate { request } => request.serialized_length(), } } } @@ -201,9 +194,9 @@ impl TryFrom<(CommandTag, &[u8])> for Command { let (transaction, remainder) = FromBytes::from_bytes(bytes)?; (Command::TrySpeculativeExec { transaction }, remainder) } - CommandTag::EvmCall => { + CommandTag::Simulate => { let (request, remainder) = FromBytes::from_bytes(bytes)?; - (Command::EvmCall { request }, remainder) + (Command::Simulate { request }, remainder) } }; if !remainder.is_empty() { @@ -223,8 +216,8 @@ pub enum CommandTag { TryAcceptTransaction = 1, /// Request to execute a transaction speculatively. TrySpeculativeExec = 2, - /// Request to execute a read-only EVM call. - EvmCall = 3, + /// Request to run a simulation. + Simulate = 3, } impl CommandTag { @@ -235,7 +228,7 @@ impl CommandTag { 0 => CommandTag::Get, 1 => CommandTag::TryAcceptTransaction, 2 => CommandTag::TrySpeculativeExec, - 3 => CommandTag::EvmCall, + 3 => CommandTag::Simulate, _ => unreachable!(), } } @@ -249,7 +242,7 @@ impl TryFrom for CommandTag { 0 => Ok(CommandTag::Get), 1 => Ok(CommandTag::TryAcceptTransaction), 2 => Ok(CommandTag::TrySpeculativeExec), - 3 => Ok(CommandTag::EvmCall), + 3 => Ok(CommandTag::Simulate), _ => Err(InvalidCommandTag), } } @@ -285,4 +278,22 @@ mod tests { let bytes = val.to_bytes().expect("should serialize"); assert_eq!(Command::try_from((val.tag(), &bytes[..])), Ok(val)); } + + #[test] + fn simulate_request_bytesrepr_roundtrip() { + let rng = &mut TestRng::new(); + + let val = Command::Simulate { + request: SimulationRequest::EvmCall(crate::EvmCallRequest::new( + casper_types::evm::Address::new(rng.gen()), + rng.gen::() + .then(|| casper_types::evm::Address::new(rng.gen())), + casper_types::U256::from_big_endian(&rng.gen::<[u8; 32]>()), + casper_types::bytesrepr::Bytes::from(rng.random_vec(0..64)), + rng.gen(), + )), + }; + let bytes = val.to_bytes().expect("should serialize"); + assert_eq!(Command::try_from((val.tag(), &bytes[..])), Ok(val)); + } } diff --git a/binary_port/src/lib.rs b/binary_port/src/lib.rs index 521927ce87..58a7dbb131 100644 --- a/binary_port/src/lib.rs +++ b/binary_port/src/lib.rs @@ -51,6 +51,6 @@ pub use state_request::GlobalStateRequest; pub use type_wrappers::{ AccountInformation, AddressableEntityInformation, ConsensusStatus, ConsensusValidatorChanges, ContractInformation, DictionaryQueryResult, EvmCallRequest, EvmCallResult, GetTrieFullResult, - LastProgress, NetworkName, ReactorStateName, RewardResponse, TransactionWithExecutionInfo, - Uptime, ValueWithProof, + LastProgress, NetworkName, ReactorStateName, RewardResponse, SimulationRequest, + SimulationResult, TransactionWithExecutionInfo, Uptime, ValueWithProof, }; diff --git a/binary_port/src/response_type.rs b/binary_port/src/response_type.rs index 6e800106fd..efaf0181ec 100644 --- a/binary_port/src/response_type.rs +++ b/binary_port/src/response_type.rs @@ -20,11 +20,12 @@ use crate::{ node_status::NodeStatus, speculative_execution_result::SpeculativeExecutionResult, type_wrappers::{ - ConsensusStatus, ConsensusValidatorChanges, EvmCallResult, GetTrieFullResult, LastProgress, - NetworkName, ReactorStateName, RewardResponse, + ConsensusStatus, ConsensusValidatorChanges, GetTrieFullResult, LastProgress, NetworkName, + ReactorStateName, RewardResponse, }, AccountInformation, AddressableEntityInformation, BalanceResponse, ContractInformation, - DictionaryQueryResult, RecordId, TransactionWithExecutionInfo, Uptime, ValueWithProof, + DictionaryQueryResult, RecordId, SimulationResult, TransactionWithExecutionInfo, Uptime, + ValueWithProof, }; /// A type of the payload being returned in a binary response. @@ -119,8 +120,8 @@ pub enum ResponseType { PackageWithProof, /// Addressable entity information. AddressableEntityInformation, - /// Result of a read-only EVM call. - EvmCallResult, + /// Result of a simulation. + SimulationResult, } impl ResponseType { @@ -230,7 +231,7 @@ impl TryFrom for ResponseType { x if x == ResponseType::AddressableEntityInformation as u8 => { Ok(ResponseType::AddressableEntityInformation) } - x if x == ResponseType::EvmCallResult as u8 => Ok(ResponseType::EvmCallResult), + x if x == ResponseType::SimulationResult as u8 => Ok(ResponseType::SimulationResult), _ => Err(()), } } @@ -293,7 +294,7 @@ impl fmt::Display for ResponseType { ResponseType::AddressableEntityInformation => { write!(f, "AddressableEntityInformation") } - ResponseType::EvmCallResult => write!(f, "EvmCallResult"), + ResponseType::SimulationResult => write!(f, "SimulationResult"), } } } @@ -456,8 +457,8 @@ impl PayloadEntity for AddressableEntityInformation { const RESPONSE_TYPE: ResponseType = ResponseType::AddressableEntityInformation; } -impl PayloadEntity for EvmCallResult { - const RESPONSE_TYPE: ResponseType = ResponseType::EvmCallResult; +impl PayloadEntity for SimulationResult { + const RESPONSE_TYPE: ResponseType = ResponseType::SimulationResult; } impl PayloadEntity for Box @@ -479,4 +480,12 @@ mod tests { let val = ResponseType::random(rng); assert_eq!(ResponseType::try_from(val as u8), Ok(val)); } + + #[test] + fn simulation_result_response_type_roundtrip() { + assert_eq!( + ResponseType::try_from(ResponseType::SimulationResult as u8), + Ok(ResponseType::SimulationResult) + ); + } } diff --git a/binary_port/src/type_wrappers.rs b/binary_port/src/type_wrappers.rs index bbba222f6c..df37b7acdc 100644 --- a/binary_port/src/type_wrappers.rs +++ b/binary_port/src/type_wrappers.rs @@ -2,7 +2,7 @@ use core::{convert::TryFrom, num::TryFromIntError, time::Duration}; use std::collections::BTreeMap; use casper_types::{ - bytesrepr::{self, Bytes, FromBytes, ToBytes}, + bytesrepr::{self, Bytes, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, contracts::ContractHash, evm, global_state::TrieMerkleProof, @@ -13,8 +13,15 @@ use casper_types::{ }; use serde::Serialize; +use crate::speculative_execution_result::SpeculativeExecutionResult; + use super::GlobalStateQueryResult; +const SIMULATION_REQUEST_EVM_CALL_TAG: u8 = 0; +const SIMULATION_REQUEST_TRANSACTION_TAG: u8 = 1; +const SIMULATION_RESULT_EVM_CALL_TAG: u8 = 0; +const SIMULATION_RESULT_TRANSACTION_TAG: u8 = 1; + // `bytesrepr` implementations for type wrappers are repetitive, hence this macro helper. We should // get rid of this after we introduce the proper "bytesrepr-derive" proc macro. macro_rules! impl_bytesrepr_for_type_wrapper { @@ -210,6 +217,135 @@ impl FromBytes for EvmCallResult { } } +/// Request for a simulation against node state. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum SimulationRequest { + /// Read-only EVM call. + EvmCall(EvmCallRequest), + /// Transaction simulation, reserved for future support. + Transaction(Transaction), +} + +impl SimulationRequest { + #[cfg(test)] + pub(crate) fn random(rng: &mut casper_types::testing::TestRng) -> Self { + use rand::Rng; + + if rng.gen() { + SimulationRequest::EvmCall(EvmCallRequest::new( + evm::Address::new(rng.gen()), + rng.gen::().then(|| evm::Address::new(rng.gen())), + U256::from_big_endian(&rng.gen::<[u8; 32]>()), + Bytes::from(rng.random_vec(0..64)), + rng.gen(), + )) + } else { + SimulationRequest::Transaction(Transaction::random(rng)) + } + } +} + +impl ToBytes for SimulationRequest { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + + match self { + SimulationRequest::EvmCall(request) => request.serialized_length(), + SimulationRequest::Transaction(transaction) => transaction.serialized_length(), + } + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + match self { + SimulationRequest::EvmCall(request) => { + SIMULATION_REQUEST_EVM_CALL_TAG.write_bytes(writer)?; + request.write_bytes(writer) + } + SimulationRequest::Transaction(transaction) => { + SIMULATION_REQUEST_TRANSACTION_TAG.write_bytes(writer)?; + transaction.write_bytes(writer) + } + } + } +} + +impl FromBytes for SimulationRequest { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + match tag { + SIMULATION_REQUEST_EVM_CALL_TAG => { + let (request, remainder) = EvmCallRequest::from_bytes(remainder)?; + Ok((SimulationRequest::EvmCall(request), remainder)) + } + SIMULATION_REQUEST_TRANSACTION_TAG => { + let (transaction, remainder) = Transaction::from_bytes(remainder)?; + Ok((SimulationRequest::Transaction(transaction), remainder)) + } + _ => Err(bytesrepr::Error::Formatting), + } + } +} + +/// Result of a simulation against node state. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum SimulationResult { + /// Result of a read-only EVM call. + EvmCall(EvmCallResult), + /// Result of transaction simulation, reserved for future support. + Transaction(SpeculativeExecutionResult), +} + +impl ToBytes for SimulationResult { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + + match self { + SimulationResult::EvmCall(result) => result.serialized_length(), + SimulationResult::Transaction(result) => result.serialized_length(), + } + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + match self { + SimulationResult::EvmCall(result) => { + SIMULATION_RESULT_EVM_CALL_TAG.write_bytes(writer)?; + result.write_bytes(writer) + } + SimulationResult::Transaction(result) => { + SIMULATION_RESULT_TRANSACTION_TAG.write_bytes(writer)?; + result.write_bytes(writer) + } + } + } +} + +impl FromBytes for SimulationResult { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + match tag { + SIMULATION_RESULT_EVM_CALL_TAG => { + let (result, remainder) = EvmCallResult::from_bytes(remainder)?; + Ok((SimulationResult::EvmCall(result), remainder)) + } + SIMULATION_RESULT_TRANSACTION_TAG => { + let (result, remainder) = SpeculativeExecutionResult::from_bytes(remainder)?; + Ok((SimulationResult::Transaction(result), remainder)) + } + _ => Err(bytesrepr::Error::Formatting), + } + } +} + /// Type representing uptime. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub struct Uptime(u64); @@ -971,6 +1107,44 @@ mod tests { )); } + #[test] + fn simulation_request_evm_call_roundtrip() { + let rng = &mut TestRng::new(); + bytesrepr::test_serialization_roundtrip(&SimulationRequest::EvmCall(EvmCallRequest::new( + evm::Address::new(rng.gen()), + rng.gen::().then(|| evm::Address::new(rng.gen())), + U256::from_big_endian(&rng.gen::<[u8; 32]>()), + Bytes::from(rng.random_vec(0..64)), + rng.gen(), + ))); + } + + #[test] + fn simulation_request_transaction_roundtrip() { + let rng = &mut TestRng::new(); + bytesrepr::test_serialization_roundtrip(&SimulationRequest::Transaction( + Transaction::random(rng), + )); + } + + #[test] + fn simulation_result_evm_call_roundtrip() { + let rng = &mut TestRng::new(); + bytesrepr::test_serialization_roundtrip(&SimulationResult::EvmCall(EvmCallResult::new( + evm::Receipt::random(rng).status, + Bytes::from(rng.random_vec(0..64)), + rng.gen(), + ))); + } + + #[test] + fn simulation_result_transaction_roundtrip() { + let rng = &mut TestRng::new(); + bytesrepr::test_serialization_roundtrip(&SimulationResult::Transaction( + SpeculativeExecutionResult::random(rng), + )); + } + #[test] fn dictionary_query_result_roundtrip() { let rng = &mut TestRng::new(); diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs index a114fe5dfd..e28a61027a 100644 --- a/executor/evm/src/request.rs +++ b/executor/evm/src/request.rs @@ -1,6 +1,6 @@ //! Public execution request types. -use casper_types::evm; +use casper_types::{evm, U256}; use crate::tx; @@ -36,7 +36,7 @@ pub struct CallRequest { /// Target account, or `None` for contract creation. pub to: Option, /// Amount of wei to send, encoded as a big-endian 256-bit word. - pub value: evm::Hash, + pub value: U256, /// Calldata or contract init code. pub input: Vec, /// Gas available for execution. diff --git a/executor/evm/src/tx.rs b/executor/evm/src/tx.rs index 59b267717a..557a1ab198 100644 --- a/executor/evm/src/tx.rs +++ b/executor/evm/src/tx.rs @@ -55,7 +55,7 @@ pub(crate) fn build_tx_env(config: &evm::EvmConfig, kind: &ExecuteKind) -> Resul Some(address) => TxKind::Call(to_revm_address(address)), None => TxKind::Create, }) - .value(to_revm_hash_word(call.value)) + .value(to_revm_u256(call.value)) .data(Bytes::from(call.input.clone())) .nonce(call.nonce) .chain_id(Some(config.chain_id)) diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index c447a71e23..e25cd0c486 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -19,7 +19,7 @@ use casper_storage::{ use casper_types::{ evm, BlockHash, CLValue, ChainspecRegistry, Digest, GenesisAccount, GenesisConfig, HoldBalanceHandling, Key, Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, - StoredValue, SystemConfig, Timestamp, WasmConfig, U512, + StoredValue, SystemConfig, Timestamp, U256 as CasperU256, WasmConfig, U512, }; use revm::bytecode::opcode; @@ -158,7 +158,7 @@ fn call_request( from: evm::Address, to: Option, input: Vec, - value: evm::Hash, + value: CasperU256, ) -> ExecuteRequest { ExecuteRequest { block: block(), @@ -180,7 +180,7 @@ fn checked_call_request( from: evm::Address, to: Option, input: Vec, - value: evm::Hash, + value: CasperU256, ) -> ExecuteRequest { ExecuteRequest { block: block(), @@ -208,7 +208,7 @@ fn execute_call>( let outcome = executor .execute( tracking_copy, - call_request(from, to, input, evm::Hash::ZERO), + call_request(from, to, input, CasperU256::zero()), ) .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); @@ -381,14 +381,14 @@ fn blockhash_uses_supplied_provider() { let outcome = executor .execute_with_block_hash_provider( &mut tracking_copy, - call_request(from, Some(contract), Vec::new(), evm::Hash::ZERO), + call_request(from, Some(contract), Vec::new(), CasperU256::zero()), &block_hash_provider, ) .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); assert_eq!(outcome.output.as_slice(), evm::Hash::ZERO.as_bytes()); - let mut too_old_request = call_request(from, Some(contract), Vec::new(), evm::Hash::ZERO); + let mut too_old_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); too_old_request.block.number = 258; let outcome = executor .execute_with_block_hash_provider(&mut tracking_copy, too_old_request, &block_hash_provider) @@ -396,7 +396,7 @@ fn blockhash_uses_supplied_provider() { assert_eq!(outcome.status, ExecutionStatus::Success); assert_eq!(outcome.output.as_slice(), evm::Hash::ZERO.as_bytes()); - let mut historical_request = call_request(from, Some(contract), Vec::new(), evm::Hash::ZERO); + let mut historical_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); historical_request.block.number = 2; let outcome = executor .execute_with_block_hash_provider( @@ -455,7 +455,7 @@ fn erc20_and_native_purse_balances_update() { let (mut tracking_copy, _tempdir) = tracking_copy(); seed_evm_balance(&mut tracking_copy, owner, U512::from(1_000u64)); - let transfer_value = word(250); + let transfer_value = CasperU256::from(250); let outcome = executor .execute( &mut tracking_copy, @@ -741,7 +741,7 @@ fn checked_calls_enforce_transaction_validation() { let recipient = evm::Address::new([2; 20]); let (mut tracking_copy, _tempdir) = tracking_copy(); - let request = checked_call_request(from, Some(recipient), Vec::new(), word(1)); + let request = checked_call_request(from, Some(recipient), Vec::new(), CasperU256::from(1)); assert!(matches!( executor.execute(&mut tracking_copy, request), Err(Error::Revm(_)) diff --git a/node/BINARY_PORT_PROTOCOL.md b/node/BINARY_PORT_PROTOCOL.md index 52ce277754..33a4a33dac 100644 --- a/node/BINARY_PORT_PROTOCOL.md +++ b/node/BINARY_PORT_PROTOCOL.md @@ -34,7 +34,7 @@ The Binary Port communication protocol is binary and supports a long lived tcp c ## Request model details -Currently, there are 3 supported types of requests, but the request model can be extended. The request types are: +Currently, there are 4 supported types of requests, but the request model can be extended. The request types are: - A `Get` request, which is one of: - A `Record` request asking for a record with an extensible `RecordId` tag and a key @@ -45,3 +45,4 @@ Currently, there are 3 supported types of requests, but the request model can be - A `Trie` request asking for a trie given a `Digest` - A `TryAcceptTransaction` request for a transaction to be accepted and executed - A `TrySpeculativeExec` request for a transaction to be executed speculatively, without saving the transaction effects in global state +- A `Simulate` request for VM simulation calls, currently supporting read-only EVM calls without saving effects in global state diff --git a/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index 8e742deec8..f025dea5e5 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -17,8 +17,8 @@ use casper_binary_port::{ EntityIdentifier, EraIdentifier, ErrorCode, EvmCallRequest, GetRequest, GetTrieFullResult, GlobalStateEntityQualifier, GlobalStateQueryResult, GlobalStateRequest, InformationRequest, InformationRequestTag, KeyPrefix, NodeStatus, PackageIdentifier, PurseIdentifier, - ReactorStateName, RecordId, ResponseType, RewardResponse, TransactionWithExecutionInfo, - ValueWithProof, + ReactorStateName, RecordId, ResponseType, RewardResponse, SimulationRequest, SimulationResult, + TransactionWithExecutionInfo, ValueWithProof, }; use casper_storage::{ data_access_layer::{ @@ -161,6 +161,7 @@ struct BinaryRequestTerminationDelayValues { get_trie: TimeDiff, accept_transaction: TimeDiff, speculative_exec: TimeDiff, + simulate: TimeDiff, } impl BinaryRequestTerminationDelayValues { @@ -172,6 +173,7 @@ impl BinaryRequestTerminationDelayValues { get_trie: config.get_trie_request_termination_delay, accept_transaction: config.accept_transaction_request_termination_delay, speculative_exec: config.speculative_exec_request_termination_delay, + simulate: config.simulate_request_termination_delay, } } fn get_life_termination_delay(&self, request: &Command) -> TimeDiff { @@ -182,7 +184,7 @@ impl BinaryRequestTerminationDelayValues { Command::Get(GetRequest::Trie { .. }) => self.get_trie, Command::TryAcceptTransaction { .. } => self.accept_transaction, Command::TrySpeculativeExec { .. } => self.speculative_exec, - Command::EvmCall { .. } => self.speculative_exec, + Command::Simulate { .. } => self.simulate, } } } @@ -223,13 +225,21 @@ where } try_speculative_execution(effect_builder, transaction).await } - Command::EvmCall { request } => { - metrics.binary_port_try_speculative_exec_count.inc(); - if !config.allow_request_speculative_exec { - debug!("received an EVM call request while speculative execution is disabled"); + Command::Simulate { request } => { + metrics.binary_port_simulate_count.inc(); + if !config.allow_request_simulate { + debug!("received a simulation request while simulation is disabled"); return BinaryResponse::new_error(ErrorCode::FunctionDisabled); } - try_evm_call(effect_builder, request).await + match request { + SimulationRequest::EvmCall(request) => try_evm_call(effect_builder, request).await, + SimulationRequest::Transaction(_) => { + // POC EVM integration only supports read-only EVM call through Simulate. + // Transaction simulation variants are reserved for future Deploy, V1, and EVM + // transaction work. + BinaryResponse::new_error(ErrorCode::UnsupportedRequest) + } + } } Command::Get(get_req) => { handle_get_request(get_req, effect_builder, config, metrics, protocol_version).await @@ -1411,7 +1421,7 @@ where .evm_call(Box::new(tip), block_hashes, Box::new(request)) .await { - Ok(result) => BinaryResponse::from_value(result), + Ok(result) => BinaryResponse::from_value(SimulationResult::EvmCall(result)), Err(error) => { debug!(%error, "EVM call failed"); BinaryResponse::new_error(ErrorCode::InternalError) diff --git a/node/src/components/binary_port/config.rs b/node/src/components/binary_port/config.rs index fe614b4a21..af8cb059df 100644 --- a/node/src/components/binary_port/config.rs +++ b/node/src/components/binary_port/config.rs @@ -32,6 +32,9 @@ const DEFAULT_ACCEPT_TRANSACTION_REQUEST_TERMINATION_DELAY: &str = "24 seconds"; // Default amount of time which is given to a connection to extend it's lifetime when a valid // [`Command::TrySpeculativeExec`] is sent to the node const DEFAULT_SPECULATIVE_EXEC_REQUEST_TERMINATION_DELAY: &str = "0 seconds"; +// Default amount of time which is given to a connection to extend it's lifetime when a valid +// [`Command::Simulate`] is sent to the node +const DEFAULT_SIMULATE_REQUEST_TERMINATION_DELAY: &str = "0 seconds"; /// Binary port server configuration. #[derive(Clone, DataSize, Debug, Deserialize, Serialize)] @@ -50,6 +53,8 @@ pub struct Config { pub allow_request_get_trie: bool, /// Flag used to enable/disable the [`TrySpeculativeExec`] request. pub allow_request_speculative_exec: bool, + /// Flag used to enable/disable the [`Simulate`] request. + pub allow_request_simulate: bool, /// Maximum size of the binary port message. pub max_message_size_bytes: u32, /// Maximum number of connections to the server. @@ -76,6 +81,9 @@ pub struct Config { // The amount of time which is given to a connection to extend it's lifetime when a valid // [`Command::TrySpeculativeExec`] is sent to the node pub speculative_exec_request_termination_delay: TimeDiff, + // The amount of time which is given to a connection to extend it's lifetime when a valid + // [`Command::Simulate`] is sent to the node + pub simulate_request_termination_delay: TimeDiff, } impl Config { @@ -87,6 +95,7 @@ impl Config { allow_request_get_all_values: false, allow_request_get_trie: false, allow_request_speculative_exec: false, + allow_request_simulate: false, max_message_size_bytes: DEFAULT_MAX_MESSAGE_SIZE, max_connections: DEFAULT_MAX_CONNECTIONS, qps_limit: DEFAULT_QPS_LIMIT, @@ -116,6 +125,10 @@ impl Config { DEFAULT_SPECULATIVE_EXEC_REQUEST_TERMINATION_DELAY, ) .unwrap(), + simulate_request_termination_delay: TimeDiff::from_str( + DEFAULT_SIMULATE_REQUEST_TERMINATION_DELAY, + ) + .unwrap(), } } } diff --git a/node/src/components/binary_port/event.rs b/node/src/components/binary_port/event.rs index 4e7d175964..108675bae7 100644 --- a/node/src/components/binary_port/event.rs +++ b/node/src/components/binary_port/event.rs @@ -3,7 +3,7 @@ use std::{ net::SocketAddr, }; -use casper_binary_port::{BinaryResponse, Command, GetRequest}; +use casper_binary_port::{BinaryResponse, Command, GetRequest, SimulationRequest}; use tokio::net::TcpStream; use crate::effect::Responder; @@ -47,7 +47,14 @@ impl Display for Event { Command::TrySpeculativeExec { transaction, .. } => { write!(f, "try speculative exec ({})", transaction.hash()) } - Command::EvmCall { request } => write!(f, "evm call ({:?})", request.to()), + Command::Simulate { request } => match request { + SimulationRequest::EvmCall(request) => { + write!(f, "simulate evm call ({:?})", request.to()) + } + SimulationRequest::Transaction(transaction) => { + write!(f, "simulate transaction ({})", transaction.hash()) + } + }, }, } } diff --git a/node/src/components/binary_port/metrics.rs b/node/src/components/binary_port/metrics.rs index 0d4ff254cb..8e8fc57c80 100644 --- a/node/src/components/binary_port/metrics.rs +++ b/node/src/components/binary_port/metrics.rs @@ -11,6 +11,9 @@ const BINARY_PORT_TRY_SPECULATIVE_EXEC_COUNT_NAME: &str = "binary_port_try_specu const BINARY_PORT_TRY_SPECULATIVE_EXEC_COUNT_HELP: &str = "number of TrySpeculativeExec queries received"; +const BINARY_PORT_SIMULATE_COUNT_NAME: &str = "binary_port_simulate_count"; +const BINARY_PORT_SIMULATE_COUNT_HELP: &str = "number of Simulate queries received"; + const BINARY_PORT_GET_RECORD_COUNT_NAME: &str = "binary_port_get_record_count"; const BINARY_PORT_GET_RECORD_COUNT_HELP: &str = "number of received Get queries for records"; @@ -36,6 +39,8 @@ pub(crate) struct Metrics { pub(super) binary_port_try_accept_transaction_count: IntCounter, /// Number of `TrySpeculativeExec` queries received. pub(super) binary_port_try_speculative_exec_count: IntCounter, + /// Number of `Simulate` queries received. + pub(super) binary_port_simulate_count: IntCounter, /// Number of `Get::Record` queries received. pub(super) binary_port_get_record_count: IntCounter, /// Number of `Get::Information` queries received. @@ -63,6 +68,11 @@ impl Metrics { BINARY_PORT_TRY_SPECULATIVE_EXEC_COUNT_HELP.to_string(), )?; + let binary_port_simulate_count = IntCounter::new( + BINARY_PORT_SIMULATE_COUNT_NAME.to_string(), + BINARY_PORT_SIMULATE_COUNT_HELP.to_string(), + )?; + let binary_port_get_record_count = IntCounter::new( BINARY_PORT_GET_RECORD_COUNT_NAME.to_string(), BINARY_PORT_GET_RECORD_COUNT_HELP.to_string(), @@ -90,6 +100,7 @@ impl Metrics { registry.register(Box::new(binary_port_try_accept_transaction_count.clone()))?; registry.register(Box::new(binary_port_try_speculative_exec_count.clone()))?; + registry.register(Box::new(binary_port_simulate_count.clone()))?; registry.register(Box::new(binary_port_get_record_count.clone()))?; registry.register(Box::new(binary_port_get_info_count.clone()))?; registry.register(Box::new(binary_port_get_state_count.clone()))?; @@ -99,6 +110,7 @@ impl Metrics { Ok(Metrics { binary_port_try_accept_transaction_count, binary_port_try_speculative_exec_count, + binary_port_simulate_count, binary_port_get_record_count, binary_port_get_info_count, binary_port_get_state_count, @@ -113,6 +125,7 @@ impl Drop for Metrics { fn drop(&mut self) { unregister_metric!(self.registry, self.binary_port_try_accept_transaction_count); unregister_metric!(self.registry, self.binary_port_try_speculative_exec_count); + unregister_metric!(self.registry, self.binary_port_simulate_count); unregister_metric!(self.registry, self.binary_port_get_record_count); unregister_metric!(self.registry, self.binary_port_get_info_count); unregister_metric!(self.registry, self.binary_port_get_state_count); diff --git a/node/src/components/binary_port/tests.rs b/node/src/components/binary_port/tests.rs index cda56ff013..efd8cf91de 100644 --- a/node/src/components/binary_port/tests.rs +++ b/node/src/components/binary_port/tests.rs @@ -6,12 +6,13 @@ use rand::Rng; use serde::Serialize; use casper_binary_port::{ - BinaryResponse, Command, GetRequest, GlobalStateEntityQualifier, GlobalStateRequest, RecordId, + BinaryResponse, Command, EvmCallRequest, GetRequest, GlobalStateEntityQualifier, + GlobalStateRequest, RecordId, SimulationRequest, }; use casper_types::{ - BlockHeader, Digest, GlobalStateIdentifier, KeyTag, PublicKey, Timestamp, Transaction, - TransactionV1, + bytesrepr::Bytes, evm, BlockHeader, Digest, GlobalStateIdentifier, KeyTag, PublicKey, + Timestamp, Transaction, TransactionV1, U256, }; use crate::{ @@ -56,6 +57,7 @@ struct TestCase { allow_request_get_all_values: bool, allow_request_get_trie: bool, allow_request_speculative_exec: bool, + allow_request_simulate: bool, request_generator: Either Command, Command>, } @@ -67,6 +69,7 @@ async fn should_enqueue_requests_for_enabled_functions() { allow_request_get_all_values: ENABLED, allow_request_get_trie: rng.gen(), allow_request_speculative_exec: rng.gen(), + allow_request_simulate: rng.gen(), request_generator: Either::Left(|_| all_values_request()), }; @@ -74,6 +77,7 @@ async fn should_enqueue_requests_for_enabled_functions() { allow_request_get_all_values: rng.gen(), allow_request_get_trie: ENABLED, allow_request_speculative_exec: rng.gen(), + allow_request_simulate: rng.gen(), request_generator: Either::Left(|_| trie_request()), }; @@ -81,13 +85,23 @@ async fn should_enqueue_requests_for_enabled_functions() { allow_request_get_all_values: rng.gen(), allow_request_get_trie: rng.gen(), allow_request_speculative_exec: ENABLED, + allow_request_simulate: rng.gen(), request_generator: Either::Left(try_speculative_exec_request), }; + let simulate_evm_call_enabled = TestCase { + allow_request_get_all_values: rng.gen(), + allow_request_get_trie: rng.gen(), + allow_request_speculative_exec: rng.gen(), + allow_request_simulate: ENABLED, + request_generator: Either::Left(simulate_evm_call_request), + }; + for test_case in [ get_all_values_enabled, get_trie_enabled, try_speculative_exec_enabled, + simulate_evm_call_enabled, ] { let (_, mut runner) = run_test_case(test_case, &mut rng).await; @@ -111,6 +125,7 @@ async fn should_return_error_for_disabled_functions() { allow_request_get_all_values: DISABLED, allow_request_get_trie: rng.gen(), allow_request_speculative_exec: rng.gen(), + allow_request_simulate: rng.gen(), request_generator: Either::Left(|_| all_values_request()), }; @@ -118,6 +133,7 @@ async fn should_return_error_for_disabled_functions() { allow_request_get_all_values: rng.gen(), allow_request_get_trie: DISABLED, allow_request_speculative_exec: rng.gen(), + allow_request_simulate: rng.gen(), request_generator: Either::Left(|_| trie_request()), }; @@ -125,13 +141,23 @@ async fn should_return_error_for_disabled_functions() { allow_request_get_all_values: rng.gen(), allow_request_get_trie: rng.gen(), allow_request_speculative_exec: DISABLED, + allow_request_simulate: rng.gen(), request_generator: Either::Left(try_speculative_exec_request), }; + let simulate_evm_call_disabled = TestCase { + allow_request_get_all_values: rng.gen(), + allow_request_get_trie: rng.gen(), + allow_request_speculative_exec: rng.gen(), + allow_request_simulate: DISABLED, + request_generator: Either::Left(simulate_evm_call_request), + }; + for test_case in [ get_all_values_disabled, get_trie_disabled, try_speculative_exec_disabled, + simulate_evm_call_disabled, ] { let (receiver, mut runner) = run_test_case(test_case, &mut rng).await; @@ -149,6 +175,32 @@ async fn should_return_error_for_disabled_functions() { } } +#[tokio::test] +async fn should_return_error_for_unsupported_simulation_transaction() { + let mut rng = TestRng::new(); + let test_case = TestCase { + allow_request_get_all_values: rng.gen(), + allow_request_get_trie: rng.gen(), + allow_request_speculative_exec: rng.gen(), + allow_request_simulate: ENABLED, + request_generator: Either::Left(simulate_transaction_request), + }; + + let (receiver, mut runner) = run_test_case(test_case, &mut rng).await; + + let result = tokio::select! { + result = receiver => result.expect("expected successful response"), + _ = runner.crank_until( + &mut rng, + got_contract_runtime_request, + Duration::from_secs(10), + ) => { + panic!("expected receiver to complete first") + } + }; + assert_eq!(result.error_code(), ErrorCode::UnsupportedRequest as u16) +} + #[tokio::test] async fn should_return_empty_response_when_fetching_empty_key() { let mut rng = TestRng::new(); @@ -159,6 +211,7 @@ async fn should_return_empty_response_when_fetching_empty_key() { allow_request_get_all_values: DISABLED, allow_request_get_trie: DISABLED, allow_request_speculative_exec: DISABLED, + allow_request_simulate: DISABLED, request_generator: Either::Right(request), }) .collect(); @@ -186,6 +239,7 @@ async fn run_test_case( allow_request_get_all_values, allow_request_get_trie, allow_request_speculative_exec, + allow_request_simulate, request_generator, }: TestCase, rng: &mut TestRng, @@ -198,6 +252,7 @@ async fn run_test_case( allow_request_get_all_values, allow_request_get_trie, allow_request_speculative_exec, + allow_request_simulate, max_message_size_bytes: 1024, max_connections: 2, ..Default::default() @@ -451,6 +506,24 @@ fn try_speculative_exec_request(rng: &mut TestRng) -> Command { } } +fn simulate_evm_call_request(rng: &mut TestRng) -> Command { + Command::Simulate { + request: SimulationRequest::EvmCall(EvmCallRequest::new( + evm::Address::new(rng.gen()), + rng.gen::().then(|| evm::Address::new(rng.gen())), + U256::from_big_endian(&rng.gen::<[u8; 32]>()), + Bytes::from(rng.random_vec(0..64)), + rng.gen(), + )), + } +} + +fn simulate_transaction_request(rng: &mut TestRng) -> Command { + Command::Simulate { + request: SimulationRequest::Transaction(Transaction::V1(TransactionV1::random(rng))), + } +} + fn got_contract_runtime_request(event: &Event) -> bool { matches!(event, Event::ContractRuntimeRequest(_)) } diff --git a/node/src/reactor/main_reactor/tests/fixture.rs b/node/src/reactor/main_reactor/tests/fixture.rs index 106aaa5cee..82f2b9e377 100644 --- a/node/src/reactor/main_reactor/tests/fixture.rs +++ b/node/src/reactor/main_reactor/tests/fixture.rs @@ -402,6 +402,7 @@ impl TestFixture { allow_request_get_all_values: true, allow_request_get_trie: true, allow_request_speculative_exec: true, + allow_request_simulate: true, ..Default::default() }, ..Default::default() diff --git a/resources/integration-test/config-example.toml b/resources/integration-test/config-example.toml index 688dbc77a1..e4e2a997ae 100644 --- a/resources/integration-test/config-example.toml +++ b/resources/integration-test/config-example.toml @@ -334,6 +334,9 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false +# Flag that enables the `Simulate` request. Disabled by default. +allow_request_simulate = false + # Maximum size of a message in bytes. max_message_size_bytes = 134_217_728 @@ -371,6 +374,10 @@ accept_transaction_request_termination_delay = '24 seconds' #[`Command::TrySpeculativeExec`] is sent to the node speculative_exec_request_termination_delay = '0 seconds' +#The amount of time which is given to a connection to extend it's lifetime when a valid +#[`Command::Simulate`] is sent to the node +simulate_request_termination_delay = '0 seconds' + # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/local/config.toml b/resources/local/config.toml index 7d8335f199..959ba00345 100644 --- a/resources/local/config.toml +++ b/resources/local/config.toml @@ -335,6 +335,9 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false +# Flag that enables the `Simulate` request. Disabled by default. +allow_request_simulate = false + # Maximum size of a message in bytes. max_message_size_bytes = 4_194_304 @@ -372,6 +375,10 @@ accept_transaction_request_termination_delay = '24 seconds' #[`Command::TrySpeculativeExec`] is sent to the node speculative_exec_request_termination_delay = '0 seconds' +#The amount of time which is given to a connection to extend it's lifetime when a valid +#[`Command::Simulate`] is sent to the node +simulate_request_termination_delay = '0 seconds' + # ============================================== # Configuration options for the REST HTTP server # ============================================== diff --git a/resources/mainnet/config-example.toml b/resources/mainnet/config-example.toml index fcf4fc82d6..958658989a 100644 --- a/resources/mainnet/config-example.toml +++ b/resources/mainnet/config-example.toml @@ -334,6 +334,9 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false +# Flag that enables the `Simulate` request. Disabled by default. +allow_request_simulate = false + # Maximum size of a message in bytes. max_message_size_bytes = 134_217_728 @@ -371,6 +374,10 @@ accept_transaction_request_termination_delay = '24 seconds' #[`Command::TrySpeculativeExec`] is sent to the node speculative_exec_request_termination_delay = '0 seconds' +#The amount of time which is given to a connection to extend it's lifetime when a valid +#[`Command::Simulate`] is sent to the node +simulate_request_termination_delay = '0 seconds' + # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/production/config-example.toml b/resources/production/config-example.toml index 92343bb95d..4efe3e3117 100644 --- a/resources/production/config-example.toml +++ b/resources/production/config-example.toml @@ -334,6 +334,9 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false +# Flag that enables the `Simulate` request. Disabled by default. +allow_request_simulate = false + # Maximum size of a message in bytes. max_message_size_bytes = 134_217_728 @@ -371,6 +374,10 @@ accept_transaction_request_termination_delay = '24 seconds' #[`Command::TrySpeculativeExec`] is sent to the node speculative_exec_request_termination_delay = '0 seconds' +#The amount of time which is given to a connection to extend it's lifetime when a valid +#[`Command::Simulate`] is sent to the node +simulate_request_termination_delay = '0 seconds' + # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/testnet/config-example.toml b/resources/testnet/config-example.toml index d7a5c4b50b..bd957db868 100644 --- a/resources/testnet/config-example.toml +++ b/resources/testnet/config-example.toml @@ -334,6 +334,9 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false +# Flag that enables the `Simulate` request. Disabled by default. +allow_request_simulate = false + # Maximum size of a message in bytes. max_message_size_bytes = 134_217_728 @@ -371,6 +374,10 @@ accept_transaction_request_termination_delay = '24 seconds' #[`Command::TrySpeculativeExec`] is sent to the node speculative_exec_request_termination_delay = '0 seconds' +#The amount of time which is given to a connection to extend it's lifetime when a valid +#[`Command::Simulate`] is sent to the node +simulate_request_termination_delay = '0 seconds' + # ============================================== # Configuration options for the REST HTTP server diff --git a/types/src/evm/eth_u256.rs b/types/src/evm/eth_u256.rs new file mode 100644 index 0000000000..dc04643ae2 --- /dev/null +++ b/types/src/evm/eth_u256.rs @@ -0,0 +1,216 @@ +use alloc::{ + format, + string::{String, ToString}, +}; +use core::fmt::{self, Display, Formatter}; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::U256; + +const ETH_U256_BYTES: usize = 32; + +/// A 256-bit unsigned integer encoded as an Ethereum JSON-RPC quantity. +/// +/// Human-readable serializers use canonical `0x`-prefixed hexadecimal without +/// leading zeroes. Binary serializers delegate to [`U256`]. +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +pub struct EthU256(U256); + +impl EthU256 { + /// The zero quantity. + pub const ZERO: EthU256 = EthU256(U256::MIN); + + /// Returns the underlying 256-bit integer. + pub fn value(self) -> U256 { + self.0 + } + + /// Converts this value into a `u64` if it fits. + pub fn as_u64(self) -> Result { + if self.0 > U256::from(u64::MAX) { + Err("quantity exceeds u64") + } else { + Ok(self.0.low_u64()) + } + } + + fn to_quantity_hex(self) -> String { + if self.0.is_zero() { + return "0x0".to_string(); + } + let mut bytes = [0u8; ETH_U256_BYTES]; + self.0.to_big_endian(&mut bytes); + let first_non_zero = bytes + .iter() + .position(|byte| *byte != 0) + .expect("non-zero U256 has a non-zero byte"); + let hex = base16::encode_lower(&bytes[first_non_zero..]); + format!("0x{}", hex.trim_start_matches('0')) + } +} + +impl From for EthU256 { + fn from(value: U256) -> Self { + EthU256(value) + } +} + +impl From for U256 { + fn from(value: EthU256) -> Self { + value.value() + } +} + +impl From for EthU256 { + fn from(value: u8) -> Self { + EthU256(U256::from(value)) + } +} + +impl From for EthU256 { + fn from(value: u64) -> Self { + EthU256(U256::from(value)) + } +} + +impl From for EthU256 { + fn from(value: u128) -> Self { + EthU256(U256::from(value)) + } +} + +impl From for EthU256 { + fn from(value: usize) -> Self { + EthU256(U256::from(value)) + } +} + +impl Display for EthU256 { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.to_quantity_hex()) + } +} + +#[cfg(feature = "json-schema")] +impl JsonSchema for EthU256 { + fn schema_name() -> String { + String::from("EthU256") + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + let schema = gen.subschema_for::(); + let mut schema_object = schema.into_object(); + schema_object.metadata().description = + Some("Ethereum JSON-RPC quantity encoded as canonical 0x-prefixed hexadecimal.".into()); + schema_object.into() + } +} + +impl Serialize for EthU256 { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + serializer.collect_str(self) + } else { + self.0.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for EthU256 { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let value = String::deserialize(deserializer)?; + parse_eth_u256(&value) + .map(EthU256) + .map_err(D::Error::custom) + } else { + U256::deserialize(deserializer).map(EthU256) + } + } +} + +fn parse_eth_u256(value: &str) -> Result { + let hex = value + .strip_prefix("0x") + .ok_or_else(|| "quantity must start with 0x".to_string())?; + if hex.is_empty() { + return Err("quantity must not be empty".to_string()); + } + if hex.len() > ETH_U256_BYTES * 2 { + return Err("quantity exceeds 256 bits".to_string()); + } + let padded = if hex.len() % 2 == 0 { + hex.to_string() + } else { + format!("0{hex}") + }; + let bytes = base16::decode(padded.as_bytes()).map_err(|error| format!("{error}"))?; + Ok(U256::from_big_endian(&bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn human_readable_serde_uses_canonical_ethereum_quantity_hex() { + let value = EthU256::from(U256::from_big_endian(&[0x01, 0x23])); + + let encoded = serde_json::to_string(&value).expect("quantity should serialize"); + assert_eq!(encoded, "\"0x123\""); + + let decoded: EthU256 = serde_json::from_str(&encoded).expect("quantity should deserialize"); + assert_eq!(decoded, value); + } + + #[test] + fn zero_serializes_as_zero_quantity() { + let encoded = serde_json::to_string(&EthU256::ZERO).expect("quantity should serialize"); + assert_eq!(encoded, "\"0x0\""); + } + + #[test] + fn human_readable_serde_accepts_full_width_quantity() { + let encoded = format!("\"0x{}\"", "f".repeat(ETH_U256_BYTES * 2)); + let decoded: EthU256 = serde_json::from_str(&encoded).expect("quantity should deserialize"); + + assert_eq!(decoded.value(), U256::MAX); + } + + #[test] + fn human_readable_serde_rejects_invalid_quantities() { + for encoded in [ + "\"1\"", + "\"0x\"", + "\"0xg\"", + "\"0x10000000000000000000000000000000000000000000000000000000000000000\"", + ] { + serde_json::from_str::(encoded) + .expect_err("invalid quantity should fail to deserialize"); + } + } + + #[test] + fn as_u64_rejects_overflow() { + assert_eq!(EthU256::from(u64::MAX).as_u64(), Ok(u64::MAX)); + assert_eq!( + EthU256::from(U256::from(u64::MAX) + U256::from(1)).as_u64(), + Err("quantity exceeds u64") + ); + } + + #[test] + fn non_human_readable_serde_roundtrip() { + let value = EthU256::from(U256::from_big_endian(&[0xab; ETH_U256_BYTES])); + let encoded = bincode::serialize(&value).expect("quantity should serialize"); + let decoded: EthU256 = bincode::deserialize(&encoded).expect("quantity should deserialize"); + + assert_eq!(decoded, value); + } +} From 950a0ee5f19b35cefd1184b232d5bba1cf9f285a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Mon, 11 May 2026 16:04:05 +0200 Subject: [PATCH 08/17] Document simulation config for EVM devnet --- EVM.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/EVM.md b/EVM.md index 007fde3b62..45d343fc49 100644 --- a/EVM.md +++ b/EVM.md @@ -394,8 +394,8 @@ The binary-port request carries: - input bytes, - gas limit. -Node handles the request only when speculative execution is enabled for the -binary port. Contract runtime checks out state at the requested/latest block, +Node handles the request only when simulation is enabled for the binary port. +Contract runtime checks out state at the requested/latest block, runs `casper-executor-evm` with: - `ExecuteKind::Call`, @@ -484,7 +484,18 @@ cargo build -p casper-sidecar The devnet tool needs a custom asset named `evm` that points at the debug node and sidecar binaries built above, plus the local chainspec and config files -from this workspace. +from this workspace. Use a node config where +`[binary_port_server].allow_request_simulate = true`; the checked-in local +config defaults this to `false`, so copy `resources/local/config.toml` and +enable it in the copy used for this custom asset. + +For example: + +```bash +export EVM_DEVNET_NODE_CONFIG=/tmp/casper-node-evm-devnet-config.toml +cp "$CASPER_NODE_WORKSPACE/resources/local/config.toml" "$EVM_DEVNET_NODE_CONFIG" +# Edit $EVM_DEVNET_NODE_CONFIG so allow_request_simulate = true. +``` From a separate `casper-devnet` checkout, register the asset with: @@ -494,7 +505,7 @@ cargo run -- assets add evm \ --casper-node "$CASPER_NODE_WORKSPACE/target/debug/casper-node" \ --casper-sidecar "$CASPER_SIDECAR_WORKSPACE/target/debug/casper-sidecar" \ --chainspec "$CASPER_NODE_WORKSPACE/resources/local/chainspec.toml" \ - --node-config "$CASPER_NODE_WORKSPACE/resources/local/config.toml" \ + --node-config "$EVM_DEVNET_NODE_CONFIG" \ --sidecar-config "$CASPER_SIDECAR_WORKSPACE/resources/example_configs/default_rpc_only_config.toml" ``` @@ -505,7 +516,7 @@ casper-devnet assets add evm \ --casper-node "$CASPER_NODE_WORKSPACE/target/debug/casper-node" \ --casper-sidecar "$CASPER_SIDECAR_WORKSPACE/target/debug/casper-sidecar" \ --chainspec "$CASPER_NODE_WORKSPACE/resources/local/chainspec.toml" \ - --node-config "$CASPER_NODE_WORKSPACE/resources/local/config.toml" \ + --node-config "$EVM_DEVNET_NODE_CONFIG" \ --sidecar-config "$CASPER_SIDECAR_WORKSPACE/resources/example_configs/default_rpc_only_config.toml" ``` @@ -700,7 +711,7 @@ Node workspace: ```bash cargo check -p casper-node --bin casper-node -cargo test -p casper-binary-port evm_call --lib +cargo test -p casper-binary-port simulation --lib ``` Sidecar workspace: From 937a955bfedbc5699be648574cb1d13c5d4ab7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Tue, 12 May 2026 12:00:04 +0200 Subject: [PATCH 09/17] EVM events observability WIP --- EVM.md | 54 +++++++++++++++++++++-- smart_contracts/evm_contracts/Counter.sol | 3 ++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/EVM.md b/EVM.md index 45d343fc49..b9f1e8037b 100644 --- a/EVM.md +++ b/EVM.md @@ -48,6 +48,8 @@ Implemented in the sidecar workspace for validation: `eth_chainId`, `eth_blockNumber`, `eth_getBlockByNumber`, `eth_getTransactionCount`, `eth_sendRawTransaction`, `eth_getTransactionReceipt`, and `eth_call`. +- `eth_getTransactionReceipt` projects logs stored in + `ExecutionResult::Evm` as Ethereum receipt log entries. - Development-only Cargo patches pointing sidecar at this node workspace for unreleased `casper-types` and `casper-binary-port` changes. @@ -56,6 +58,9 @@ Not implemented yet: - Native Ethereum JSON-RPC in node. - `eth_estimateGas`, `eth_getBalance`, `eth_getCode`, historical `eth_call`, `eth_getLogs`, or `eth_getTransactionByHash`. +- Ethereum filter and subscription methods, including `eth_newFilter`, + `eth_getFilterChanges`, `eth_getFilterLogs`, `eth_uninstallFilter`, and + `eth_subscribe`. - [EIP-4844][eip-4844] blob transactions. - [EIP-7702][eip-7702] set-code transactions. - Non-empty [EIP-2930][eip-2930]/[EIP-1559][eip-1559] access lists. @@ -625,7 +630,7 @@ Expected output: ```text Deployer: 0x24790C4849cCAE43c0c1749e2C5b8d00Cc63AB80 Deployed to: 0x6c0704679CA22b83778Ef815607359cf6F5352B6 -Transaction hash: 0xa86146276e1cc132ddb750e9e053c3e7a1381222104cefb0c6c567556e5e9198 +Transaction hash: ``` Forge may create local `cache/` and `out/` directories. They are build @@ -636,7 +641,7 @@ The corresponding receipt should contain: ```text status 0x1 contractAddress 0x6c0704679ca22b83778ef815607359cf6f5352b6 -gasUsed 0x262ef +gasUsed effectiveGasPrice 0xf4240 ``` @@ -657,7 +662,9 @@ Expected output: ### Increment Counter ```bash -cast send 0x6c0704679CA22b83778Ef815607359cf6F5352B6 \ +export COUNTER_ADDRESS=0x6c0704679CA22b83778Ef815607359cf6F5352B6 + +cast send "$COUNTER_ADDRESS" \ 'increment()' \ --rpc-url http://127.0.0.1:11101/rpc \ --private-key 0xb6cc5d5faa7c3c37db4bf9a1566023aaa9a1d716fe78ed1a6fb79a690b9400e8 \ @@ -673,11 +680,46 @@ Expected receipt highlights: status 1 (success) type 0 effectiveGasPrice 1000000 -gasUsed 43803 +gasUsed to 0x6c0704679CA22b83778Ef815607359cf6F5352B6 transactionHash 0x042ff975ec4b8fa8012f486bb7bd930e69978782b8b3c107ca2a276a43d7f293 ``` +`increment()` emits: + +```solidity +event CounterIncremented(address indexed caller, uint256 newValue); +``` + +For the deterministic devnet key, the increment receipt should include one log +emitted by `$COUNTER_ADDRESS`. Sidecar currently exposes emitted events through +`eth_getTransactionReceipt`, so receipt-oriented tooling can see and verify the +event: + +```bash +export INCREMENT_TX_HASH=0x042ff975ec4b8fa8012f486bb7bd930e69978782b8b3c107ca2a276a43d7f293 + +cast receipt "$INCREMENT_TX_HASH" \ + --rpc-url http://127.0.0.1:11101/rpc \ + --json | jq '.logs[0]' + +cast sig-event 'CounterIncremented(address,uint256)' +``` + +Expected event checks: + +```text +log.address == $COUNTER_ADDRESS +log.topics[0] == 0x59950fb23669ee30425f6d79758e75fae698a6c88b2982f2980638d8bcd9397d +log.topics[1] == 0x00000000000000000000000024790c4849ccae43c0c1749e2c5b8d00cc63ab80 +log.data == 0x0000000000000000000000000000000000000000000000000000000000000001 +``` + +That is enough for tools that validate a known transaction receipt. Generic +Ethereum event discovery, for example `cast logs`, ethers.js filters, or +web3.js filter polling, also needs sidecar support for `eth_getLogs` and the +filter/subscription RPCs listed in the current caveats. + ### Read Counter Again ```bash @@ -727,6 +769,10 @@ cargo build -p casper-sidecar - `casper-devnet` SSE parsing is not yet updated for new EVM variants. - `eth_getBlockByNumber` currently returns enough typed fields for Foundry polling, but it is not a complete Ethereum block projection. +- `eth_getBlockByNumber` currently returns placeholder block-level + `logsBloom`, `receiptsRoot`, and `gasUsed` values. Receipt-level log data is + available through `eth_getTransactionReceipt`, but block-level receipt-root + verification is not implemented. - Sidecar derives receipt fields from stored `ExecutionResult::Evm` and block metadata; efficient historical log queries are not implemented. - EVM call support is latest/pending only in sidecar. diff --git a/smart_contracts/evm_contracts/Counter.sol b/smart_contracts/evm_contracts/Counter.sol index 2006062208..08ddf9ac14 100644 --- a/smart_contracts/evm_contracts/Counter.sol +++ b/smart_contracts/evm_contracts/Counter.sol @@ -4,8 +4,11 @@ pragma solidity ^0.8.24; contract Counter { uint256 private counter; + event CounterIncremented(address indexed caller, uint256 newValue); + function increment() external payable returns (uint256) { counter += 1; + emit CounterIncremented(msg.sender, counter); return counter; } From 0cad1af24e94b7fd5e82acede2948a6eff2e15e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Tue, 12 May 2026 14:44:23 +0200 Subject: [PATCH 10/17] Unify EVM state types and storage words --- EVM.md | 29 ++-- binary_port/src/key_prefix.rs | 21 ++- execution_engine/src/runtime_context/mod.rs | 4 +- executor/evm/src/db.rs | 31 ++-- executor/evm/src/outcome.rs | 2 +- executor/evm/src/state.rs | 32 ++-- executor/evm/src/tx.rs | 13 +- executor/evm/tests/executor.rs | 62 ++++--- .../components/transaction_acceptor/tests.rs | 5 +- .../main_reactor/tests/transactions.rs | 22 ++- storage/src/data_access_layer/balance.rs | 6 +- storage/src/data_access_layer/key_prefix.rs | 32 +++- storage/src/global_state/state/mod.rs | 8 +- storage/src/system/transfer.rs | 6 +- storage/src/tracking_copy/byte_size.rs | 4 +- storage/src/tracking_copy/mod.rs | 12 +- types/src/byte_code.rs | 25 ++- types/src/evm.rs | 10 +- types/src/evm/account.rs | 94 +++-------- types/src/evm/evm_addr.rs | 112 +++++++++++++ types/src/evm/evm_value.rs | 109 +++++++++++++ types/src/evm/hash.rs | 6 +- types/src/evm/receipt.rs | 8 +- types/src/evm/topic.rs | 152 ++++++++++++++++++ types/src/execution/transform_kind.rs | 14 +- types/src/gens.rs | 34 ++-- types/src/key.rs | 138 ++++++---------- types/src/stored_value.rs | 82 +++------- 28 files changed, 710 insertions(+), 363 deletions(-) create mode 100644 types/src/evm/evm_addr.rs create mode 100644 types/src/evm/evm_value.rs create mode 100644 types/src/evm/topic.rs diff --git a/EVM.md b/EVM.md index b9f1e8037b..ad7da1661d 100644 --- a/EVM.md +++ b/EVM.md @@ -79,11 +79,13 @@ a raw signed RLP blob. The EVM transaction is stored as: - Ethereum signed transaction hash: `hash`. - Exactly one Casper `Approval` containing the Ethereum secp256k1 signature. -`evm::Hash` and `evm::TransactionHash` are `Digest`-backed wrappers, but their -constructors preserve the supplied 32 bytes as raw Ethereum values. They do -not hash the bytes again. `evm::Hash` is used for EVM words, storage keys, -storage values, topics, and bytecode hashes. `evm::TransactionHash` is the -Ethereum transaction hash produced from the signed Ethereum envelope. +`evm::Hash`, `evm::Topic`, and `evm::TransactionHash` are `Digest`-backed +wrappers, but their constructors preserve the supplied 32 bytes as raw +Ethereum values. They do not hash the bytes again. `evm::Hash` is used for +EVM bytecode hashes and other hash-shaped EVM values. `evm::Topic` is used for +EVM log topics. EVM storage slots and storage values use Casper `U256`, matching +revm's `StorageKey = U256` and `StorageValue = U256` boundary. `evm::TransactionHash` +is the Ethereum transaction hash produced from the signed Ethereum envelope. The Ethereum transaction hash remains Ethereum-compatible. For an EVM transaction: @@ -330,9 +332,12 @@ handling. EVM state is stored in Casper global state using typed keys and values: -- `Key::EvmAccount(Address)` stores `StoredValue::EvmAccount(Account)`. -- `Key::EvmByteCode(Hash)` stores `StoredValue::EvmByteCode(ByteCode)`. -- `Key::EvmStorage(StorageAddr)` stores `StoredValue::EvmStorage(StorageValue)`. +- `Key::Evm(EvmAddr::Account(Address))` stores + `StoredValue::Evm(EvmValue::Account(Account))`. +- `Key::Evm(EvmAddr::ByteCode(Hash))` stores + `StoredValue::Evm(EvmValue::ByteCode(ByteCode))`. +- `Key::Evm(EvmAddr::Storage(StorageAddr))` stores + `StoredValue::Evm(EvmValue::Storage(StorageValue))`. An EVM account record contains: @@ -346,10 +351,10 @@ through the account main purse and `Key::Balance(main_purse.addr())`. Genesis does not create EVM account records for Casper genesis accounts. Funding an EVM identity is explicit: a native Casper transfer can use a 20-byte `evm::Address` as its `target` argument when `[evm].enabled = true`. -If `Key::EvmAccount(address)` already exists, the transfer credits that +If `Key::Evm(EvmAddr::Account(address))` already exists, the transfer credits that account's main purse. If it does not exist, the transfer creates -`StoredValue::EvmAccount(Account::new(0, EMPTY_CODE_HASH, -evm::deterministic_purse(address)))`, initializes that deterministic purse +`StoredValue::Evm(EvmValue::Account(Account::new(0, EMPTY_CODE_HASH, +evm::deterministic_purse(address))))`, initializes that deterministic purse with a zero balance, then transfers the requested motes into it. Transfer records keep the Casper transfer schema unchanged: `to` is `None`, and `target` is the EVM account's backing purse. @@ -599,7 +604,7 @@ casper-cli transaction transfer \ ``` The transfer target is encoded as `byte-array[20]`. A successful transfer -creates `Key::EvmAccount(0x24790c...)`, initializes its deterministic backing +creates `Key::Evm(EvmAddr::Account(0x24790c...))`, initializes its deterministic backing purse, and credits it with the transferred motes. The EVM nonce remains `0x0` until the first EVM transaction is executed. diff --git a/binary_port/src/key_prefix.rs b/binary_port/src/key_prefix.rs index acf0e04bcc..70e4c82750 100644 --- a/binary_port/src/key_prefix.rs +++ b/binary_port/src/key_prefix.rs @@ -4,7 +4,7 @@ use casper_types::{ account::AccountHash, bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, contract_messages::TopicNameHash, - evm::Address as EvmAddress, + evm::{Address as EvmAddress, EvmAddr}, system::{auction::BidAddrTag, mint::BalanceHoldAddrTag}, EntityAddr, KeyTag, URefAddr, }; @@ -101,7 +101,8 @@ impl ToBytes for KeyPrefix { entity.write_bytes(writer)?; } KeyPrefix::EvmStorageByAddress(address) => { - writer.push(KeyTag::EvmStorage as u8); + writer.push(KeyTag::Evm as u8); + writer.push(EvmAddr::STORAGE_TAG); address.write_bytes(writer)?; } } @@ -131,7 +132,9 @@ impl ToBytes for KeyPrefix { KeyPrefix::EntryPointsV2ByEntity(entity) => { U8_SERIALIZED_LENGTH + entity.serialized_length() } - KeyPrefix::EvmStorageByAddress(address) => address.serialized_length(), + KeyPrefix::EvmStorageByAddress(address) => { + U8_SERIALIZED_LENGTH + address.serialized_length() + } } } } @@ -191,9 +194,15 @@ impl FromBytes for KeyPrefix { _ => return Err(bytesrepr::Error::Formatting), } } - tag if tag == KeyTag::EvmStorage as u8 => { - let (address, remainder) = EvmAddress::from_bytes(remainder)?; - (KeyPrefix::EvmStorageByAddress(address), remainder) + tag if tag == KeyTag::Evm as u8 => { + let (evm_addr_tag, remainder) = u8::from_bytes(remainder)?; + match evm_addr_tag { + tag if tag == EvmAddr::STORAGE_TAG => { + let (address, remainder) = EvmAddress::from_bytes(remainder)?; + (KeyPrefix::EvmStorageByAddress(address), remainder) + } + _ => return Err(bytesrepr::Error::Formatting), + } } _ => return Err(bytesrepr::Error::Formatting), }; diff --git a/execution_engine/src/runtime_context/mod.rs b/execution_engine/src/runtime_context/mod.rs index c1fbaa2744..a00c15ddf4 100644 --- a/execution_engine/src/runtime_context/mod.rs +++ b/execution_engine/src/runtime_context/mod.rs @@ -765,9 +765,7 @@ where | StoredValue::Prepayment(_) | StoredValue::EntryPoint(_) | StoredValue::RawBytes(_) - | StoredValue::EvmAccount(_) - | StoredValue::EvmByteCode(_) - | StoredValue::EvmStorage(_) => Ok(()), + | StoredValue::Evm(_) => Ok(()), } } diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs index 9ba022e753..7d27931a4e 100644 --- a/executor/evm/src/db.rs +++ b/executor/evm/src/db.rs @@ -57,9 +57,9 @@ where fn basic(&mut self, address: Address) -> Result, Self::Error> { let address = tx::from_revm_address(address); - let key = Key::EvmAccount(address); + let key = Key::Evm(evm::EvmAddr::Account(address)); match self.tracking_copy.read(&key)? { - Some(StoredValue::EvmAccount(account)) => { + Some(StoredValue::Evm(evm::EvmValue::Account(account))) => { let balance = self.balance(account.main_purse())?; Ok(Some(AccountInfo { balance, @@ -71,7 +71,7 @@ where } Some(stored_value) => Err(DbError::TypeMismatch { key: Box::new(key), - expected: "StoredValue::EvmAccount", + expected: "StoredValue::Evm(Account)", found: stored_value.type_name(), }), None => Ok(None), @@ -80,14 +80,21 @@ where fn code_by_hash(&mut self, code_hash: B256) -> Result { let code_hash = tx::from_revm_hash(code_hash); - let key = Key::EvmByteCode(code_hash); + let key = Key::Evm(evm::EvmAddr::ByteCode(code_hash)); match self.tracking_copy.read(&key)? { - Some(StoredValue::EvmByteCode(byte_code)) => { - Ok(Bytecode::new_raw(Bytes::from(byte_code.into_bytes()))) + Some(StoredValue::Evm(evm::EvmValue::ByteCode(byte_code))) => { + if !byte_code.kind().is_evm() { + return Err(DbError::TypeMismatch { + key: Box::new(key), + expected: "EVM bytecode kind", + found: byte_code.kind().to_string(), + }); + } + Ok(Bytecode::new_raw(Bytes::from(byte_code.take_bytes()))) } Some(stored_value) => Err(DbError::TypeMismatch { key: Box::new(key), - expected: "StoredValue::EvmByteCode", + expected: "StoredValue::Evm(ByteCode)", found: stored_value.type_name(), }), None => Ok(Bytecode::default()), @@ -100,13 +107,15 @@ where index: StorageKey, ) -> Result { let address = tx::from_revm_address(address); - let slot = tx::from_revm_u256(index); - let key = Key::EvmStorage(evm::StorageAddr::new(address, slot)); + let slot = tx::from_revm_storage_word(index); + let key = Key::Evm(evm::EvmAddr::Storage(evm::StorageAddr::new(address, slot))); match self.tracking_copy.read(&key)? { - Some(StoredValue::EvmStorage(value)) => Ok(tx::to_revm_hash_word(value.value())), + Some(StoredValue::Evm(evm::EvmValue::Storage(value))) => { + Ok(tx::to_revm_storage_word(value.value())) + } Some(stored_value) => Err(DbError::TypeMismatch { key: Box::new(key), - expected: "StoredValue::EvmStorage", + expected: "StoredValue::Evm(Storage)", found: stored_value.type_name(), }), None => Ok(U256::ZERO), diff --git a/executor/evm/src/outcome.rs b/executor/evm/src/outcome.rs index 5c10f30e89..b76f946759 100644 --- a/executor/evm/src/outcome.rs +++ b/executor/evm/src/outcome.rs @@ -136,7 +136,7 @@ fn from_revm_log(log: &revm::primitives::Log) -> evm::Log { .topics() .iter() .copied() - .map(tx::from_revm_hash) + .map(tx::from_revm_topic) .collect(), data: log.data.data.to_vec().into(), } diff --git a/executor/evm/src/state.rs b/executor/evm/src/state.rs index 231ddd5892..bc6fb7e106 100644 --- a/executor/evm/src/state.rs +++ b/executor/evm/src/state.rs @@ -4,7 +4,7 @@ use casper_storage::{ global_state::{error::Error as GlobalStateError, state::StateReader}, KeyPrefix, TrackingCopy, }; -use casper_types::{evm, CLValue, Key, StoredValue, U512}; +use casper_types::{evm, ByteCode, ByteCodeKind, CLValue, Key, StoredValue, U512}; use revm::{ primitives::{Address, U256}, state::{Account, EvmState}, @@ -64,7 +64,7 @@ where { // Check how to deal with Key::Balance after selfdestruct let address = tx::from_revm_address(address); - let account_key = Key::EvmAccount(address); + let account_key = Key::Evm(evm::EvmAddr::Account(address)); if account.is_selfdestructed() { let main_purse = existing_main_purse(tracking_copy, &account_key)? @@ -77,8 +77,13 @@ where let bytes = code.original_byte_slice(); if !bytes.is_empty() { tracking_copy.write( - Key::EvmByteCode(tx::from_revm_hash(account.info.code_hash)), - StoredValue::EvmByteCode(evm::ByteCode::new(bytes.to_vec())), + Key::Evm(evm::EvmAddr::ByteCode(tx::from_revm_hash( + account.info.code_hash, + ))), + StoredValue::Evm(evm::EvmValue::ByteCode(ByteCode::new( + ByteCodeKind::EvmPrague, + bytes.to_vec(), + ))), ); } } @@ -89,19 +94,26 @@ where tracking_copy.write( account_key, - StoredValue::EvmAccount(evm::Account::new(account.info.nonce, code_hash, main_purse)), + StoredValue::Evm(evm::EvmValue::Account(evm::Account::new( + account.info.nonce, + code_hash, + main_purse, + ))), ); write_balance(tracking_copy, main_purse, account.info.balance)?; for (slot, value) in account.changed_storage_slots() { - let key = Key::EvmStorage(evm::StorageAddr::new(address, tx::from_revm_u256(*slot))); + let key = Key::Evm(evm::EvmAddr::Storage(evm::StorageAddr::new( + address, + tx::from_revm_storage_word(*slot), + ))); if value.present_value.is_zero() { tracking_copy.prune(key); } else { tracking_copy.write( key, - StoredValue::EvmStorage(evm::StorageValue::new(tx::from_revm_u256( - value.present_value, + StoredValue::Evm(evm::EvmValue::Storage(evm::StorageValue::new( + tx::from_revm_storage_word(value.present_value), ))), ); } @@ -141,9 +153,9 @@ where .read(account_key) .map_err(|error| Error::State(error.to_string()))? { - Some(StoredValue::EvmAccount(account)) => Ok(Some(account.main_purse())), + Some(StoredValue::Evm(evm::EvmValue::Account(account))) => Ok(Some(account.main_purse())), Some(stored_value) => Err(Error::State(format!( - "unexpected stored value for {account_key}: expected StoredValue::EvmAccount, found {}", + "unexpected stored value for {account_key}: expected StoredValue::Evm(Account), found {}", stored_value.type_name() ))), None => Ok(None), diff --git a/executor/evm/src/tx.rs b/executor/evm/src/tx.rs index 557a1ab198..fac5e2a424 100644 --- a/executor/evm/src/tx.rs +++ b/executor/evm/src/tx.rs @@ -81,6 +81,10 @@ pub(crate) fn from_revm_hash(hash: B256) -> evm::Hash { evm::Hash::new(hash.0) } +pub(crate) fn from_revm_topic(topic: B256) -> evm::Topic { + evm::Topic::new(topic.0) +} + pub(crate) fn to_revm_block_hash(block_hash: BlockHash) -> B256 { let mut bytes = [0u8; evm::HASH_LENGTH]; bytes.copy_from_slice(block_hash.as_ref()); @@ -93,10 +97,11 @@ pub(crate) fn to_revm_u256(value: CasperU256) -> U256 { U256::from_be_slice(&bytes) } -pub(crate) fn to_revm_hash_word(value: evm::Hash) -> U256 { - U256::from_be_slice(value.as_bytes()) +pub(crate) fn to_revm_storage_word(value: CasperU256) -> U256 { + to_revm_u256(value) } -pub(crate) fn from_revm_u256(value: U256) -> evm::Hash { - evm::Hash::new(value.to_be_bytes()) +pub(crate) fn from_revm_storage_word(value: U256) -> CasperU256 { + let bytes = value.to_be_bytes::<32>(); + CasperU256::from_big_endian(&bytes) } diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index e25cd0c486..1714c1f600 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -19,7 +19,7 @@ use casper_storage::{ use casper_types::{ evm, BlockHash, CLValue, ChainspecRegistry, Digest, GenesisAccount, GenesisConfig, HoldBalanceHandling, Key, Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, - StoredValue, SystemConfig, Timestamp, U256 as CasperU256, WasmConfig, U512, + StoredValue, SystemConfig, Timestamp, WasmConfig, U256 as CasperU256, U512, }; use revm::bytecode::opcode; @@ -245,24 +245,30 @@ fn selector(signature: &str) -> Vec { revm::primitives::keccak256(signature.as_bytes())[..4].to_vec() } -fn calldata(signature: &str, args: &[evm::Hash]) -> Vec { +type AbiWord = [u8; evm::HASH_LENGTH]; + +fn calldata(signature: &str, args: &[AbiWord]) -> Vec { let mut bytes = selector(signature); for arg in args { - bytes.extend_from_slice(arg.as_bytes()); + bytes.extend_from_slice(arg); } bytes } -fn word(value: u64) -> evm::Hash { +fn word(value: u64) -> AbiWord { let mut bytes = [0u8; 32]; bytes[24..].copy_from_slice(&value.to_be_bytes()); - evm::Hash::new(bytes) + bytes } -fn address_word(address: evm::Address) -> evm::Hash { +fn storage_word(value: u64) -> CasperU256 { + CasperU256::from(value) +} + +fn address_word(address: evm::Address) -> AbiWord { let mut bytes = [0u8; 32]; bytes[12..].copy_from_slice(address.as_bytes()); - evm::Hash::new(bytes) + bytes } fn decode_word(output: &[u8]) -> u64 { @@ -312,13 +318,15 @@ fn legacy_transaction_without_chain_id() -> evm::Transaction { fn read_storage>( tracking_copy: &mut TrackingCopy, address: evm::Address, - slot: evm::Hash, -) -> Option { + slot: CasperU256, +) -> Option { match tracking_copy - .read(&Key::EvmStorage(evm::StorageAddr::new(address, slot))) + .read(&Key::Evm(evm::EvmAddr::Storage(evm::StorageAddr::new( + address, slot, + )))) .expect("storage read should not fail") { - Some(StoredValue::EvmStorage(value)) => Some(value.value()), + Some(StoredValue::Evm(evm::EvmValue::Storage(value))) => Some(value.value()), Some(other) => panic!("unexpected storage value: {other:?}"), None => None, } @@ -329,10 +337,10 @@ fn read_balance>( address: evm::Address, ) -> U512 { let purse = match tracking_copy - .read(&Key::EvmAccount(address)) + .read(&Key::Evm(evm::EvmAddr::Account(address))) .expect("account read should not fail") { - Some(StoredValue::EvmAccount(account)) => account.main_purse(), + Some(StoredValue::Evm(evm::EvmValue::Account(account))) => account.main_purse(), Some(other) => panic!("unexpected account value: {other:?}"), None => return U512::zero(), }; @@ -353,8 +361,12 @@ fn seed_evm_balance>( ) { let main_purse = evm::deterministic_purse(address); tracking_copy.write( - Key::EvmAccount(address), - StoredValue::EvmAccount(evm::Account::new(0, EMPTY_CODE_HASH, main_purse)), + Key::Evm(evm::EvmAddr::Account(address)), + StoredValue::Evm(evm::EvmValue::Account(evm::Account::new( + 0, + EMPTY_CODE_HASH, + main_purse, + ))), ); tracking_copy.write( Key::Balance(main_purse.addr()), @@ -386,7 +398,7 @@ fn blockhash_uses_supplied_provider() { ) .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); - assert_eq!(outcome.output.as_slice(), evm::Hash::ZERO.as_bytes()); + assert_eq!(outcome.output.as_slice(), &[0u8; evm::HASH_LENGTH]); let mut too_old_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); too_old_request.block.number = 258; @@ -394,7 +406,7 @@ fn blockhash_uses_supplied_provider() { .execute_with_block_hash_provider(&mut tracking_copy, too_old_request, &block_hash_provider) .expect("EVM execution should succeed"); assert_eq!(outcome.status, ExecutionStatus::Success); - assert_eq!(outcome.output.as_slice(), evm::Hash::ZERO.as_bytes()); + assert_eq!(outcome.output.as_slice(), &[0u8; evm::HASH_LENGTH]); let mut historical_request = call_request(from, Some(contract), Vec::new(), CasperU256::zero()); historical_request.block.number = 2; @@ -608,8 +620,8 @@ fn storage_zeroes_are_pruned() { calldata("set(uint256)", &[word(123)]), ); assert_eq!( - read_storage(&mut tracking_copy, contract, evm::Hash::ZERO), - Some(word(123)) + read_storage(&mut tracking_copy, contract, CasperU256::zero()), + Some(storage_word(123)) ); execute_call( @@ -620,7 +632,7 @@ fn storage_zeroes_are_pruned() { selector("clear()"), ); assert_eq!( - read_storage(&mut tracking_copy, contract, evm::Hash::ZERO), + read_storage(&mut tracking_copy, contract, CasperU256::zero()), None ); } @@ -642,9 +654,9 @@ fn selfdestruct_cleanup_follows_selected_fork() { read_storage( &mut shanghai_tracking_copy, shanghai_contract, - evm::Hash::ZERO + CasperU256::zero() ), - Some(word(7)) + Some(storage_word(7)) ); execute_call( &shanghai_executor, @@ -655,7 +667,7 @@ fn selfdestruct_cleanup_follows_selected_fork() { ); assert_eq!( shanghai_tracking_copy - .read(&Key::EvmAccount(shanghai_contract)) + .read(&Key::Evm(evm::EvmAddr::Account(shanghai_contract))) .unwrap(), None ); @@ -671,7 +683,7 @@ fn selfdestruct_cleanup_follows_selected_fork() { read_storage( &mut shanghai_tracking_copy, shanghai_contract, - evm::Hash::ZERO + CasperU256::zero() ), None ); @@ -692,7 +704,7 @@ fn selfdestruct_cleanup_follows_selected_fork() { calldata("destroy(address)", &[address_word(beneficiary)]), ); assert!(prague_tracking_copy - .read(&Key::EvmAccount(prague_contract)) + .read(&Key::Evm(evm::EvmAddr::Account(prague_contract))) .unwrap() .is_some()); } diff --git a/node/src/components/transaction_acceptor/tests.rs b/node/src/components/transaction_acceptor/tests.rs index 35aac95691..b7b8acbffa 100644 --- a/node/src/components/transaction_acceptor/tests.rs +++ b/node/src/components/transaction_acceptor/tests.rs @@ -34,6 +34,7 @@ use casper_types::{ contracts::{ ContractHash, ContractPackage, ContractPackageStatus, ContractVersionKey, NamedKeys, }, + evm, global_state::TrieMerkleProof, testing::TestRng, Block, BlockV2, CLValue, Chainspec, ChainspecRawBytes, Contract, Deploy, EraId, Groups, @@ -1125,7 +1126,9 @@ impl reactor::Reactor for Reactor { | BalanceIdentifier::PenalizedAccount(account_hash) => { Key::Account(*account_hash) } - BalanceIdentifier::Evm(address) => Key::EvmAccount(*address), + BalanceIdentifier::Evm(address) => { + Key::Evm(evm::EvmAddr::Account(*address)) + } BalanceIdentifier::Entity(entity_addr) => { Key::AddressableEntity(*entity_addr) } diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index 818e931722..4fff5ea9cb 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -825,7 +825,7 @@ pub fn exec_result_is_success(exec_result: &ExecutionResult) -> bool { const EVM_TEST_GAS_LIMIT: u64 = 500_000; const EVM_TEST_GAS_PRICE: u128 = 1; const EVM_INITIAL_BALANCE: u64 = 10_000_000_000_000; -const EVM_LOG_TOPIC: evm::Hash = evm::Hash::new([0xAB; evm::HASH_LENGTH]); +const EVM_LOG_TOPIC: evm::Topic = evm::Topic::new([0xAB; evm::HASH_LENGTH]); fn evm_log_emitting_init_code() -> Vec { const MEMORY_OFFSET: u8 = 0; @@ -933,8 +933,12 @@ fn seed_evm_account(fixture: &mut TestFixture, address: evm::Address, balance: U let main_purse = evm::deterministic_purse(address); let values_to_write = vec![ ( - Key::EvmAccount(address), - StoredValue::EvmAccount(evm::Account::new(0, EMPTY_CODE_HASH, main_purse)), + Key::Evm(evm::EvmAddr::Account(address)), + StoredValue::Evm(evm::EvmValue::Account(evm::Account::new( + 0, + EMPTY_CODE_HASH, + main_purse, + ))), ), ( Key::Balance(main_purse.addr()), @@ -1006,9 +1010,13 @@ fn evm_account_at( .expect("failure to read block header") .expect("should have header"); let state_root_hash = *block_header.state_root_hash(); - match query_global_state(fixture, state_root_hash, Key::EvmAccount(address)) { + match query_global_state( + fixture, + state_root_hash, + Key::Evm(evm::EvmAddr::Account(address)), + ) { Some(value) => match *value { - StoredValue::EvmAccount(account) => account, + StoredValue::Evm(evm::EvmValue::Account(account)) => account, value => panic!("expected EVM account, got {value:?}"), }, value => panic!("expected EVM account, got {value:?}"), @@ -1205,7 +1213,7 @@ async fn should_reject_evm_transaction_when_value_and_fee_exceed_balance() { assert!(query_global_state( &mut test.fixture, *block_header.state_root_hash(), - Key::EvmAccount(recipient) + Key::Evm(evm::EvmAddr::Account(recipient)) ) .is_none()); } @@ -1255,7 +1263,7 @@ async fn should_not_seed_evm_accounts_at_genesis() { assert!(query_global_state( &mut test.fixture, *block_header.state_root_hash(), - Key::EvmAccount(alice_evm_address), + Key::Evm(evm::EvmAddr::Account(alice_evm_address)), ) .is_none()); } diff --git a/storage/src/data_access_layer/balance.rs b/storage/src/data_access_layer/balance.rs index c131855fd2..1e5ea6b088 100644 --- a/storage/src/data_access_layer/balance.rs +++ b/storage/src/data_access_layer/balance.rs @@ -123,13 +123,13 @@ impl BalanceIdentifier { } } BalanceIdentifier::Evm(address) => { - let key = Key::EvmAccount(*address); + let key = Key::Evm(evm::EvmAddr::Account(*address)); match tc.read(&key)? { - Some(StoredValue::EvmAccount(account)) => account.main_purse(), + Some(StoredValue::Evm(evm::EvmValue::Account(account))) => account.main_purse(), Some(stored_value) => { return Err(TrackingCopyError::TypeMismatch( StoredValueTypeMismatch::new( - "StoredValue::EvmAccount".to_string(), + "StoredValue::Evm(Account)".to_string(), stored_value.type_name(), ), )); diff --git a/storage/src/data_access_layer/key_prefix.rs b/storage/src/data_access_layer/key_prefix.rs index 498b1def40..fd621493ce 100644 --- a/storage/src/data_access_layer/key_prefix.rs +++ b/storage/src/data_access_layer/key_prefix.rs @@ -2,7 +2,7 @@ use casper_types::{ account::AccountHash, bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, contract_messages::TopicNameHash, - evm::Address as EvmAddress, + evm::{Address as EvmAddress, EvmAddr}, system::{auction::BidAddrTag, mint::BalanceHoldAddrTag}, EntityAddr, KeyTag, URefAddr, }; @@ -60,7 +60,9 @@ impl ToBytes for KeyPrefix { KeyPrefix::EntryPointsV2ByEntity(entity) => { U8_SERIALIZED_LENGTH + entity.serialized_length() } - KeyPrefix::EvmStorageByAddress(address) => address.serialized_length(), + KeyPrefix::EvmStorageByAddress(address) => { + U8_SERIALIZED_LENGTH + address.serialized_length() + } } } @@ -105,7 +107,8 @@ impl ToBytes for KeyPrefix { entity.write_bytes(writer)?; } KeyPrefix::EvmStorageByAddress(address) => { - writer.push(KeyTag::EvmStorage as u8); + writer.push(KeyTag::Evm as u8); + writer.push(EvmAddr::STORAGE_TAG); address.write_bytes(writer)?; } } @@ -168,9 +171,15 @@ impl FromBytes for KeyPrefix { _ => return Err(bytesrepr::Error::Formatting), } } - tag if tag == KeyTag::EvmStorage as u8 => { - let (address, remainder) = EvmAddress::from_bytes(remainder)?; - (KeyPrefix::EvmStorageByAddress(address), remainder) + tag if tag == KeyTag::Evm as u8 => { + let (evm_addr_tag, remainder) = u8::from_bytes(remainder)?; + match evm_addr_tag { + tag if tag == EvmAddr::STORAGE_TAG => { + let (address, remainder) = EvmAddress::from_bytes(remainder)?; + (KeyPrefix::EvmStorageByAddress(address), remainder) + } + _ => return Err(bytesrepr::Error::Formatting), + } } _ => return Err(bytesrepr::Error::Formatting), }; @@ -188,7 +197,7 @@ mod tests { contract_messages::MessageAddr, gens::{account_hash_arb, entity_addr_arb, topic_name_hash_arb, u8_slice_32}, system::{auction::BidAddr, mint::BalanceHoldAddr}, - BlockTime, EntryPointAddr, Key, + BlockTime, EntryPointAddr, Key, U256, }; use super::*; @@ -228,6 +237,8 @@ mod tests { let hash1 = rng.gen(); let hash2 = rng.gen(); + let evm_address = EvmAddress::new(rng.gen()); + let evm_slot = U256::from(1); for (key, prefix) in [ ( @@ -267,6 +278,13 @@ mod tests { ), KeyPrefix::EntryPointsV1ByEntity(EntityAddr::Account(hash1)), ), + ( + Key::Evm(EvmAddr::Storage(casper_types::evm::StorageAddr::new( + evm_address, + evm_slot, + ))), + KeyPrefix::EvmStorageByAddress(evm_address), + ), ] { let key_bytes = key.to_bytes().expect("should serialize key"); let (parsed_key_prefix, remainder) = diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index 698aa7e840..41058ae317 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -2248,8 +2248,12 @@ pub trait StateProvider: Send + Sync + Sized { Err(error) => return TransferResult::Failure(TransferError::CLValue(error)), }; tc.borrow_mut().write( - Key::EvmAccount(address), - StoredValue::EvmAccount(evm::Account::new(0, evm::EMPTY_CODE_HASH, main_purse)), + Key::Evm(evm::EvmAddr::Account(address)), + StoredValue::Evm(evm::EvmValue::Account(evm::Account::new( + 0, + evm::EMPTY_CODE_HASH, + main_purse, + ))), ); tc.borrow_mut().write( Key::Balance(main_purse.addr()), diff --git a/storage/src/system/transfer.rs b/storage/src/system/transfer.rs index 1dcc6d5a1d..419adbe86e 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -356,16 +356,16 @@ impl TransferRuntimeArgsBuilder { if *cl_value.cl_type() == CLType::ByteArray(evm::ADDRESS_LENGTH as u32) => { let address: evm::Address = self.map_cl_value(cl_value)?; - let key = Key::EvmAccount(address); + let key = Key::Evm(evm::EvmAddr::Account(address)); return match tracking_copy.borrow_mut().read(&key)? { - Some(StoredValue::EvmAccount(account)) => { + Some(StoredValue::Evm(evm::EvmValue::Account(account))) => { Ok(TransferTargetMode::ExistingEvmAccount { main_purse: account.main_purse().with_access_rights(AccessRights::ADD), }) } Some(stored_value) => { Err(TransferError::TypeMismatch(StoredValueTypeMismatch::new( - "StoredValue::EvmAccount".to_string(), + "StoredValue::Evm(Account)".to_string(), stored_value.type_name(), ))) } diff --git a/storage/src/tracking_copy/byte_size.rs b/storage/src/tracking_copy/byte_size.rs index c1e99e2db3..7866aa6b03 100644 --- a/storage/src/tracking_copy/byte_size.rs +++ b/storage/src/tracking_copy/byte_size.rs @@ -48,9 +48,7 @@ impl ByteSize for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.serialized_length(), StoredValue::EntryPoint(entry_point) => entry_point.serialized_length(), StoredValue::RawBytes(raw_bytes) => raw_bytes.serialized_length(), - StoredValue::EvmAccount(account) => account.serialized_length(), - StoredValue::EvmByteCode(byte_code) => byte_code.serialized_length(), - StoredValue::EvmStorage(value) => value.serialized_length(), + StoredValue::Evm(value) => value.serialized_length(), } } } diff --git a/storage/src/tracking_copy/mod.rs b/storage/src/tracking_copy/mod.rs index 850bb81027..fe8afa674d 100644 --- a/storage/src/tracking_copy/mod.rs +++ b/storage/src/tracking_copy/mod.rs @@ -891,14 +891,10 @@ where StoredValue::RawBytes(_) => { return Ok(query.into_not_found_result("RawBytes value found.")); } - StoredValue::EvmAccount(_) => { - return Ok(query.into_not_found_result("EvmAccount value found.")); - } - StoredValue::EvmByteCode(_) => { - return Ok(query.into_not_found_result("EvmByteCode value found.")); - } - StoredValue::EvmStorage(_) => { - return Ok(query.into_not_found_result("EvmStorage value found.")); + StoredValue::Evm(value) => { + return Ok( + query.into_not_found_result(&format!("{} value found.", value.type_name())) + ); } } } diff --git a/types/src/byte_code.rs b/types/src/byte_code.rs index a34b4bafcc..d3f197e026 100644 --- a/types/src/byte_code.rs +++ b/types/src/byte_code.rs @@ -135,6 +135,7 @@ impl ByteCodeAddr { ByteCodeKind::V1CasperWasm => Ok(ByteCodeAddr::V1CasperWasm(byte_code_addr)), ByteCodeKind::V2CasperWasm => Ok(ByteCodeAddr::V2CasperWasm(byte_code_addr)), ByteCodeKind::Empty => Ok(ByteCodeAddr::Empty), + ByteCodeKind::EvmPrague => Err(FromStrError::InvalidPrefix), }; } @@ -187,6 +188,7 @@ impl FromBytes for ByteCodeAddr { let (addr, remainder) = HashAddr::from_bytes(remainder)?; Ok((ByteCodeAddr::V2CasperWasm(addr), remainder)) } + ByteCodeKind::EvmPrague => Err(Error::Formatting), } } } @@ -420,6 +422,20 @@ pub enum ByteCodeKind { V1CasperWasm = 1, /// Byte code to be executed with the version 2 Casper execution engine. V2CasperWasm = 2, + /// Prague-compatible EVM bytecode. + /// + /// This variant records bytecode that is valid for the Prague EVM rules. + /// When support for a future bytecode-affecting EVM spec such as Osaka is + /// officially added, introduce a new `Evm` variant instead of + /// changing the meaning of this one. + EvmPrague = 3, +} + +impl ByteCodeKind { + /// Returns whether this bytecode kind is executable by the EVM executor. + pub fn is_evm(self) -> bool { + matches!(self, ByteCodeKind::EvmPrague) + } } impl ToBytes for ByteCodeKind { @@ -449,6 +465,9 @@ impl FromBytes for ByteCodeKind { byte_code_kind if byte_code_kind == ByteCodeKind::V2CasperWasm as u8 => { Ok((ByteCodeKind::V2CasperWasm, remainder)) } + byte_code_kind if byte_code_kind == ByteCodeKind::EvmPrague as u8 => { + Ok((ByteCodeKind::EvmPrague, remainder)) + } _ => Err(Error::Formatting), } } @@ -466,6 +485,9 @@ impl Display for ByteCodeKind { ByteCodeKind::V2CasperWasm => { write!(f, "v2-casper-wasm") } + ByteCodeKind::EvmPrague => { + write!(f, "evm-prague") + } } } } @@ -473,10 +495,11 @@ impl Display for ByteCodeKind { #[cfg(any(feature = "testing", test))] impl Distribution for Standard { fn sample(&self, rng: &mut R) -> ByteCodeKind { - match rng.gen_range(0..=2) { + match rng.gen_range(0..=3) { 0 => ByteCodeKind::Empty, 1 => ByteCodeKind::V1CasperWasm, 2 => ByteCodeKind::V2CasperWasm, + 3 => ByteCodeKind::EvmPrague, _ => unreachable!(), } } diff --git a/types/src/evm.rs b/types/src/evm.rs index 699f2179c0..a314f581c9 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -9,18 +9,22 @@ mod account; mod address; mod config; mod eth_u256; +mod evm_addr; +mod evm_value; mod hash; mod receipt; +mod topic; mod transaction; -pub use account::{ - deterministic_purse, Account, ByteCode, StorageAddr, StorageValue, EMPTY_CODE_HASH, -}; +pub use account::{deterministic_purse, Account, StorageAddr, StorageValue, EMPTY_CODE_HASH}; pub use address::{Address, ADDRESS_LENGTH}; pub use config::{EvmConfig, EvmSpec}; pub use eth_u256::EthU256; +pub use evm_addr::EvmAddr; +pub use evm_value::EvmValue; pub use hash::{Hash, HASH_LENGTH}; pub use receipt::{HaltReason, Log, OutOfGasError, Receipt, ReceiptStatus}; +pub use topic::Topic; pub use transaction::{ Transaction, TransactionError, TransactionHash, TransactionKind, EIP1559_TRANSACTION_TYPE_ID, EIP2930_TRANSACTION_TYPE_ID, EIP4844_TRANSACTION_TYPE_ID, EIP7702_TRANSACTION_TYPE_ID, diff --git a/types/src/evm/account.rs b/types/src/evm/account.rs index d103782eeb..c45ac3caa4 100644 --- a/types/src/evm/account.rs +++ b/types/src/evm/account.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "json-schema")] +use alloc::string::String; use alloc::vec::Vec; #[cfg(feature = "datasize")] @@ -8,8 +10,8 @@ use serde::{Deserialize, Serialize}; use super::{Address, Hash, ADDRESS_LENGTH}; use crate::{ - bytesrepr::{self, Bytes, FromBytes, ToBytes}, - Digest, URef, + bytesrepr::{self, FromBytes, ToBytes}, + Digest, URef, U256, }; /// Keccak-256 hash of empty EVM bytecode. @@ -84,83 +86,22 @@ impl FromBytes for Account { } } -/// EVM contract bytecode stored in global state. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] -pub struct ByteCode(Vec); - -impl ByteCode { - /// Creates EVM bytecode from raw bytes. - pub fn new(bytes: Vec) -> Self { - ByteCode(bytes) - } - - /// Returns the bytecode bytes. - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - /// Consumes the wrapper and returns the bytecode bytes. - pub fn into_bytes(self) -> Vec { - self.0 - } -} - -impl AsRef<[u8]> for ByteCode { - fn as_ref(&self) -> &[u8] { - self.as_bytes() - } -} - -impl From> for ByteCode { - fn from(bytes: Vec) -> Self { - ByteCode::new(bytes) - } -} - -impl From for ByteCode { - fn from(bytes: Bytes) -> Self { - ByteCode::new(bytes.into()) - } -} - -impl ToBytes for ByteCode { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - self.0.to_bytes() - } - - fn serialized_length(&self) -> usize { - self.0.serialized_length() - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - self.0.write_bytes(writer) - } -} - -impl FromBytes for ByteCode { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - Bytes::from_bytes(bytes).map(|(bytes, remainder)| (ByteCode::from(bytes), remainder)) - } -} - /// EVM storage value stored in global state. #[derive( Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, )] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -pub struct StorageValue(Hash); +pub struct StorageValue(#[cfg_attr(feature = "json-schema", schemars(with = "String"))] U256); impl StorageValue { - /// Creates an EVM storage value from a 32-byte word. - pub const fn new(value: Hash) -> Self { + /// Creates an EVM storage value from a 256-bit word. + pub const fn new(value: U256) -> Self { StorageValue(value) } - /// Returns the 32-byte word stored in this slot. - pub const fn value(self) -> Hash { + /// Returns the 256-bit word stored in this slot. + pub const fn value(self) -> U256 { self.0 } @@ -170,13 +111,13 @@ impl StorageValue { } } -impl From for StorageValue { - fn from(value: Hash) -> Self { +impl From for StorageValue { + fn from(value: U256) -> Self { StorageValue::new(value) } } -impl From for Hash { +impl From for U256 { fn from(value: StorageValue) -> Self { value.value() } @@ -198,7 +139,7 @@ impl ToBytes for StorageValue { impl FromBytes for StorageValue { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - Hash::from_bytes(bytes).map(|(hash, remainder)| (StorageValue::new(hash), remainder)) + U256::from_bytes(bytes).map(|(value, remainder)| (StorageValue::new(value), remainder)) } } @@ -211,12 +152,13 @@ impl FromBytes for StorageValue { #[serde(deny_unknown_fields)] pub struct StorageAddr { address: Address, - slot: Hash, + #[cfg_attr(feature = "json-schema", schemars(with = "String"))] + slot: U256, } impl StorageAddr { /// Creates an EVM storage address from a contract address and storage slot. - pub const fn new(address: Address, slot: Hash) -> Self { + pub const fn new(address: Address, slot: U256) -> Self { StorageAddr { address, slot } } @@ -226,7 +168,7 @@ impl StorageAddr { } /// Returns the EVM storage slot key. - pub const fn slot(self) -> Hash { + pub const fn slot(self) -> U256 { self.slot } } @@ -251,7 +193,7 @@ impl ToBytes for StorageAddr { impl FromBytes for StorageAddr { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { let (address, remainder) = Address::from_bytes(bytes)?; - let (slot, remainder) = Hash::from_bytes(remainder)?; + let (slot, remainder) = U256::from_bytes(remainder)?; Ok((StorageAddr::new(address, slot), remainder)) } } diff --git a/types/src/evm/evm_addr.rs b/types/src/evm/evm_addr.rs new file mode 100644 index 0000000000..460baf3f94 --- /dev/null +++ b/types/src/evm/evm_addr.rs @@ -0,0 +1,112 @@ +use alloc::vec::Vec; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::{Address, Hash, StorageAddr}; +use crate::bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}; + +/// EVM global-state address. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum EvmAddr { + /// EVM account metadata address. + Account(Address), + /// EVM contract bytecode address, keyed by code hash. + ByteCode(Hash), + /// EVM contract storage slot address. + Storage(StorageAddr), +} + +impl EvmAddr { + /// Inner tag for EVM account addresses. + pub const ACCOUNT_TAG: u8 = 0; + /// Inner tag for EVM bytecode addresses. + pub const BYTE_CODE_TAG: u8 = 1; + /// Inner tag for EVM storage addresses. + pub const STORAGE_TAG: u8 = 2; +} + +impl ToBytes for EvmAddr { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut bytes = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut bytes)?; + Ok(bytes) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + + match self { + EvmAddr::Account(address) => address.serialized_length(), + EvmAddr::ByteCode(hash) => hash.serialized_length(), + EvmAddr::Storage(addr) => addr.serialized_length(), + } + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + match self { + EvmAddr::Account(address) => { + writer.push(Self::ACCOUNT_TAG); + address.write_bytes(writer) + } + EvmAddr::ByteCode(hash) => { + writer.push(Self::BYTE_CODE_TAG); + hash.write_bytes(writer) + } + EvmAddr::Storage(addr) => { + writer.push(Self::STORAGE_TAG); + addr.write_bytes(writer) + } + } + } +} + +impl FromBytes for EvmAddr { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + match tag { + Self::ACCOUNT_TAG => Address::from_bytes(remainder) + .map(|(address, remainder)| (EvmAddr::Account(address), remainder)), + Self::BYTE_CODE_TAG => Hash::from_bytes(remainder) + .map(|(hash, remainder)| (EvmAddr::ByteCode(hash), remainder)), + Self::STORAGE_TAG => StorageAddr::from_bytes(remainder) + .map(|(addr, remainder)| (EvmAddr::Storage(addr), remainder)), + _ => Err(bytesrepr::Error::Formatting), + } + } +} + +#[cfg(any(feature = "testing", test))] +impl rand::distributions::Distribution for rand::distributions::Standard { + fn sample(&self, rng: &mut R) -> EvmAddr { + match rng.gen_range(0..=2) { + 0 => EvmAddr::Account(Address::new(rng.gen())), + 1 => EvmAddr::ByteCode(Hash::new(rng.gen())), + 2 => EvmAddr::Storage(StorageAddr::new(Address::new(rng.gen()), rng.gen())), + _ => unreachable!(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{bytesrepr, U256}; + + #[test] + fn bytesrepr_roundtrip() { + let address = Address::new([1; 20]); + let hash = Hash::new([2; 32]); + + bytesrepr::test_serialization_roundtrip(&EvmAddr::Account(address)); + bytesrepr::test_serialization_roundtrip(&EvmAddr::ByteCode(hash)); + bytesrepr::test_serialization_roundtrip(&EvmAddr::Storage(StorageAddr::new( + address, + U256::MAX, + ))); + } +} diff --git a/types/src/evm/evm_value.rs b/types/src/evm/evm_value.rs new file mode 100644 index 0000000000..b52f355daa --- /dev/null +++ b/types/src/evm/evm_value.rs @@ -0,0 +1,109 @@ +use alloc::vec::Vec; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::{Account, StorageValue}; +use crate::{ + bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, + ByteCode, +}; + +/// EVM value stored in global state. +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum EvmValue { + /// EVM account metadata. + Account(Account), + /// EVM contract bytecode. + ByteCode(ByteCode), + /// EVM contract storage value for one slot. + Storage(StorageValue), +} + +impl EvmValue { + const ACCOUNT_TAG: u8 = 0; + const BYTE_CODE_TAG: u8 = 1; + const STORAGE_TAG: u8 = 2; + + /// Returns a short type name for diagnostics. + pub fn type_name(&self) -> &'static str { + match self { + EvmValue::Account(_) => "EvmAccount", + EvmValue::ByteCode(_) => "EvmByteCode", + EvmValue::Storage(_) => "EvmStorage", + } + } +} + +impl ToBytes for EvmValue { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut bytes = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut bytes)?; + Ok(bytes) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + + match self { + EvmValue::Account(account) => account.serialized_length(), + EvmValue::ByteCode(byte_code) => byte_code.serialized_length(), + EvmValue::Storage(value) => value.serialized_length(), + } + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + match self { + EvmValue::Account(account) => { + writer.push(Self::ACCOUNT_TAG); + account.write_bytes(writer) + } + EvmValue::ByteCode(byte_code) => { + writer.push(Self::BYTE_CODE_TAG); + byte_code.write_bytes(writer) + } + EvmValue::Storage(value) => { + writer.push(Self::STORAGE_TAG); + value.write_bytes(writer) + } + } + } +} + +impl FromBytes for EvmValue { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + match tag { + Self::ACCOUNT_TAG => Account::from_bytes(remainder) + .map(|(account, remainder)| (EvmValue::Account(account), remainder)), + Self::BYTE_CODE_TAG => ByteCode::from_bytes(remainder) + .map(|(byte_code, remainder)| (EvmValue::ByteCode(byte_code), remainder)), + Self::STORAGE_TAG => StorageValue::from_bytes(remainder) + .map(|(value, remainder)| (EvmValue::Storage(value), remainder)), + _ => Err(bytesrepr::Error::Formatting), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{bytesrepr, evm, testing::TestRng, ByteCodeKind, U256}; + use rand::Rng; + + #[test] + fn bytesrepr_roundtrip() { + let rng = &mut TestRng::new(); + let account = Account::new(1, evm::EMPTY_CODE_HASH, rng.gen()); + let byte_code = ByteCode::new(ByteCodeKind::EvmPrague, vec![0x60, 0x00]); + let storage = StorageValue::new(U256::MAX); + + bytesrepr::test_serialization_roundtrip(&EvmValue::Account(account)); + bytesrepr::test_serialization_roundtrip(&EvmValue::ByteCode(byte_code)); + bytesrepr::test_serialization_roundtrip(&EvmValue::Storage(storage)); + } +} diff --git a/types/src/evm/hash.rs b/types/src/evm/hash.rs index de19895cbf..0b2a93a9bc 100644 --- a/types/src/evm/hash.rs +++ b/types/src/evm/hash.rs @@ -15,10 +15,10 @@ use crate::{ Digest, }; -/// The number of bytes in an EVM 256-bit hash or word. +/// The number of bytes in an EVM 256-bit hash. pub const HASH_LENGTH: usize = 32; -/// A 32-byte EVM hash or storage word. +/// A 32-byte EVM hash. #[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] pub struct Hash(Digest); @@ -75,7 +75,7 @@ impl JsonSchema for Hash { let schema = gen.subschema_for::(); let mut schema_object = schema.into_object(); schema_object.metadata().description = - Some("A 32-byte EVM hash or storage word encoded as 0x-prefixed hexadecimal.".into()); + Some("A 32-byte EVM hash encoded as 0x-prefixed hexadecimal.".into()); schema_object.into() } } diff --git a/types/src/evm/receipt.rs b/types/src/evm/receipt.rs index ffad0e7684..634ba21834 100644 --- a/types/src/evm/receipt.rs +++ b/types/src/evm/receipt.rs @@ -10,7 +10,7 @@ use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use super::{Address, Hash}; +use super::{Address, Topic}; use crate::bytesrepr::{self, Bytes, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; @@ -397,7 +397,7 @@ pub struct Log { /// Indexed event arguments are ABI-encoded into the following topics. /// Anonymous Solidity events omit the signature topic, allowing all four /// topics to hold indexed arguments. - pub topics: Vec, + pub topics: Vec, /// ABI-encoded unindexed log data. /// /// This contains the event arguments that are not marked `indexed`, @@ -448,7 +448,7 @@ impl ToBytes for Log { impl FromBytes for Log { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { let (address, remainder) = Address::from_bytes(bytes)?; - let (topics, remainder) = Vec::::from_bytes(remainder)?; + let (topics, remainder) = Vec::::from_bytes(remainder)?; let (data, remainder) = Bytes::from_bytes(remainder)?; Ok(( Log { @@ -494,7 +494,7 @@ impl Receipt { .map(|_| Log { address: Address::new(rng.gen()), topics: (0..rng.gen_range(0..4)) - .map(|_| Hash::new(rng.gen())) + .map(|_| Topic::new(rng.gen())) .collect(), data: Bytes::from({ let mut data = vec![0; rng.gen_range(0..16)]; diff --git a/types/src/evm/topic.rs b/types/src/evm/topic.rs new file mode 100644 index 0000000000..ed431b5e43 --- /dev/null +++ b/types/src/evm/topic.rs @@ -0,0 +1,152 @@ +use alloc::{string::String, vec::Vec}; +use core::{ + convert::TryFrom, + fmt::{self, Display, Formatter}, +}; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; + +use super::HASH_LENGTH; +use crate::{ + bytesrepr::{self, FromBytes, ToBytes}, + Digest, +}; + +/// A 32-byte EVM log topic. +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +pub struct Topic(Digest); + +impl Topic { + /// The zero topic. + pub const ZERO: Topic = Topic(Digest::from_raw([0; HASH_LENGTH])); + + /// Creates a log topic from raw bytes. + pub const fn new(bytes: [u8; HASH_LENGTH]) -> Self { + Topic(Digest::from_raw(bytes)) + } + + /// Returns the raw bytes backing this topic. + pub fn value(self) -> [u8; HASH_LENGTH] { + self.0.value() + } + + /// Returns the raw bytes backing this topic by reference. + pub fn as_bytes(&self) -> &[u8; HASH_LENGTH] { + <&[u8; HASH_LENGTH]>::try_from(self.0.as_ref()).expect("digest length is 32 bytes") + } + + /// Returns `true` when all bytes are zero. + pub fn is_zero(&self) -> bool { + self.0.as_ref().iter().all(|byte| *byte == 0) + } + + /// Returns a lower-case hexadecimal string without a `0x` prefix. + pub fn to_hex_string(self) -> String { + base16::encode_lower(&self.0) + } +} + +impl AsRef<[u8]> for Topic { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Display for Topic { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "0x{}", self.to_hex_string()) + } +} + +#[cfg(feature = "json-schema")] +impl JsonSchema for Topic { + fn schema_name() -> String { + String::from("EvmTopic") + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + let schema = gen.subschema_for::(); + let mut schema_object = schema.into_object(); + schema_object.metadata().description = + Some("A 32-byte EVM log topic encoded as 0x-prefixed hexadecimal.".into()); + schema_object.into() + } +} + +impl Serialize for Topic { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + serializer.collect_str(self) + } else { + self.0.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for Topic { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let value = String::deserialize(deserializer)?; + let hex = value + .strip_prefix("0x") + .ok_or_else(|| D::Error::custom("topic must start with 0x"))?; + let bytes = base16::decode(hex.as_bytes()).map_err(SerdeError::custom)?; + let bytes = + <[u8; HASH_LENGTH]>::try_from(bytes.as_ref()).map_err(SerdeError::custom)?; + Ok(Topic::new(bytes)) + } else { + Digest::deserialize(deserializer).map(Topic) + } + } +} + +impl ToBytes for Topic { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + self.0.to_bytes() + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.0.write_bytes(writer) + } +} + +impl FromBytes for Topic { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + Digest::from_bytes(bytes).map(|(digest, remainder)| (Topic(digest), remainder)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn human_readable_serde_uses_0x_prefixed_hex() { + let topic = Topic::new([0xab; HASH_LENGTH]); + let expected_hex = "ab".repeat(HASH_LENGTH); + + let encoded = serde_json::to_string(&topic).expect("topic should serialize"); + assert_eq!(encoded, format!("\"0x{expected_hex}\"")); + + let decoded: Topic = serde_json::from_str(&encoded).expect("topic should deserialize"); + assert_eq!(decoded, topic); + } + + #[test] + fn non_human_readable_serde_roundtrip() { + let topic = Topic::new([0xcd; HASH_LENGTH]); + let encoded = bincode::serialize(&topic).expect("topic should serialize"); + let decoded: Topic = bincode::deserialize(&encoded).expect("topic should deserialize"); + + assert_eq!(decoded, topic); + } +} diff --git a/types/src/execution/transform_kind.rs b/types/src/execution/transform_kind.rs index 7fdedf3cbe..995c88ccb6 100644 --- a/types/src/execution/transform_kind.rs +++ b/types/src/execution/transform_kind.rs @@ -208,19 +208,9 @@ impl TransformKindV2 { let found = "EntryPoint".to_string(); Err(StoredValueTypeMismatch::new(expected, found).into()) } - StoredValue::EvmAccount(_) => { + StoredValue::Evm(value) => { let expected = "Contract or Account".to_string(); - let found = "EvmAccount".to_string(); - Err(StoredValueTypeMismatch::new(expected, found).into()) - } - StoredValue::EvmByteCode(_) => { - let expected = "Contract or Account".to_string(); - let found = "EvmByteCode".to_string(); - Err(StoredValueTypeMismatch::new(expected, found).into()) - } - StoredValue::EvmStorage(_) => { - let expected = "Contract or Account".to_string(); - let found = "EvmStorage".to_string(); + let found = value.type_name().to_string(); Err(StoredValueTypeMismatch::new(expected, found).into()) } }, diff --git a/types/src/gens.rs b/types/src/gens.rs index 2468c7a3ce..4902a13164 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -198,6 +198,7 @@ pub fn all_keys_arb() -> impl Strategy { balance_hold_addr_arb().prop_map(Key::BalanceHold), entry_point_addr_arb().prop_map(Key::EntryPoint), entity_addr_arb().prop_map(Key::State), + evm_addr_arb().prop_map(Key::Evm), ] } @@ -318,6 +319,17 @@ pub fn u256_arb() -> impl Strategy { collection::vec(any::(), 0..32).prop_map(|b| U256::from_little_endian(b.as_slice())) } +pub fn evm_addr_arb() -> impl Strategy { + prop_oneof![ + prop::array::uniform20(any::()) + .prop_map(|bytes| evm::EvmAddr::Account(evm::Address::new(bytes))), + u8_slice_32().prop_map(|bytes| evm::EvmAddr::ByteCode(evm::Hash::new(bytes))), + (prop::array::uniform20(any::()), u256_arb()).prop_map(|(address, slot)| { + evm::EvmAddr::Storage(evm::StorageAddr::new(evm::Address::new(address), slot)) + }), + ] +} + pub fn u512_arb() -> impl Strategy { prop_oneof![ 1 => Just(U512::zero()), @@ -987,16 +999,22 @@ pub fn stored_value_arb() -> impl Strategy { named_key_value_arb().prop_map(StoredValue::NamedKey), collection::vec(any::(), 0..1000).prop_map(StoredValue::RawBytes), (any::(), u8_slice_32(), uref_arb()).prop_map(|(nonce, code_hash, main_purse)| { - StoredValue::EvmAccount(crate::evm::Account::new( + StoredValue::Evm(crate::evm::EvmValue::Account(crate::evm::Account::new( nonce, crate::evm::Hash::new(code_hash), main_purse, - )) + ))) }), - collection::vec(any::(), 0..1000) - .prop_map(|bytes| StoredValue::EvmByteCode(crate::evm::ByteCode::new(bytes))), - u8_slice_32().prop_map(|value| { - StoredValue::EvmStorage(crate::evm::StorageValue::new(crate::evm::Hash::new(value))) + collection::vec(any::(), 0..1000).prop_map(|bytes| { + StoredValue::Evm(crate::evm::EvmValue::ByteCode(ByteCode::new( + ByteCodeKind::EvmPrague, + bytes, + ))) + }), + u256_arb().prop_map(|value| { + StoredValue::Evm(crate::evm::EvmValue::Storage( + crate::evm::StorageValue::new(value), + )) }), ] .prop_map(|stored_value| @@ -1024,9 +1042,7 @@ pub fn stored_value_arb() -> impl Strategy { StoredValue::Prepayment(_) => stored_value, StoredValue::EntryPoint(_) => stored_value, StoredValue::RawBytes(_) => stored_value, - StoredValue::EvmAccount(_) => stored_value, - StoredValue::EvmByteCode(_) => stored_value, - StoredValue::EvmStorage(_) => stored_value, + StoredValue::Evm(_) => stored_value, }) } diff --git a/types/src/key.rs b/types/src/key.rs index bbcdbb9d71..da22c4c081 100644 --- a/types/src/key.rs +++ b/types/src/key.rs @@ -49,7 +49,7 @@ use crate::{ contract_wasm::ContractWasmHash, contracts::{ContractHash, ContractPackageHash}, evm::{ - Address as EvmAddress, Hash as EvmHash, StorageAddr as EvmStorageAddr, + Address as EvmAddress, EvmAddr, Hash as EvmHash, StorageAddr as EvmStorageAddr, ADDRESS_LENGTH as EVM_ADDRESS_LENGTH, }, package::PackageHash, @@ -59,7 +59,7 @@ use crate::{ }, uref::{self, URef, URefAddr, UREF_SERIALIZED_LENGTH}, ByteCodeAddr, DeployHash, Digest, EraId, Tagged, TransferAddr, TransferFromStrError, - TRANSFER_ADDR_LENGTH, UREF_ADDR_LENGTH, + TRANSFER_ADDR_LENGTH, U256, UREF_ADDR_LENGTH, }; const HASH_PREFIX: &str = "hash-"; @@ -87,6 +87,7 @@ const REWARDS_HANDLING_PREFIX: &str = "rewards-handling-"; const EVM_ACCOUNT_PREFIX: &str = "evm-account-"; const EVM_BYTE_CODE_PREFIX: &str = "evm-byte-code-"; const EVM_STORAGE_PREFIX: &str = "evm-storage-"; +const EVM_STORAGE_FORMATTED_LENGTH: usize = EVM_ADDRESS_LENGTH + KEY_HASH_LENGTH; /// The number of bytes in a Blake2b hash pub const BLAKE2B_DIGEST_LENGTH: usize = 32; @@ -125,10 +126,6 @@ const KEY_CHECKSUM_REGISTRY_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + PADDING_BYTES.len(); const KEY_REWARDS_HANDLING_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + PADDING_BYTES.len(); -const KEY_EVM_ACCOUNT_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + EVM_ADDRESS_LENGTH; -const KEY_EVM_BYTE_CODE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + KEY_HASH_LENGTH; -const KEY_EVM_STORAGE_SERIALIZED_LENGTH: usize = - KEY_ID_SERIALIZED_LENGTH + EVM_ADDRESS_LENGTH + KEY_HASH_LENGTH; const KEY_PACKAGE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + 32; const KEY_MESSAGE_SERIALIZED_LENGTH: usize = KEY_ID_SERIALIZED_LENGTH + U8_SERIALIZED_LENGTH @@ -178,16 +175,14 @@ pub enum KeyTag { EntryPoint = 23, State = 24, RewardsHandling = 25, - EvmAccount = 26, - EvmByteCode = 27, - EvmStorage = 28, + Evm = 26, } impl KeyTag { /// Returns a random `KeyTag`. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..=28) { + match rng.gen_range(0..=26) { 0 => KeyTag::Account, 1 => KeyTag::Hash, 2 => KeyTag::URef, @@ -214,9 +209,7 @@ impl KeyTag { 23 => KeyTag::EntryPoint, 24 => KeyTag::State, 25 => KeyTag::RewardsHandling, - 26 => KeyTag::EvmAccount, - 27 => KeyTag::EvmByteCode, - 28 => KeyTag::EvmStorage, + 26 => KeyTag::Evm, _ => unreachable!(), } } @@ -251,9 +244,7 @@ impl Display for KeyTag { KeyTag::State => write!(f, "State"), KeyTag::EntryPoint => write!(f, "EntryPoint"), KeyTag::RewardsHandling => write!(f, "RewardsHandling"), - KeyTag::EvmAccount => write!(f, "EvmAccount"), - KeyTag::EvmByteCode => write!(f, "EvmByteCode"), - KeyTag::EvmStorage => write!(f, "EvmStorage"), + KeyTag::Evm => write!(f, "Evm"), } } } @@ -305,9 +296,7 @@ impl FromBytes for KeyTag { tag if tag == KeyTag::EntryPoint as u8 => KeyTag::EntryPoint, tag if tag == KeyTag::State as u8 => KeyTag::State, tag if tag == KeyTag::RewardsHandling as u8 => KeyTag::RewardsHandling, - tag if tag == KeyTag::EvmAccount as u8 => KeyTag::EvmAccount, - tag if tag == KeyTag::EvmByteCode as u8 => KeyTag::EvmByteCode, - tag if tag == KeyTag::EvmStorage as u8 => KeyTag::EvmStorage, + tag if tag == KeyTag::Evm as u8 => KeyTag::Evm, _ => return Err(Error::Formatting), }; Ok((tag, rem)) @@ -374,12 +363,8 @@ pub enum Key { State(EntityAddr), /// A `Key` under which we store rewards handling information RewardsHandling, - /// A `Key` under which EVM account metadata is stored. - EvmAccount(EvmAddress), - /// A `Key` under which EVM contract bytecode is stored by hash. - EvmByteCode(EvmHash), - /// A `Key` under which one EVM contract storage slot is stored. - EvmStorage(EvmStorageAddr), + /// A `Key` under which EVM account, bytecode, or storage data is stored. + Evm(EvmAddr), } #[cfg(feature = "json-schema")] @@ -563,6 +548,12 @@ impl Display for FromStrError { } } +fn u256_to_padded_hex(value: U256) -> String { + let mut bytes = [0u8; KEY_HASH_LENGTH]; + value.to_big_endian(&mut bytes); + base16::encode_lower(&bytes) +} + impl Key { // This method is not intended to be used by third party crates. #[doc(hidden)] @@ -594,9 +585,7 @@ impl Key { Key::EntryPoint(_) => String::from("Key::EntryPoint"), Key::State(_) => String::from("Key::State"), Key::RewardsHandling => String::from("Key::RewardsHandling"), - Key::EvmAccount(_) => String::from("Key::EvmAccount"), - Key::EvmByteCode(_) => String::from("Key::EvmByteCode"), - Key::EvmStorage(_) => String::from("Key::EvmStorage"), + Key::Evm(_) => String::from("Key::Evm"), } } @@ -732,18 +721,18 @@ impl Key { base16::encode_lower(&PADDING_BYTES) ) } - Key::EvmAccount(address) => { + Key::Evm(EvmAddr::Account(address)) => { format!("{}{}", EVM_ACCOUNT_PREFIX, address.to_hex_string()) } - Key::EvmByteCode(hash) => { + Key::Evm(EvmAddr::ByteCode(hash)) => { format!("{}{}", EVM_BYTE_CODE_PREFIX, hash.to_hex_string()) } - Key::EvmStorage(addr) => { + Key::Evm(EvmAddr::Storage(addr)) => { format!( "{}{}{}", EVM_STORAGE_PREFIX, addr.address().to_hex_string(), - addr.slot().to_hex_string() + u256_to_padded_hex(addr.slot()) ) } } @@ -1078,7 +1067,7 @@ impl Key { .map_err(|error| FromStrError::EvmAccount(error.to_string()))?; let address = <[u8; EVM_ADDRESS_LENGTH]>::try_from(bytes.as_ref()) .map_err(|error| FromStrError::EvmAccount(error.to_string()))?; - return Ok(Key::EvmAccount(EvmAddress::new(address))); + return Ok(Key::Evm(EvmAddr::Account(EvmAddress::new(address)))); } if let Some(hex) = input.strip_prefix(EVM_BYTE_CODE_PREFIX) { @@ -1086,16 +1075,16 @@ impl Key { .map_err(|error| FromStrError::EvmByteCode(error.to_string()))?; let hash = <[u8; KEY_HASH_LENGTH]>::try_from(bytes.as_ref()) .map_err(|error| FromStrError::EvmByteCode(error.to_string()))?; - return Ok(Key::EvmByteCode(EvmHash::new(hash))); + return Ok(Key::Evm(EvmAddr::ByteCode(EvmHash::new(hash)))); } if let Some(hex) = input.strip_prefix(EVM_STORAGE_PREFIX) { let bytes = checksummed_hex::decode(hex) .map_err(|error| FromStrError::EvmStorage(error.to_string()))?; - if bytes.len() != EVM_ADDRESS_LENGTH + KEY_HASH_LENGTH { + if bytes.len() != EVM_STORAGE_FORMATTED_LENGTH { return Err(FromStrError::EvmStorage(format!( "expected {} bytes, got {}", - EVM_ADDRESS_LENGTH + KEY_HASH_LENGTH, + EVM_STORAGE_FORMATTED_LENGTH, bytes.len() ))); } @@ -1103,10 +1092,10 @@ impl Key { .map_err(|error| FromStrError::EvmStorage(error.to_string()))?; let slot = <[u8; KEY_HASH_LENGTH]>::try_from(&bytes[EVM_ADDRESS_LENGTH..]) .map_err(|error| FromStrError::EvmStorage(error.to_string()))?; - return Ok(Key::EvmStorage(EvmStorageAddr::new( + return Ok(Key::Evm(EvmAddr::Storage(EvmStorageAddr::new( EvmAddress::new(address), - EvmHash::new(slot), - ))); + U256::from_big_endian(&slot), + )))); } Err(FromStrError::UnknownPrefix) @@ -1251,7 +1240,7 @@ impl Key { /// Returns the EVM address if this key stores an EVM account. pub fn as_evm_account(&self) -> Option<&EvmAddress> { - if let Self::EvmAccount(address) = self { + if let Self::Evm(EvmAddr::Account(address)) = self { Some(address) } else { None @@ -1260,7 +1249,7 @@ impl Key { /// Returns the EVM storage owner and slot if this key stores an EVM storage value. pub fn as_evm_storage(&self) -> Option<&EvmStorageAddr> { - if let Self::EvmStorage(addr) = self { + if let Self::Evm(EvmAddr::Storage(addr)) = self { Some(addr) } else { None @@ -1446,9 +1435,7 @@ impl Key { | Key::Message(_) | Key::BlockGlobal(_) | Key::EntryPoint(_) - | Key::EvmAccount(_) - | Key::EvmByteCode(_) - | Key::EvmStorage(_) => true, + | Key::Evm(_) => true, _ => false, }; if !ret { @@ -1606,10 +1593,10 @@ impl Display for Key { "Key::RewardsHandling({})", base16::encode_lower(&PADDING_BYTES), ), - Key::EvmAccount(address) => write!(f, "Key::EvmAccount({})", address), - Key::EvmByteCode(hash) => write!(f, "Key::EvmByteCode({})", hash), - Key::EvmStorage(addr) => { - write!(f, "Key::EvmStorage({}-{})", addr.address(), addr.slot()) + Key::Evm(EvmAddr::Account(address)) => write!(f, "Key::Evm(Account({}))", address), + Key::Evm(EvmAddr::ByteCode(hash)) => write!(f, "Key::Evm(ByteCode({}))", hash), + Key::Evm(EvmAddr::Storage(addr)) => { + write!(f, "Key::Evm(Storage({}-{}))", addr.address(), addr.slot()) } } } @@ -1650,9 +1637,7 @@ impl Tagged for Key { Key::EntryPoint(_) => KeyTag::EntryPoint, Key::State(_) => KeyTag::State, Key::RewardsHandling => KeyTag::RewardsHandling, - Key::EvmAccount(_) => KeyTag::EvmAccount, - Key::EvmByteCode(_) => KeyTag::EvmByteCode, - Key::EvmStorage(_) => KeyTag::EvmStorage, + Key::Evm(_) => KeyTag::Evm, } } } @@ -1771,9 +1756,7 @@ impl ToBytes for Key { } Key::State(entity_addr) => KEY_ID_SERIALIZED_LENGTH + entity_addr.serialized_length(), Key::RewardsHandling => KEY_REWARDS_HANDLING_SERIALIZED_LENGTH, - Key::EvmAccount(_) => KEY_EVM_ACCOUNT_SERIALIZED_LENGTH, - Key::EvmByteCode(_) => KEY_EVM_BYTE_CODE_SERIALIZED_LENGTH, - Key::EvmStorage(_) => KEY_EVM_STORAGE_SERIALIZED_LENGTH, + Key::Evm(addr) => KEY_ID_SERIALIZED_LENGTH + addr.serialized_length(), } } @@ -1809,9 +1792,7 @@ impl ToBytes for Key { Key::BalanceHold(balance_hold_addr) => balance_hold_addr.write_bytes(writer), Key::EntryPoint(entry_point_addr) => entry_point_addr.write_bytes(writer), Key::State(entity_addr) => entity_addr.write_bytes(writer), - Key::EvmAccount(address) => address.write_bytes(writer), - Key::EvmByteCode(hash) => hash.write_bytes(writer), - Key::EvmStorage(addr) => addr.write_bytes(writer), + Key::Evm(addr) => addr.write_bytes(writer), } } } @@ -1934,17 +1915,9 @@ impl FromBytes for Key { let (_, rem) = <[u8; 32]>::from_bytes(remainder)?; Ok((Key::RewardsHandling, rem)) } - KeyTag::EvmAccount => { - let (address, rem) = EvmAddress::from_bytes(remainder)?; - Ok((Key::EvmAccount(address), rem)) - } - KeyTag::EvmByteCode => { - let (hash, rem) = EvmHash::from_bytes(remainder)?; - Ok((Key::EvmByteCode(hash), rem)) - } - KeyTag::EvmStorage => { - let (addr, rem) = EvmStorageAddr::from_bytes(remainder)?; - Ok((Key::EvmStorage(addr), rem)) + KeyTag::Evm => { + let (addr, rem) = EvmAddr::from_bytes(remainder)?; + Ok((Key::Evm(addr), rem)) } } } @@ -1981,16 +1954,14 @@ fn please_add_to_distribution_impl(key: Key) { Key::EntryPoint(_) => unimplemented!(), Key::State(_) => unimplemented!(), Key::RewardsHandling => unimplemented!(), - Key::EvmAccount(_) => unimplemented!(), - Key::EvmByteCode(_) => unimplemented!(), - Key::EvmStorage(_) => unimplemented!(), + Key::Evm(_) => unimplemented!(), } } #[cfg(any(feature = "testing", test))] impl Distribution for Standard { fn sample(&self, rng: &mut R) -> Key { - match rng.gen_range(0..=28) { + match rng.gen_range(0..=26) { 0 => Key::Account(rng.gen()), 1 => Key::Hash(rng.gen()), 2 => Key::URef(rng.gen()), @@ -2017,12 +1988,7 @@ impl Distribution for Standard { 23 => Key::EntryPoint(rng.gen()), 24 => Key::State(rng.gen()), 25 => Key::RewardsHandling, - 26 => Key::EvmAccount(EvmAddress::new(rng.gen())), - 27 => Key::EvmByteCode(EvmHash::new(rng.gen())), - 28 => Key::EvmStorage(EvmStorageAddr::new( - EvmAddress::new(rng.gen()), - EvmHash::new(rng.gen()), - )), + 26 => Key::Evm(rng.gen()), _ => unreachable!(), } } @@ -2060,9 +2026,7 @@ mod serde_helpers { EntryPoint(&'a EntryPointAddr), State(&'a EntityAddr), RewardsHandling, - EvmAccount(&'a EvmAddress), - EvmByteCode(&'a EvmHash), - EvmStorage(&'a EvmStorageAddr), + Evm(&'a EvmAddr), } #[derive(Deserialize)] @@ -2094,9 +2058,7 @@ mod serde_helpers { EntryPoint(EntryPointAddr), State(EntityAddr), RewardsHandling, - EvmAccount(EvmAddress), - EvmByteCode(EvmHash), - EvmStorage(EvmStorageAddr), + Evm(EvmAddr), } impl<'a> From<&'a Key> for BinarySerHelper<'a> { @@ -2132,9 +2094,7 @@ mod serde_helpers { Key::EntryPoint(entry_point_addr) => BinarySerHelper::EntryPoint(entry_point_addr), Key::State(entity_addr) => BinarySerHelper::State(entity_addr), Key::RewardsHandling => BinarySerHelper::RewardsHandling, - Key::EvmAccount(address) => BinarySerHelper::EvmAccount(address), - Key::EvmByteCode(hash) => BinarySerHelper::EvmByteCode(hash), - Key::EvmStorage(addr) => BinarySerHelper::EvmStorage(addr), + Key::Evm(addr) => BinarySerHelper::Evm(addr), } } } @@ -2174,9 +2134,7 @@ mod serde_helpers { } BinaryDeserHelper::State(entity_addr) => Key::State(entity_addr), BinaryDeserHelper::RewardsHandling => Key::RewardsHandling, - BinaryDeserHelper::EvmAccount(address) => Key::EvmAccount(address), - BinaryDeserHelper::EvmByteCode(hash) => Key::EvmByteCode(hash), - BinaryDeserHelper::EvmStorage(addr) => Key::EvmStorage(addr), + BinaryDeserHelper::Evm(addr) => Key::Evm(addr), } } } diff --git a/types/src/stored_value.rs b/types/src/stored_value.rs index 1f09e26f11..8bf2a244b5 100644 --- a/types/src/stored_value.rs +++ b/types/src/stored_value.rs @@ -79,12 +79,8 @@ pub enum StoredValueTag { EntryPoint = 19, /// Raw bytes. RawBytes = 20, - /// EVM account metadata. - EvmAccount = 21, - /// EVM contract bytecode. - EvmByteCode = 22, - /// EVM contract storage value. - EvmStorage = 23, + /// EVM account, bytecode, or storage value. + Evm = 21, } /// A value stored in Global State. @@ -140,12 +136,8 @@ pub enum StoredValue { /// Raw bytes. Similar to a [`crate::StoredValue::CLValue`] but does not incur overhead of a /// [`crate::CLValue`] and [`crate::CLType`]. RawBytes(#[cfg_attr(feature = "json-schema", schemars(with = "String"))] Vec), - /// EVM account metadata. - EvmAccount(evm::Account), - /// EVM contract bytecode, addressed by [`crate::Key::EvmByteCode`]. - EvmByteCode(evm::ByteCode), - /// EVM storage value for a single contract slot. - EvmStorage(evm::StorageValue), + /// EVM account, bytecode, or storage value. + Evm(evm::EvmValue), } impl StoredValue { @@ -305,26 +297,26 @@ impl StoredValue { } } - /// Returns EVM account metadata if this is an `EvmAccount` variant. + /// Returns EVM account metadata if this is an EVM account value. pub fn as_evm_account(&self) -> Option<&evm::Account> { match self { - StoredValue::EvmAccount(account) => Some(account), + StoredValue::Evm(evm::EvmValue::Account(account)) => Some(account), _ => None, } } - /// Returns EVM bytecode if this is an `EvmByteCode` variant. - pub fn as_evm_byte_code(&self) -> Option<&evm::ByteCode> { + /// Returns EVM bytecode if this is an EVM bytecode value. + pub fn as_evm_byte_code(&self) -> Option<&ByteCode> { match self { - StoredValue::EvmByteCode(byte_code) => Some(byte_code), + StoredValue::Evm(evm::EvmValue::ByteCode(byte_code)) => Some(byte_code), _ => None, } } - /// Returns an EVM storage value if this is an `EvmStorage` variant. + /// Returns an EVM storage value if this is an EVM storage value. pub fn as_evm_storage(&self) -> Option<&evm::StorageValue> { match self { - StoredValue::EvmStorage(value) => Some(value), + StoredValue::Evm(evm::EvmValue::Storage(value)) => Some(value), _ => None, } } @@ -483,9 +475,7 @@ impl StoredValue { StoredValue::Prepayment(_) => "Prepayment".to_string(), StoredValue::EntryPoint(_) => "EntryPoint".to_string(), StoredValue::RawBytes(_) => "RawBytes".to_string(), - StoredValue::EvmAccount(_) => "EvmAccount".to_string(), - StoredValue::EvmByteCode(_) => "EvmByteCode".to_string(), - StoredValue::EvmStorage(_) => "EvmStorage".to_string(), + StoredValue::Evm(value) => value.type_name().to_string(), } } @@ -513,9 +503,7 @@ impl StoredValue { StoredValue::Prepayment(_) => StoredValueTag::Prepayment, StoredValue::EntryPoint(_) => StoredValueTag::EntryPoint, StoredValue::RawBytes(_) => StoredValueTag::RawBytes, - StoredValue::EvmAccount(_) => StoredValueTag::EvmAccount, - StoredValue::EvmByteCode(_) => StoredValueTag::EvmByteCode, - StoredValue::EvmStorage(_) => StoredValueTag::EvmStorage, + StoredValue::Evm(_) => StoredValueTag::Evm, } } @@ -824,9 +812,7 @@ impl ToBytes for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.serialized_length(), StoredValue::EntryPoint(entry_point_value) => entry_point_value.serialized_length(), StoredValue::RawBytes(bytes) => bytes.serialized_length(), - StoredValue::EvmAccount(account) => account.serialized_length(), - StoredValue::EvmByteCode(byte_code) => byte_code.serialized_length(), - StoredValue::EvmStorage(value) => value.serialized_length(), + StoredValue::Evm(value) => value.serialized_length(), } } @@ -856,9 +842,7 @@ impl ToBytes for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.write_bytes(writer), StoredValue::EntryPoint(entry_point_value) => entry_point_value.write_bytes(writer), StoredValue::RawBytes(bytes) => bytes.write_bytes(writer), - StoredValue::EvmAccount(account) => account.write_bytes(writer), - StoredValue::EvmByteCode(byte_code) => byte_code.write_bytes(writer), - StoredValue::EvmStorage(value) => value.write_bytes(writer), + StoredValue::Evm(value) => value.write_bytes(writer), } } } @@ -930,14 +914,8 @@ impl FromBytes for StoredValue { let (bytes, remainder) = Bytes::from_bytes(remainder)?; Ok((StoredValue::RawBytes(bytes.into()), remainder)) } - tag if tag == StoredValueTag::EvmAccount as u8 => evm::Account::from_bytes(remainder) - .map(|(account, remainder)| (StoredValue::EvmAccount(account), remainder)), - tag if tag == StoredValueTag::EvmByteCode as u8 => evm::ByteCode::from_bytes(remainder) - .map(|(byte_code, remainder)| (StoredValue::EvmByteCode(byte_code), remainder)), - tag if tag == StoredValueTag::EvmStorage as u8 => { - evm::StorageValue::from_bytes(remainder) - .map(|(value, remainder)| (StoredValue::EvmStorage(value), remainder)) - } + tag if tag == StoredValueTag::Evm as u8 => evm::EvmValue::from_bytes(remainder) + .map(|(value, remainder)| (StoredValue::Evm(value), remainder)), _ => Err(Error::Formatting), } } @@ -981,9 +959,7 @@ pub mod serde_helpers { Prepayment(&'a PrepaymentKind), EntryPoint(&'a EntryPointValue), RawBytes(Bytes), - EvmAccount(&'a evm::Account), - EvmByteCode(Bytes), - EvmStorage(&'a evm::StorageValue), + Evm(&'a evm::EvmValue), } /// A value stored in Global State. @@ -1040,12 +1016,8 @@ pub mod serde_helpers { /// Raw bytes. Similar to a [`crate::StoredValue::CLValue`] but does not incur overhead of /// a [`crate::CLValue`] and [`crate::CLType`]. RawBytes(Bytes), - /// EVM account metadata. - EvmAccount(evm::Account), - /// EVM contract bytecode. - EvmByteCode(Bytes), - /// EVM contract storage value. - EvmStorage(evm::StorageValue), + /// EVM account, bytecode, or storage value. + Evm(evm::EvmValue), } impl<'a> From<&'a StoredValue> for HumanReadableSerHelper<'a> { @@ -1084,11 +1056,7 @@ pub mod serde_helpers { StoredValue::RawBytes(bytes) => { HumanReadableSerHelper::RawBytes(bytes.as_slice().into()) } - StoredValue::EvmAccount(account) => HumanReadableSerHelper::EvmAccount(account), - StoredValue::EvmByteCode(byte_code) => { - HumanReadableSerHelper::EvmByteCode(byte_code.as_bytes().into()) - } - StoredValue::EvmStorage(value) => HumanReadableSerHelper::EvmStorage(value), + StoredValue::Evm(value) => HumanReadableSerHelper::Evm(value), } } } @@ -1156,11 +1124,7 @@ pub mod serde_helpers { HumanReadableDeserHelper::Prepayment(prepayment_kind) => { StoredValue::Prepayment(prepayment_kind) } - HumanReadableDeserHelper::EvmAccount(account) => StoredValue::EvmAccount(account), - HumanReadableDeserHelper::EvmByteCode(bytes) => { - StoredValue::EvmByteCode(evm::ByteCode::from(bytes)) - } - HumanReadableDeserHelper::EvmStorage(value) => StoredValue::EvmStorage(value), + HumanReadableDeserHelper::Evm(value) => StoredValue::Evm(value), }) } } @@ -1230,7 +1194,7 @@ mod tests { "access": "Public", "entry_point_type": "Factory" } - + ], "protocol_version": "2.0.0" } From 2ebc751cc78594a6e43a4e9146f3c0dcfae819fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Wed, 13 May 2026 16:57:24 +0200 Subject: [PATCH 11/17] Add EVM nonce validation and ignore generated outputs --- .gitignore | 2 + EVM.md | 18 +- binary_port/src/error_code.rs | 23 +- node/src/components/network/tasks.rs | 199 +++++++++--------- node/src/components/transaction_acceptor.rs | 120 ++++++++--- .../components/transaction_acceptor/event.rs | 15 +- .../components/transaction_acceptor/tests.rs | 95 ++++++++- storage/src/system/transfer.rs | 43 ++-- types/src/evm/transaction.rs | 13 ++ 9 files changed, 374 insertions(+), 154 deletions(-) diff --git a/.gitignore b/.gitignore index 655163f353..f99cd150ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ target_as +/cache/ +/out/ # Criterion puts results in wrong directories inside workspace: https://github.com/bheisler/criterion.rs/issues/192 target diff --git a/EVM.md b/EVM.md index ad7da1661d..dc4780f180 100644 --- a/EVM.md +++ b/EVM.md @@ -197,15 +197,21 @@ For client-submitted EVM transactions, the acceptor currently validates: 9. [EIP-1559][eip-1559] `max_priority_fee_per_gas` must be zero because Casper does not currently prioritize transactions based on transaction gas parameters. -10. `BalanceIdentifier::Evm(from)` must resolve to a balance. -11. That balance must meet the chain baseline motes requirement. +10. The EVM account for `from` must exist and resolve to a balance. +11. The transaction nonce must match the EVM account nonce in global state. +12. That balance must meet the chain baseline motes requirement. The acceptor does not require a Casper `AddressableEntity` for the EVM sender. The sender identity is `InitiatorAddr::EvmAddress(transaction.from())`, and -balance checks use `BalanceIdentifier::Evm(address)`. The acceptor only checks -that the EVM initiator has a known balance and meets the same baseline balance -requirement used for other client transactions. The runtime later checks the -full EVM maximum fee amount. +the acceptor reads the EVM account from global state before checking its backing +main purse balance. The acceptor only checks that the EVM initiator has a known +balance, uses the current account nonce, and meets the same baseline balance +requirement used for other client transactions. The runtime later checks the full +EVM maximum fee amount. + +The nonce check is also applied to peer-sourced EVM transactions before storage, +so a gossiped transaction with a nonce that cannot execute at the current state +root is rejected before it can enter the transaction buffer. ## Runtime Execution diff --git a/binary_port/src/error_code.rs b/binary_port/src/error_code.rs index 74056dfd18..74e2d31876 100644 --- a/binary_port/src/error_code.rs +++ b/binary_port/src/error_code.rs @@ -1,6 +1,6 @@ use core::{convert::TryFrom, fmt}; -use casper_types::{InvalidDeploy, InvalidTransaction, InvalidTransactionV1}; +use casper_types::{evm, InvalidDeploy, InvalidTransaction, InvalidTransactionV1}; use num_derive::FromPrimitive; use num_traits::FromPrimitive; @@ -373,6 +373,9 @@ pub enum ErrorCode { /// EVM address transfer target is disabled for this deploy. #[error("EVM address transfer target is disabled for this deploy")] DeployEvmAddressTransferDisabled = 118, + /// EVM transaction nonce does not match the account nonce. + #[error("the EVM transaction nonce does not match the account nonce")] + InvalidTransactionEvmInvalidNonce = 119, } impl TryFrom for ErrorCode { @@ -400,6 +403,9 @@ impl From for ErrorCode { match value { InvalidTransaction::Deploy(invalid_deploy) => ErrorCode::from(invalid_deploy), InvalidTransaction::V1(invalid_transaction) => ErrorCode::from(invalid_transaction), + InvalidTransaction::Evm(evm::TransactionError::InvalidNonce { .. }) => { + ErrorCode::InvalidTransactionEvmInvalidNonce + } _ => ErrorCode::InvalidTransactionOrDeployUnspecified, } } @@ -584,7 +590,7 @@ mod tests { use std::convert::TryFrom; use crate::ErrorCode; - use casper_types::{InvalidDeploy, InvalidTransactionV1}; + use casper_types::{evm, InvalidDeploy, InvalidTransaction, InvalidTransactionV1}; use strum::IntoEnumIterator; #[test] @@ -621,6 +627,19 @@ mod tests { } } + #[test] + fn evm_invalid_nonce_has_specific_error_code() { + let error = InvalidTransaction::Evm(evm::TransactionError::InvalidNonce { + expected: 0, + actual: 1, + }); + + assert_eq!( + ErrorCode::from(error), + ErrorCode::InvalidTransactionEvmInvalidNonce + ); + } + #[test] fn try_from_decoded_all_variants() { for variant in ErrorCode::iter() { diff --git a/node/src/components/network/tasks.rs b/node/src/components/network/tasks.rs index 5e08c9db54..9700ddbbb6 100644 --- a/node/src/components/network/tasks.rs +++ b/node/src/components/network/tasks.rs @@ -685,110 +685,111 @@ where let demands_in_flight = Arc::new(Semaphore::new(context.max_in_flight_demands)); let event_queue = context.event_queue.expect("component not initialized"); - let read_messages = - async move { - while let Some(msg_result) = stream.next().await { - match msg_result { - Ok(msg) => { - trace!(%msg, "message received"); - - let effect_builder = EffectBuilder::new(event_queue); - - match msg.try_into_demand(effect_builder, peer_id) { - Ok((event, wait_for_response)) => { - // Note: For now, demands bypass the limiter, as we expect the - // backpressure to handle this instead. - - // Acquire a permit. If we are handling too many demands at this - // time, this will block, halting the processing of new message, - // thus letting the peer they have reached their maximum allowance. - let in_flight = demands_in_flight - .clone() - .acquire_owned() - .await - // Note: Since the semaphore is reference counted, it must - // explicitly be closed for acquisition to fail, which we - // never do. If this happens, there is a bug in the code; - // we exit with an error and close the connection. - .map_err(|_| { - io::Error::new( - io::ErrorKind::Other, - "demand limiter semaphore closed unexpectedly", - ) - })?; - - Metrics::record_trie_request_start(&context.net_metrics); - - let net_metrics = context.net_metrics.clone(); - // Spawn a future that will eventually send the returned message. It - // will essentially buffer the response. - tokio::spawn(async move { - if let Some(payload) = wait_for_response.await { - // Send message and await its return. `send_message` should - // only return when the message has been buffered, if the - // peer is not accepting data, we will block here until the - // send buffer has sufficient room. - effect_builder.send_message(peer_id, payload).await; - - // Note: We could short-circuit the event queue here and - // directly insert into the outgoing message queue, - // which may be potential performance improvement. - } - - // Missing else: The handler of the demand did not deem it - // worthy a response. Just drop it. - - // After we have either successfully buffered the message for - // sending, failed to do so or did not have a message to send - // out, we consider the request handled and free up the permit. - Metrics::record_trie_request_end(&net_metrics); - drop(in_flight); - }); - - // Schedule the created event. - event_queue - .schedule::(event, QueueKind::NetworkDemand) - .await; - } - Err(msg) => { - // We've received a non-demand message. Ensure we have the proper amount - // of resources, then push it to the reactor. - limiter - .request_allowance(msg.payload_incoming_resource_estimate( - &context.payload_weights, - )) - .await; - - let queue_kind = if msg.is_low_priority() { - QueueKind::NetworkLowPriority - } else { - QueueKind::NetworkIncoming - }; - - event_queue - .schedule( - Event::IncomingMessage { - peer_id: Box::new(peer_id), - msg, - span: span.clone(), - }, - queue_kind, + let read_messages = async move { + while let Some(msg_result) = stream.next().await { + match msg_result { + Ok(msg) => { + trace!(%msg, "message received"); + + let effect_builder = EffectBuilder::new(event_queue); + + match msg.try_into_demand(effect_builder, peer_id) { + Ok((event, wait_for_response)) => { + // Note: For now, demands bypass the limiter, as we expect the + // backpressure to handle this instead. + + // Acquire a permit. If we are handling too many demands at this + // time, this will block, halting the processing of new message, + // thus letting the peer they have reached their maximum allowance. + let in_flight = demands_in_flight + .clone() + .acquire_owned() + .await + // Note: Since the semaphore is reference counted, it must + // explicitly be closed for acquisition to fail, which we + // never do. If this happens, there is a bug in the code; + // we exit with an error and close the connection. + .map_err(|_| { + io::Error::new( + io::ErrorKind::Other, + "demand limiter semaphore closed unexpectedly", ) - .await; - } + })?; + + Metrics::record_trie_request_start(&context.net_metrics); + + let net_metrics = context.net_metrics.clone(); + // Spawn a future that will eventually send the returned message. It + // will essentially buffer the response. + tokio::spawn(async move { + if let Some(payload) = wait_for_response.await { + // Send message and await its return. `send_message` should + // only return when the message has been buffered, if the + // peer is not accepting data, we will block here until the + // send buffer has sufficient room. + effect_builder.send_message(peer_id, payload).await; + + // Note: We could short-circuit the event queue here and + // directly insert into the outgoing message queue, + // which may be potential performance improvement. + } + + // Missing else: The handler of the demand did not deem it + // worthy a response. Just drop it. + + // After we have either successfully buffered the message for + // sending, failed to do so or did not have a message to send + // out, we consider the request handled and free up the permit. + Metrics::record_trie_request_end(&net_metrics); + drop(in_flight); + }); + + // Schedule the created event. + event_queue + .schedule::(event, QueueKind::NetworkDemand) + .await; + } + Err(msg) => { + // We've received a non-demand message. Ensure we have the proper amount + // of resources, then push it to the reactor. + limiter + .request_allowance( + msg.payload_incoming_resource_estimate( + &context.payload_weights, + ), + ) + .await; + + let queue_kind = if msg.is_low_priority() { + QueueKind::NetworkLowPriority + } else { + QueueKind::NetworkIncoming + }; + + event_queue + .schedule( + Event::IncomingMessage { + peer_id: Box::new(peer_id), + msg, + span: span.clone(), + }, + queue_kind, + ) + .await; } - } - Err(err) => { - warn!( - err = display_error(&err), - "receiving message failed, closing connection" - ); - return Err(err); } } + Err(err) => { + warn!( + err = display_error(&err), + "receiving message failed, closing connection" + ); + return Err(err); + } } - Ok(()) - }; + } + Ok(()) + }; let shutdown_messages = async move { while close_incoming_receiver.changed().await.is_ok() {} }; diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index 0bdaf4f594..27fb4c96a3 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -14,15 +14,15 @@ use prometheus::Registry; use tracing::{debug, error, trace}; use casper_storage::data_access_layer::{ - balance::BalanceHandling, BalanceIdentifier, BalanceRequest, ProofHandling, + balance::BalanceHandling, BalanceRequest, ProofHandling, QueryRequest, QueryResult, }; use casper_types::{ - account::AccountHash, addressable_entity::AddressableEntity, system::auction::ARG_AMOUNT, + account::AccountHash, addressable_entity::AddressableEntity, evm, system::auction::ARG_AMOUNT, AddressableEntityHash, AddressableEntityIdentifier, BlockHeader, CLType, Chainspec, EntityAddr, EntityKind, EntityVersion, EntityVersionKey, ExecutableDeployItem, - ExecutableDeployItemIdentifier, Package, PackageAddr, PackageHash, PackageIdentifier, - Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, TransactionTarget, - DEFAULT_ENTRY_POINT_NAME, U512, + ExecutableDeployItemIdentifier, Key, Package, PackageAddr, PackageHash, PackageIdentifier, + StoredValue, Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, + TransactionTarget, DEFAULT_ENTRY_POINT_NAME, U512, }; use crate::{ @@ -46,6 +46,16 @@ const COMPONENT_NAME: &str = "transaction_acceptor"; const ARG_TARGET: &str = "target"; +fn evm_account_from_query_result(query_result: QueryResult) -> Option { + match query_result { + QueryResult::Success { value, .. } => match *value { + StoredValue::Evm(evm::EvmValue::Account(account)) => Some(account), + _ => None, + }, + QueryResult::RootNotFound | QueryResult::ValueNotFound(_) | QueryResult::Failure(_) => None, + } +} + /// A helper trait constraining `TransactionAcceptor` compatible reactor events. pub(crate) trait ReactorEventT: From @@ -213,24 +223,22 @@ impl TransactionAcceptor { } }; - if event_metadata.source.is_client() { - if let Some(evm_transaction) = event_metadata.meta_transaction.as_evm() { - let balance_request = BalanceRequest::new( - *block_header.state_root_hash(), - block_header.protocol_version(), - BalanceIdentifier::Evm(evm_transaction.from()), - BalanceHandling::Available, - ProofHandling::NoProofs, - ); - return effect_builder - .get_balance(balance_request) - .event(move |balance_result| Event::GetBalanceResult { - event_metadata, - block_header, - maybe_balance: balance_result.available_balance().copied(), - }); - } + if let Some(evm_transaction) = event_metadata.meta_transaction.as_evm() { + let query_request = QueryRequest::new( + *block_header.state_root_hash(), + Key::Evm(evm::EvmAddr::Account(evm_transaction.from())), + vec![], + ); + return effect_builder + .query_global_state(query_request) + .event(move |query_result| Event::GetEvmAccountResult { + event_metadata, + block_header, + maybe_account: evm_account_from_query_result(query_result), + }); + } + if event_metadata.source.is_client() { let initiator_addr = event_metadata.transaction.initiator_addr(); let account_hash = initiator_addr .account_hash() @@ -248,6 +256,61 @@ impl TransactionAcceptor { } } + fn handle_get_evm_account_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + maybe_account: Option, + ) -> Effects { + let account = match maybe_account { + Some(account) => account, + None => { + let initiator_addr = event_metadata.transaction.initiator_addr(); + let error = Error::parameter_failure( + &block_header, + ParameterFailure::UnknownBalance { initiator_addr }, + ); + return self.reject_transaction(effect_builder, *event_metadata, error); + } + }; + + let evm_transaction = event_metadata + .meta_transaction + .as_evm() + .expect("EVM account lookup should only be used for EVM transactions"); + let expected = account.nonce(); + let actual = evm_transaction.nonce(); + if actual != expected { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::InvalidNonce { expected, actual }, + )), + ); + } + + if event_metadata.source.is_client() { + let balance_request = BalanceRequest::from_purse( + *block_header.state_root_hash(), + block_header.protocol_version(), + account.main_purse(), + BalanceHandling::Available, + ProofHandling::NoProofs, + ); + effect_builder + .get_balance(balance_request) + .event(move |balance_result| Event::GetBalanceResult { + event_metadata, + block_header, + maybe_balance: balance_result.available_balance().copied(), + }) + } else { + self.verify_payment(effect_builder, event_metadata, block_header) + } + } + fn handle_get_entity_result( &mut self, effect_builder: EffectBuilder, @@ -471,8 +534,7 @@ impl TransactionAcceptor { return self.reject_transaction(effect_builder, *event_metadata, error); }; if !self.chainspec.evm_config.enabled - && target.cl_type() - == &CLType::ByteArray(casper_types::evm::ADDRESS_LENGTH as u32) + && target.cl_type() == &CLType::ByteArray(evm::ADDRESS_LENGTH as u32) { let error = Error::parameter_failure( &block_header, @@ -1091,6 +1153,16 @@ impl Component for TransactionAcceptor { block_header, maybe_balance, ), + Event::GetEvmAccountResult { + event_metadata, + block_header, + maybe_account, + } => self.handle_get_evm_account_result( + effect_builder, + event_metadata, + block_header, + maybe_account, + ), Event::GetContractResult { event_metadata, block_header, diff --git a/node/src/components/transaction_acceptor/event.rs b/node/src/components/transaction_acceptor/event.rs index d03c949e3a..3919ea90c9 100644 --- a/node/src/components/transaction_acceptor/event.rs +++ b/node/src/components/transaction_acceptor/event.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Display, Formatter}; use serde::Serialize; use casper_types::{ - contracts::ProtocolVersionMajor, AddressableEntity, AddressableEntityHash, BlockHeader, + contracts::ProtocolVersionMajor, evm, AddressableEntity, AddressableEntityHash, BlockHeader, EntityVersion, Package, PackageHash, Timestamp, Transaction, U512, }; @@ -78,6 +78,12 @@ pub(crate) enum Event { block_header: Box, maybe_balance: Option, }, + /// The result of querying global state for the EVM account associated with an EVM transaction. + GetEvmAccountResult { + event_metadata: Box, + block_header: Box, + maybe_account: Option, + }, /// The result of querying global state for a `Contract` to verify the executable logic. GetContractResult { event_metadata: Box, @@ -176,6 +182,13 @@ impl Display for Event { event_metadata.transaction.hash() ) } + Event::GetEvmAccountResult { event_metadata, .. } => { + write!( + formatter, + "verifying EVM account nonce to validate transaction with hash {}", + event_metadata.transaction.hash() + ) + } Event::GetContractResult { event_metadata, block_header, diff --git a/node/src/components/transaction_acceptor/tests.rs b/node/src/components/transaction_acceptor/tests.rs index b7b8acbffa..5c4604cc20 100644 --- a/node/src/components/transaction_acceptor/tests.rs +++ b/node/src/components/transaction_acceptor/tests.rs @@ -8,11 +8,18 @@ use std::{ time::Duration, }; +use alloy_consensus::{SignableTransaction, TxEnvelope, TxLegacy}; +use alloy_eips::Encodable2718; +use alloy_primitives::{ + Address as AlloyAddress, Bytes as AlloyBytes, Signature as AlloySignature, TxKind, + U256 as AlloyU256, +}; use derive_more::From; use futures::{ channel::oneshot::{self, Sender}, FutureExt, }; +use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey}; use prometheus::Registry; use reactor::ReactorEvent; use serde::Serialize; @@ -70,6 +77,8 @@ use crate::{ const POLL_INTERVAL: Duration = Duration::from_millis(10); const TIMEOUT: Duration = Duration::from_secs(30); +const EVM_TEST_CHAIN_ID: u64 = 1_129_533_695; +const EVM_TEST_GAS_PRICE: u128 = 1_000_000; /// Top-level event for the reactor. #[derive(Debug, From, Serialize)] @@ -195,6 +204,7 @@ enum TxnType { #[derive(Clone, PartialEq, Eq, Debug)] enum TestScenario { + FromPeerEvmInvalidNonce, FromPeerInvalidTransaction(TxnType), FromPeerInvalidTransactionZeroPayment(TxnType), FromPeerExpired(TxnType), @@ -208,6 +218,7 @@ enum TestScenario { FromPeerSessionContract(TxnType, ContractScenario), FromPeerSessionContractPackage(TxnType, ContractPackageScenario), FromClientInvalidTransaction(TxnType), + FromClientEvmInvalidNonce, FromClientInvalidTransactionZeroPayment(TxnType), FromClientSlightlyFutureDatedTransaction(TxnType), FromClientFutureDatedTransaction(TxnType), @@ -257,6 +268,7 @@ impl TestScenario { fn source(&self, rng: &mut NodeRng) -> Source { match self { TestScenario::FromPeerInvalidTransaction(_) + | TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromPeerInvalidTransactionZeroPayment(_) | TestScenario::FromPeerExpired(_) | TestScenario::FromPeerValidTransaction(_) @@ -271,6 +283,7 @@ impl TestScenario { | TestScenario::FromPeerSessionContractPackage(..) | TestScenario::InvalidFieldsFromPeer => Source::Peer(NodeId::random(rng)), TestScenario::FromClientInvalidTransaction(_) + | TestScenario::FromClientEvmInvalidNonce | TestScenario::FromClientInvalidTransactionZeroPayment(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) | TestScenario::FromClientFutureDatedTransaction(_) @@ -325,6 +338,9 @@ impl TestScenario { txn.invalidate(); Transaction::from(txn) } + TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromClientEvmInvalidNonce => { + Transaction::from(signed_evm_legacy_transaction(1)) + } TestScenario::FromClientInvalidTransactionZeroPayment(TxnType::V1) => { let txn = TransactionV1Builder::new_session( false, @@ -877,10 +893,12 @@ impl TestScenario { | TestScenario::FromClientSlightlyFutureDatedTransaction(_) | TestScenario::FromClientSignedByAdmin(..) => true, TestScenario::FromPeerInvalidTransaction(_) + | TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromPeerInvalidTransactionZeroPayment(_) | TestScenario::FromClientInsufficientBalance(_) | TestScenario::FromClientMissingAccount(_) | TestScenario::FromClientInvalidTransaction(_) + | TestScenario::FromClientEvmInvalidNonce | TestScenario::FromClientInvalidTransactionZeroPayment(_) | TestScenario::FromClientFutureDatedTransaction(_) | TestScenario::FromClientAccountWithInsufficientWeight(_) @@ -964,6 +982,39 @@ impl TestScenario { fn is_v2_casper_vm(&self) -> bool { matches!(self, TestScenario::VmCasperV2ByPackageHash) } + + fn is_evm(&self) -> bool { + matches!( + self, + TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromClientEvmInvalidNonce + ) + } +} + +fn signed_evm_legacy_transaction(nonce: u64) -> evm::Transaction { + let recipient = evm::Address::new([1; evm::ADDRESS_LENGTH]); + let transaction = TxLegacy { + chain_id: Some(EVM_TEST_CHAIN_ID), + nonce, + gas_price: EVM_TEST_GAS_PRICE, + gas_limit: 21_000, + to: TxKind::Call(AlloyAddress::from(recipient.value())), + value: AlloyU256::ZERO, + input: AlloyBytes::new(), + }; + let signing_key = + SigningKey::from_slice(&[0x11; 32]).expect("test EVM private key should be valid"); + let (signature, recovery_id) = signing_key + .sign_prehash(transaction.signature_hash().as_ref()) + .expect("test EVM transaction signing should succeed"); + let signed = transaction.into_signed(AlloySignature::from((signature, recovery_id))); + let envelope = TxEnvelope::from(signed); + evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::now(), + TimeDiff::from_seconds(300), + ) + .expect("test EVM transaction should decode") } fn create_account(account_hash: AccountHash, test_scenario: &TestScenario) -> Account { @@ -1037,9 +1088,17 @@ impl reactor::Reactor for Reactor { request: query_request, responder, } => { - let query_result = if let Key::Hash(_) | Key::SmartContract(_) = + let query_result = if let Key::Evm(evm::EvmAddr::Account(address)) = query_request.key() { + let main_purse = evm::deterministic_purse(address); + QueryResult::Success { + value: Box::new(StoredValue::Evm(evm::EvmValue::Account( + evm::Account::new(0, evm::EMPTY_CODE_HASH, main_purse), + ))), + proofs: vec![], + } + } else if let Key::Hash(_) | Key::SmartContract(_) = query_request.key() { match &self.test_scenario { TestScenario::FromPeerCustomPaymentContractPackage( ContractPackageScenario::MissingPackageAtHash, @@ -1458,6 +1517,10 @@ async fn run_transaction_acceptor_without_timeout( chainspec.with_vm_casper_v2(true); chainspec } + test_scenario if test_scenario.is_evm() => { + chainspec.evm_config.enabled = true; + chainspec + } _ => chainspec, }; chainspec.core_config.administrators = iter::once(PublicKey::from(&admin)).collect(); @@ -1542,6 +1605,7 @@ async fn run_transaction_acceptor_without_timeout( // Check that invalid transactions sent by a client raise the `InvalidTransaction` // announcement with the appropriate source. TestScenario::FromClientInvalidTransaction(_) + | TestScenario::FromClientEvmInvalidNonce | TestScenario::FromClientInvalidTransactionZeroPayment(_) | TestScenario::FromClientFutureDatedTransaction(_) | TestScenario::FromClientMissingAccount(_) @@ -1630,6 +1694,7 @@ async fn run_transaction_acceptor_without_timeout( // Check that invalid transactions sent by a peer raise the `InvalidTransaction` // announcement with the appropriate source. TestScenario::FromPeerInvalidTransaction(_) + | TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromPeerInvalidTransactionZeroPayment(_) | TestScenario::BalanceCheckForDeploySentByPeer | TestScenario::InvalidFieldsFromPeer => { @@ -1844,6 +1909,20 @@ async fn should_reject_invalid_transaction_v1_from_peer() { )) } +#[tokio::test] +async fn should_reject_evm_transaction_with_invalid_nonce_from_peer() { + let result = run_transaction_acceptor(TestScenario::FromPeerEvmInvalidNonce).await; + assert!(matches!( + result, + Err(super::Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::InvalidNonce { + expected: 0, + actual: 1 + } + ))) + )) +} + #[tokio::test] async fn should_reject_zero_payment_transaction_v1_from_peer() { let result = run_transaction_acceptor(TestScenario::FromPeerInvalidTransactionZeroPayment( @@ -1944,6 +2023,20 @@ async fn should_reject_invalid_transaction_v1_from_client() { )) } +#[tokio::test] +async fn should_reject_evm_transaction_with_invalid_nonce_from_client() { + let result = run_transaction_acceptor(TestScenario::FromClientEvmInvalidNonce).await; + assert!(matches!( + result, + Err(super::Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::InvalidNonce { + expected: 0, + actual: 1 + } + ))) + )) +} + #[tokio::test] async fn should_reject_invalid_transaction_v1_zero_payment_from_client() { let result = run_transaction_acceptor(TestScenario::FromClientInvalidTransactionZeroPayment( diff --git a/storage/src/system/transfer.rs b/storage/src/system/transfer.rs index 419adbe86e..f0e077c3f8 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -449,27 +449,28 @@ impl TransferRuntimeArgsBuilder { where R: StateReader, { - let (to, target) = - match self.resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))? { - TransferTargetMode::ExistingAccount { - main_purse: purse_uref, - target_account_hash: target_account, - } => (Some(target_account), purse_uref), - TransferTargetMode::ExistingEvmAccount { - main_purse: purse_uref, - .. - } => (None, purse_uref), - TransferTargetMode::PurseExists { - target_account_hash, - purse_uref, - } => (target_account_hash, purse_uref), - TransferTargetMode::CreateAccount(_) | TransferTargetMode::CreateEvmAccount(_) => { - // Method "build()" is called after `resolve_transfer_target_mode` is first called - // and handled by creating a new account. Calling `resolve_transfer_target_mode` - // for the second time should never return `CreateAccount` variant. - return Err(TransferError::InvalidOperation); - } - }; + let (to, target) = match self + .resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))? + { + TransferTargetMode::ExistingAccount { + main_purse: purse_uref, + target_account_hash: target_account, + } => (Some(target_account), purse_uref), + TransferTargetMode::ExistingEvmAccount { + main_purse: purse_uref, + .. + } => (None, purse_uref), + TransferTargetMode::PurseExists { + target_account_hash, + purse_uref, + } => (target_account_hash, purse_uref), + TransferTargetMode::CreateAccount(_) | TransferTargetMode::CreateEvmAccount(_) => { + // Method "build()" is called after `resolve_transfer_target_mode` is first called + // and handled by creating a new account. Calling `resolve_transfer_target_mode` + // for the second time should never return `CreateAccount` variant. + return Err(TransferError::InvalidOperation); + } + }; let source = self.resolve_source_uref(from, Rc::clone(&tracking_copy))?; diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index ccad44e2da..9d194f85cd 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -267,6 +267,13 @@ pub enum TransactionError { /// Configured EVM block gas limit. block_gas_limit: u64, }, + /// The transaction nonce does not match the account nonce in global state. + InvalidNonce { + /// Expected account nonce. + expected: u64, + /// Transaction nonce. + actual: u64, + }, /// The transaction does not contain an EVM approval. MissingApproval, /// The transaction contains more than one approval. @@ -344,6 +351,12 @@ impl Display for TransactionError { "EVM gas limit {gas_limit} exceeds block gas limit {block_gas_limit}" ) } + TransactionError::InvalidNonce { expected, actual } => { + write!( + formatter, + "EVM transaction nonce {actual} does not match account nonce {expected}" + ) + } TransactionError::MissingApproval => formatter.write_str("missing EVM approval"), TransactionError::MultipleApprovals => formatter.write_str("multiple EVM approvals"), TransactionError::NonSecp256k1Approval => { From c007db1002fe1644f5a7d111de7ff8dc8aa3316c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Wed, 13 May 2026 17:41:15 +0200 Subject: [PATCH 12/17] Remove "FeeCharge" experiment --- EVM.md | 7 ++- executor/evm/src/executor.rs | 18 +++---- executor/evm/src/lib.rs | 4 +- executor/evm/src/request.rs | 11 ---- executor/evm/tests/executor.rs | 52 ++++++++++++++++--- .../components/contract_runtime/operations.rs | 4 +- 6 files changed, 58 insertions(+), 38 deletions(-) diff --git a/EVM.md b/EVM.md index dc4780f180..0d4df4d3b7 100644 --- a/EVM.md +++ b/EVM.md @@ -263,7 +263,7 @@ When execution proceeds: - deterministic proposer-derived beneficiary, - `[evm].block_gas_limit`, - `[evm].base_fee`. -5. Runtime calls `casper-executor-evm` with `FeeCharge::External`. +5. Runtime calls `casper-executor-evm`. 6. `revm` executes EVM account, nonce, code, storage, log, create, and value transfer semantics. 7. Runtime commits EVM tracking-copy effects into scratch global state. @@ -274,8 +274,8 @@ When execution proceeds: 11. Runtime applies Casper fee handling. 12. Runtime stores `ExecutionResult::Evm`. -`FeeCharge::External` is important. It prevents `revm` from charging gas fees -from EVM balances. Casper runtime owns fee and refund policy. +The executor always disables `revm` gas fee balance mutation. Casper runtime +owns fee and refund policy. ## Fee And Refund Policy @@ -416,7 +416,6 @@ runs `casper-executor-evm` with: - `ExecuteKind::Call`, - `CallValidation::UncheckedSimulation`, -- `FeeCharge::External`, and returns output, status, and gas used. The tracking-copy effects are discarded. diff --git a/executor/evm/src/executor.rs b/executor/evm/src/executor.rs index 8f2c89a7c4..7139ac97a9 100644 --- a/executor/evm/src/executor.rs +++ b/executor/evm/src/executor.rs @@ -13,7 +13,7 @@ use revm::{ use crate::{ db::CasperDb, state, tx, BlockHashProvider, DbError, Error, ExecuteKind, ExecuteRequest, - ExecutionOutcome, FeeCharge, NoBlockHashProvider, Result, + ExecutionOutcome, NoBlockHashProvider, Result, }; /// Executes EVM transactions and calls against a Casper tracking copy. @@ -88,8 +88,6 @@ impl EvmExecutor { ExecuteKind::Transaction(_) => false, ExecuteKind::Call(call) => call.validation.is_unchecked_simulation(), }; - let fee_charge_disabled = - matches!(request.fee_charge, FeeCharge::External) || skip_validation; let result_and_state = { let db = CasperDb::new(tracking_copy, block_hash_provider); @@ -104,7 +102,7 @@ impl EvmExecutor { cfg.disable_base_fee = skip_validation; cfg.disable_balance_check = skip_validation; cfg.disable_nonce_check = skip_validation; - cfg.disable_fee_charge = fee_charge_disabled; + cfg.disable_fee_charge = true; }) .build_mainnet(); @@ -113,13 +111,11 @@ impl EvmExecutor { let outcome = ExecutionOutcome::from_revm_result(&result_and_state.result); let mut state = result_and_state.state; - if fee_charge_disabled { - // revm skips the upfront fee debit but still applies the - // post-execution gas reimbursement and beneficiary reward. - let disabled_fee_transfers = - disabled_fee_transfers(&self.config, spec, &request, &result_and_state.result); - state::remove_disabled_fee_transfers(&mut state, disabled_fee_transfers)?; - } + // revm skips the upfront fee debit but still applies the + // post-execution gas reimbursement and beneficiary reward. + let disabled_fee_transfers = + disabled_fee_transfers(&self.config, spec, &request, &result_and_state.result); + state::remove_disabled_fee_transfers(&mut state, disabled_fee_transfers)?; state::apply(tracking_copy, state)?; Ok(outcome) } diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs index fb44ce45be..8add0f4fd2 100644 --- a/executor/evm/src/lib.rs +++ b/executor/evm/src/lib.rs @@ -19,9 +19,7 @@ pub use block_hash::{ pub use error::{DbError, Error, Result}; pub use executor::EvmExecutor; pub use outcome::{ExecutionOutcome, ExecutionStatus}; -pub use request::{ - BlockContext, CallRequest, CallValidation, ExecuteKind, ExecuteRequest, FeeCharge, -}; +pub use request::{BlockContext, CallRequest, CallValidation, ExecuteKind, ExecuteRequest}; use casper_types::evm; diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs index e28a61027a..d805fd70db 100644 --- a/executor/evm/src/request.rs +++ b/executor/evm/src/request.rs @@ -11,8 +11,6 @@ pub struct ExecuteRequest { pub block: BlockContext, /// EVM work item to execute. pub kind: ExecuteKind, - /// Component responsible for charging EVM gas fees. - pub fee_charge: FeeCharge, } /// EVM work item to execute. @@ -64,15 +62,6 @@ impl CallValidation { } } -/// Component responsible for mutating account balances for gas fees. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum FeeCharge { - /// Let the EVM apply Ethereum gas fee debits and refunds. - Evm, - /// Skip EVM gas fee balance mutation so the caller can charge fees externally. - External, -} - /// Per-execution block context. #[derive(Clone, Debug, Eq, PartialEq)] pub struct BlockContext { diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 1714c1f600..58eff9abdb 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -5,7 +5,7 @@ use alloy_eips::eip2718::Encodable2718; use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, U256}; use casper_executor_evm::{ BlockContext, BlockHashProvider, BlockHashProviderResult, CallRequest, CallValidation, Error, - EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, FeeCharge, EMPTY_CODE_HASH, + EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, EMPTY_CODE_HASH, }; use casper_storage::{ data_access_layer::{GenesisRequest, GenesisResult}, @@ -172,7 +172,6 @@ fn call_request( nonce: 0, validation: CallValidation::UncheckedSimulation, }), - fee_charge: FeeCharge::Evm, } } @@ -194,7 +193,6 @@ fn checked_call_request( nonce: 0, validation: CallValidation::Checked, }), - fee_charge: FeeCharge::Evm, } } @@ -550,6 +548,49 @@ fn erc20_and_native_purse_balances_update() { assert_eq!(decode_word(&remaining_allowance.output), 60); } +#[test] +fn nonzero_gas_price_does_not_charge_evm_balances() { + let executor = executor(evm::EvmSpec::Prague); + let sender = evm::Address::new([1; 20]); + let recipient = evm::Address::new([2; 20]); + let beneficiary = evm::Address::new([3; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let initial_balance = U512::from(10_000_000u64); + let transfer_value = CasperU256::from(250u64); + + seed_evm_balance(&mut tracking_copy, sender, initial_balance); + let mut block_context = block(); + block_context.beneficiary = beneficiary; + let request = ExecuteRequest { + block: block_context, + kind: ExecuteKind::Call(CallRequest { + from: sender, + to: Some(recipient), + value: transfer_value, + input: Vec::new(), + gas_limit: 100_000, + gas_price: 2, + nonce: 0, + validation: CallValidation::Checked, + }), + }; + + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("native EVM transfer should succeed"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!( + read_balance(&mut tracking_copy, sender), + initial_balance - U512::from(250u64) + ); + assert_eq!( + read_balance(&mut tracking_copy, recipient), + U512::from(250u64) + ); + assert_eq!(read_balance(&mut tracking_copy, beneficiary), U512::zero()); +} + #[test] fn erc721_mint_approve_and_transfer() { let executor = executor(evm::EvmSpec::Prague); @@ -717,7 +758,6 @@ fn signed_transactions_require_configured_chain_id() { let request = ExecuteRequest { block: block(), kind: ExecuteKind::Transaction(missing_chain_id), - fee_charge: FeeCharge::Evm, }; assert!(matches!( executor.execute(&mut tracking_copy, request), @@ -735,7 +775,6 @@ fn signed_transactions_require_configured_chain_id() { let request = ExecuteRequest { block: block(), kind: ExecuteKind::Transaction(transaction), - fee_charge: FeeCharge::Evm, }; assert!(matches!( wrong_chain_executor.execute(&mut tracking_copy, request), @@ -753,7 +792,8 @@ fn checked_calls_enforce_transaction_validation() { let recipient = evm::Address::new([2; 20]); let (mut tracking_copy, _tempdir) = tracking_copy(); - let request = checked_call_request(from, Some(recipient), Vec::new(), CasperU256::from(1)); + let mut request = checked_call_request(from, Some(recipient), Vec::new(), CasperU256::zero()); + request.block.base_fee = Some(1); assert!(matches!( executor.execute(&mut tracking_copy, request), Err(Error::Revm(_)) diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 6e6f2201de..14637fe88d 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -14,7 +14,7 @@ use casper_executor_evm::{ BlockContext as EvmBlockContext, BlockHashProvider as EvmBlockHashProvider, BlockHashProviderResult as EvmBlockHashProviderResult, EvmExecutor, ExecuteKind as EvmExecuteKind, ExecuteRequest as EvmExecuteRequest, - ExecutionStatus as EvmExecutionStatus, FeeCharge as EvmFeeCharge, + ExecutionStatus as EvmExecutionStatus, }; use casper_storage::{ block_store::types::ApprovalsHashes, @@ -719,7 +719,6 @@ pub fn execute_finalized_block( let request = EvmExecuteRequest { block: block_context, kind: EvmExecuteKind::Transaction(evm_transaction.clone()), - fee_charge: EvmFeeCharge::External, }; let mut tracking_copy = scratch_state .tracking_copy(state_root_hash)? @@ -1654,7 +1653,6 @@ where let execute_request = EvmExecuteRequest { block: block_context, kind: EvmExecuteKind::Call(call), - fee_charge: EvmFeeCharge::External, }; let block_hash_provider = StaticEvmBlockHashProvider { block_hashes }; let outcome = EvmExecutor::new(chainspec.evm_config) From 4b03b21f2a16df5fa97a45b422916aa784cce607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 14 May 2026 23:34:16 +0200 Subject: [PATCH 13/17] Link EVM senders to Casper accounts EVM addresses and Casper account hashes come from different preimages, so using the same signing key currently gives users two separate identities. This change adds a small identity record under `EvmAddr::Account` so an EVM address can point either at an existing Casper account or at an EVM-native purse. The old EVM account blob is split into separate records for identity, nonce, code hash, bytecode, and storage. Runtime and transaction validation now resolve the EVM sender before balance and nonce checks, while the executor only reads and writes the new layout. When an EVM transaction is signed by a key that already has a Casper account, the runtime links the EVM sender address to that account. When no Casper account exists yet, it creates one using the deterministic EVM purse as the main purse. EVM-native accounts and contract-created accounts stay purse-backed and are not forced into Casper account identities. Native transfers to EVM addresses now follow the same identity model: linked accounts credit the Casper main purse, EVM-native identities credit their purse, and missing targets create the deterministic purse-backed identity first. This also removes the unreleased `EvmValue::Account` and `EvmValue::ByteCode` storage forms, storing bytecode as `StoredValue::ByteCode` instead. --- EVM.md | 113 ++--- execution_engine/src/runtime_context/mod.rs | 3 +- executor/evm/src/account_state.rs | 338 +++++++++++++++ executor/evm/src/db.rs | 50 ++- executor/evm/src/error.rs | 52 ++- executor/evm/src/lib.rs | 1 + executor/evm/src/state.rs | 81 ++-- executor/evm/tests/executor.rs | 147 ++++++- node/src/components/contract_runtime/error.rs | 21 +- .../components/contract_runtime/operations.rs | 363 +++++++++++++++- node/src/components/transaction_acceptor.rs | 407 ++++++++++++++++-- .../components/transaction_acceptor/event.rs | 107 ++++- .../components/transaction_acceptor/tests.rs | 71 ++- .../main_reactor/tests/transactions.rs | 116 ++++- .../src/types/transaction/meta_transaction.rs | 1 + storage/src/data_access_layer.rs | 4 +- storage/src/data_access_layer/balance.rs | 68 +-- storage/src/global_state/state/mod.rs | 31 +- storage/src/system/transfer.rs | 52 ++- storage/src/tracking_copy/byte_size.rs | 1 - storage/src/tracking_copy/mod.rs | 5 - types/src/evm.rs | 4 +- types/src/evm/account.rs | 123 ------ types/src/evm/evm_addr.rs | 30 +- types/src/evm/evm_value.rs | 109 ----- types/src/evm/hash.rs | 8 +- types/src/evm/transaction.rs | 5 + types/src/execution/transform_kind.rs | 5 - types/src/gens.rs | 23 +- types/src/key.rs | 38 ++ types/src/stored_value.rs | 34 +- types/src/transaction/initiator_addr.rs | 11 +- 32 files changed, 1868 insertions(+), 554 deletions(-) create mode 100644 executor/evm/src/account_state.rs delete mode 100644 types/src/evm/evm_value.rs diff --git a/EVM.md b/EVM.md index 0d4df4d3b7..5fd6583a7f 100644 --- a/EVM.md +++ b/EVM.md @@ -31,8 +31,8 @@ Implemented in this workspace: transactions, accounts, config, and receipts. - `Transaction::Evm` and `TransactionHash::Evm`. - `ExecutionResult::Evm` carrying EVM receipt data. -- Global-state keys and values for EVM account metadata, bytecode, and - storage. +- Global-state keys and values for EVM account identity links, nonce, code + hash, bytecode, and storage. - EVM hash wrappers backed by Casper `Digest` while preserving raw Ethereum 32-byte values. - `casper-executor-evm`, backed by `revm`, with Casper-owned public types. @@ -40,7 +40,7 @@ Implemented in this workspace: - Casper fee and refund handling for EVM transactions. - Binary-port `Simulate` for read-only `eth_call` support. - Native Casper transfers to 20-byte EVM addresses when `[evm].enabled = true`, - creating or funding the corresponding EVM account record. + creating or funding the corresponding EVM-native purse identity. Implemented in the sidecar workspace for validation: @@ -178,7 +178,7 @@ top-level approval replacement hook used after storage lookup leaves `Transaction::Evm` is converted into `MetaTransaction::Evm` and routed through the normal transaction acceptor skeleton. EVM branches only where the payload -genuinely differs from Deploy and V1/V2 transactions: identity, balance +genuinely differs from Deploy and native Transaction::V1 payloads: identity, balance lookup, no Casper session/payment validation, and EVM-specific chainspec checks. @@ -197,17 +197,22 @@ For client-submitted EVM transactions, the acceptor currently validates: 9. [EIP-1559][eip-1559] `max_priority_fee_per_gas` must be zero because Casper does not currently prioritize transactions based on transaction gas parameters. -10. The EVM account for `from` must exist and resolve to a balance. -11. The transaction nonce must match the EVM account nonce in global state. +10. The EVM account identity for `from` must resolve to a balance, or the + recovered secp256k1 signer must resolve to a Casper account balance or the + address's deterministic EVM purse balance. +11. The transaction nonce must match the EVM nonce in global state, defaulting + to `0` before the first EVM transaction for that address. 12. That balance must meet the chain baseline motes requirement. -The acceptor does not require a Casper `AddressableEntity` for the EVM sender. -The sender identity is `InitiatorAddr::EvmAddress(transaction.from())`, and -the acceptor reads the EVM account from global state before checking its backing -main purse balance. The acceptor only checks that the EVM initiator has a known -balance, uses the current account nonce, and meets the same baseline balance -requirement used for other client transactions. The runtime later checks the full -EVM maximum fee amount. +The acceptor does not require a Casper `AddressableEntity` for every EVM +address. The sender identity is still +`InitiatorAddr::EvmAddress(transaction.from())`. If the EVM address is linked to +`Key::Account(account_hash)`, the Casper account's main purse is used. If it is +an EVM-native identity, the stored purse is used. If no EVM identity exists yet, +the acceptor uses the recovered signer public key to check the corresponding +Casper account balance, falling back to the address's deterministic EVM purse +when no Casper account exists. The runtime later checks the full EVM maximum +fee amount. The nonce check is also applied to peer-sourced EVM transactions before storage, so a gossiped transaction with a nonce that cannot execute at the current state @@ -216,7 +221,7 @@ root is rejected before it can enter the transaction buffer. ## Runtime Execution Finalized block execution now routes EVM transactions through the same -per-transaction accounting skeleton used by Deploy and V1/V2 transactions. +per-transaction accounting skeleton used by Deploy and native Transaction::V1 payloads. Runtime constructs `MetaTransaction::Evm` before entering the loop's normal balance, hold, refund, fee, and artifact builder flow. @@ -253,8 +258,10 @@ precondition failure. When execution proceeds: -1. Runtime creates a processing hold against - `BalanceIdentifier::Evm(transaction.from())`. +1. Runtime resolves the EVM origin into a concrete payer + (`BalanceIdentifier::Account` for linked Casper accounts or + `BalanceIdentifier::Purse` for EVM-native identities) and creates a + processing hold against that payer. 2. Runtime enters the shared execution `match` through the `_ if is_evm` arm. 3. Runtime checks out a tracking copy at the current scratch state root. 4. Runtime builds an EVM block context from Casper block data: @@ -310,7 +317,7 @@ prioritize transactions based on transaction gas parameters. Accepted EIP-1559 transactions therefore pay `[evm].base_fee`; `max_fee_per_gas` is only a sender cap and must be high enough to cover the base fee. -The maximum fee is held from `BalanceIdentifier::Evm(from)`. After execution: +The maximum fee is held from the resolved EVM payer. After execution: - Successful execution consumes `gas_used * effective_gas_price`. - Failed/reverted/halted execution consumes the full held amount. @@ -318,7 +325,7 @@ The maximum fee is held from `BalanceIdentifier::Evm(from)`. After execution: - The final fee is processed through Casper `FeeHandling`. This keeps EVM transactions aligned with the same chain policy knobs used by -Deploy and V1/V2 transactions. The EVM gas price is converted to motes before +Deploy and native Transaction::V1 payloads. The EVM gas price is converted to motes before calling the balance/fee/refund machinery. In the shared accounting loop, EVM cost is already expressed as motes. Refund @@ -338,32 +345,45 @@ handling. EVM state is stored in Casper global state using typed keys and values: -- `Key::Evm(EvmAddr::Account(Address))` stores - `StoredValue::Evm(EvmValue::Account(Account))`. +- `Key::Evm(EvmAddr::Account(Address))` stores a minimal identity pointer as + `StoredValue::CLValue(Key::Account(AccountHash))` for linked Casper accounts + or `StoredValue::CLValue(Key::URef(URef))` for EVM-native accounts. +- `Key::Evm(EvmAddr::Nonce(Address))` stores `StoredValue::CLValue(u64)`. +- `Key::Evm(EvmAddr::CodeHash(Address))` stores + `StoredValue::CLValue(evm::Hash)`. - `Key::Evm(EvmAddr::ByteCode(Hash))` stores - `StoredValue::Evm(EvmValue::ByteCode(ByteCode))`. + `StoredValue::ByteCode(ByteCode)`. - `Key::Evm(EvmAddr::Storage(StorageAddr))` stores - `StoredValue::Evm(EvmValue::Storage(StorageValue))`. + `StoredValue::CLValue(U256)`. -An EVM account record contains: - -- nonce, -- code hash, -- main purse. +`StoredValue::Evm` is not part of the current layout. Balances are Casper purse balances. EVM balance reads and writes reconcile -through the account main purse and `Key::Balance(main_purse.addr())`. +through either the linked Casper account main purse or the EVM-native purse and +`Key::Balance(main_purse.addr())`. Genesis does not create EVM account records for Casper genesis accounts. Funding an EVM identity is explicit: a native Casper transfer can use a 20-byte `evm::Address` as its `target` argument when `[evm].enabled = true`. -If `Key::Evm(EvmAddr::Account(address))` already exists, the transfer credits that -account's main purse. If it does not exist, the transfer creates -`StoredValue::Evm(EvmValue::Account(Account::new(0, EMPTY_CODE_HASH, -evm::deterministic_purse(address))))`, initializes that deterministic purse -with a zero balance, then transfers the requested motes into it. Transfer -records keep the Casper transfer schema unchanged: `to` is `None`, and -`target` is the EVM account's backing purse. +If `Key::Evm(EvmAddr::Account(address))` already exists, the transfer credits +the linked account or purse. If it does not exist, the transfer writes an +EVM-native identity pointing to `evm::deterministic_purse(address)`, initializes +`Nonce(address)` to `0`, initializes `CodeHash(address)` to +`EMPTY_CODE_HASH`, initializes that deterministic purse with a zero balance, +then transfers the requested motes into it. Transfer records keep the Casper +transfer schema unchanged: `to` is `None`, and `target` is the EVM account's +backing purse. + +When a signed EVM transaction is executed for an address without a linked Casper +identity, contract runtime uses the transaction approval to recover the +secp256k1 public key before invoking the EVM executor. If the corresponding +`Key::Account(account_hash)` already exists and no established EVM-native +identity conflicts with it, `EvmAddr::Account(address)` is written as a bridge +to that Casper account. If no Casper account exists, contract runtime creates a +Casper account for that account hash backed by +`evm::deterministic_purse(address)`, then writes the bridge. EVM addresses +created by contract creation or normal runtime effects are not forced to have an +account hash; they remain EVM-native purse identities. ## Receipts @@ -595,23 +615,14 @@ eth_chainId: 0x435350ff eth_getTransactionCount: 0x0 ``` -### Fund EVM Identity - -Create and fund the EVM identity explicitly with a native Casper transfer: +### No EVM Prefund Required -```bash -casper-cli transaction transfer \ - --from devnet:user-1 \ - --to 0x24790C4849cCAE43c0c1749e2C5b8d00Cc63AB80 \ - --amount 10000 \ - --raw \ - --no-interactive -``` - -The transfer target is encoded as `byte-array[20]`. A successful transfer -creates `Key::Evm(EvmAddr::Account(0x24790c...))`, initializes its deterministic backing -purse, and credits it with the transferred motes. The EVM nonce remains `0x0` -until the first EVM transaction is executed. +Do not fund the 20-byte EVM address before deploying. The first EVM transaction +from `user-1` recovers the secp256k1 public key, resolves the existing Casper +account, and writes the `EvmAddr::Account` identity link during execution. A +native transfer to a missing 20-byte target is still supported, but that path +creates an EVM-native purse identity instead of demonstrating Casper account +linking. ### Deploy Counter diff --git a/execution_engine/src/runtime_context/mod.rs b/execution_engine/src/runtime_context/mod.rs index a00c15ddf4..301432debf 100644 --- a/execution_engine/src/runtime_context/mod.rs +++ b/execution_engine/src/runtime_context/mod.rs @@ -764,8 +764,7 @@ where | StoredValue::Message(_) | StoredValue::Prepayment(_) | StoredValue::EntryPoint(_) - | StoredValue::RawBytes(_) - | StoredValue::Evm(_) => Ok(()), + | StoredValue::RawBytes(_) => Ok(()), } } diff --git a/executor/evm/src/account_state.rs b/executor/evm/src/account_state.rs new file mode 100644 index 0000000000..56c92d1fde --- /dev/null +++ b/executor/evm/src/account_state.rs @@ -0,0 +1,338 @@ +//! Helpers for the EVM account records stored in Casper global state. +//! +//! This module is intentionally layout-focused. It translates split Casper +//! global-state records into the account shape revm needs, but it does not +//! recover transaction signers, create Casper accounts, or decide whether an EVM +//! address should be linked to a Casper account. Those policy decisions live in +//! contract runtime and transaction validation. + +use casper_storage::{tracking_copy::TrackingCopyError, TrackingCopy}; +use casper_types::{account::AccountHash, evm, CLValue, Key, StoredValue, URef}; + +/// Identity backing an EVM address. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum AccountIdentity { + /// The EVM address is linked to a Casper account. + /// + /// Balance reads and writes should go through that account's main purse. + Account(AccountHash), + /// The EVM address is backed only by an EVM purse. + /// + /// This is used for EVM-native externally owned accounts and contracts that + /// do not have a Casper account identity. + Purse(URef), +} + +/// EVM account metadata resolved into revm's account view. +/// +/// This is not a stored account type. It is the adapter result used to construct +/// revm's `AccountInfo` from independent identity, nonce, and code-hash records. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct AccountMetadata { + pub(crate) nonce: u64, + pub(crate) code_hash: evm::Hash, + pub(crate) main_purse: URef, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum AccountStorageError { + #[error(transparent)] + TrackingCopy(#[from] TrackingCopyError), + #[error("unexpected stored value for {key}: expected {expected}, found {found}")] + TypeMismatch { + key: Key, + expected: &'static str, + found: String, + }, + #[error("failed to decode {expected} at {key}: {error}")] + Decode { + key: Key, + expected: &'static str, + error: String, + }, + #[error("identity for {identity_key} points to missing account {account_key}")] + MissingAccount { identity_key: Key, account_key: Key }, +} + +pub(crate) fn read_account_metadata( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let identity = read_account_identity(tracking_copy, address)?; + let nonce = read_nonce(tracking_copy, address)?; + let code_hash = read_code_hash(tracking_copy, address)?; + + // No split records at all means revm should treat the account as absent. + // A partially present record still resolves to an account: missing nonce + // defaults to zero, missing code hash defaults to empty code, and missing + // identity falls back to the deterministic purse for this EVM address. + if identity.is_none() && nonce.is_none() && code_hash.is_none() { + return Ok(None); + } + + let main_purse = match identity { + Some(AccountIdentity::Account(account_hash)) => { + // A linked identity is only valid while the target Casper account + // exists. Treat a dangling bridge as state corruption rather than + // silently falling back to the deterministic EVM purse. + let identity_key = Key::Evm(evm::EvmAddr::Account(address)); + account_main_purse(tracking_copy, account_hash)?.ok_or( + AccountStorageError::MissingAccount { + identity_key, + account_key: Key::Account(account_hash), + }, + )? + } + Some(AccountIdentity::Purse(main_purse)) => main_purse, + // This fallback lets runtime-created contract addresses or partially + // initialized EVM-native records be read without forcing a Casper + // account link. + None => evm::deterministic_purse(address), + }; + + Ok(Some(AccountMetadata { + nonce: nonce.unwrap_or(0), + code_hash: code_hash.unwrap_or(evm::EMPTY_CODE_HASH), + main_purse, + })) +} + +pub(crate) fn read_account_identity( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let key = Key::Evm(evm::EvmAddr::Account(address)); + match tracking_copy.read(&key)? { + Some(StoredValue::CLValue(cl_value)) => { + let identity_key = cl_value_to_key(key, cl_value)?; + match identity_key { + // The only valid identity pointer variants are a Casper account + // hash or a purse. Nonce/code/storage are not stored here. + Key::Account(account_hash) => Ok(Some(AccountIdentity::Account(account_hash))), + Key::URef(uref) => Ok(Some(AccountIdentity::Purse(uref))), + other => Err(AccountStorageError::TypeMismatch { + key, + expected: "CLValue(Key::Account) or CLValue(Key::URef)", + found: other.type_string(), + }), + } + } + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key, + expected: "StoredValue::CLValue(Key)", + found: stored_value.type_name(), + }), + None => Ok(None), + } +} + +pub(crate) fn account_main_purse( + tracking_copy: &mut TrackingCopy, + account_hash: AccountHash, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let account_key = Key::Account(account_hash); + match tracking_copy.read(&account_key)? { + // Support both account representations because this helper is used by + // executor reads, runtime linking, and validation against the current + // global-state model. + Some(StoredValue::Account(account)) => Ok(Some(account.main_purse())), + Some(StoredValue::CLValue(cl_value)) => { + let key = cl_value_to_key(account_key, cl_value)?; + let Key::AddressableEntity(entity_addr) = key else { + return Err(AccountStorageError::TypeMismatch { + key: account_key, + expected: "CLValue(Key::AddressableEntity)", + found: key.type_string(), + }); + }; + let entity_key = Key::AddressableEntity(entity_addr); + match tracking_copy.read(&entity_key)? { + Some(StoredValue::AddressableEntity(entity)) => Ok(Some(entity.main_purse())), + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key: entity_key, + expected: "StoredValue::AddressableEntity", + found: stored_value.type_name(), + }), + None => Err(AccountStorageError::MissingAccount { + identity_key: account_key, + account_key: entity_key, + }), + } + } + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key: account_key, + expected: "StoredValue::Account or StoredValue::CLValue(Key::AddressableEntity)", + found: stored_value.type_name(), + }), + None => Ok(None), + } +} + +pub(crate) fn write_account_identity( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + identity_key: Key, +) -> Result<(), AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + // This helper only serializes the caller's chosen identity key. Runtime may + // write `Key::Account`; executor state application writes `Key::URef` only + // for EVM-native accounts and preserves existing `Key::Account` links. + let key = Key::Evm(evm::EvmAddr::Account(address)); + let cl_value = CLValue::from_t(identity_key).map_err(|error| AccountStorageError::Decode { + key, + expected: "Key", + error: error.to_string(), + })?; + tracking_copy.write(key, StoredValue::CLValue(cl_value)); + Ok(()) +} + +pub(crate) fn write_nonce( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + nonce: u64, +) -> Result<(), AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + // Nonce is deliberately independent from the identity pointer so linking an + // address to a Casper account does not move or rewrite EVM replay state. + let key = Key::Evm(evm::EvmAddr::Nonce(address)); + let cl_value = CLValue::from_t(nonce).map_err(|error| AccountStorageError::Decode { + key, + expected: "u64", + error: error.to_string(), + })?; + tracking_copy.write(key, StoredValue::CLValue(cl_value)); + Ok(()) +} + +pub(crate) fn write_code_hash( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + code_hash: evm::Hash, +) -> Result<(), AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + // Code hash is deliberately independent from the identity pointer so + // contracts can remain EVM-native even when EOAs may link to Casper + // accounts. + let key = Key::Evm(evm::EvmAddr::CodeHash(address)); + let cl_value = CLValue::from_t(code_hash).map_err(|error| AccountStorageError::Decode { + key, + expected: "evm::Hash", + error: error.to_string(), + })?; + tracking_copy.write(key, StoredValue::CLValue(cl_value)); + Ok(()) +} + +fn read_nonce( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let key = Key::Evm(evm::EvmAddr::Nonce(address)); + match tracking_copy.read(&key)? { + Some(StoredValue::CLValue(cl_value)) => { + cl_value + .into_t::() + .map(Some) + .map_err(|error| AccountStorageError::Decode { + key, + expected: "u64", + error: error.to_string(), + }) + } + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key, + expected: "StoredValue::CLValue(u64)", + found: stored_value.type_name(), + }), + None => Ok(None), + } +} + +fn read_code_hash( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let key = Key::Evm(evm::EvmAddr::CodeHash(address)); + match tracking_copy.read(&key)? { + Some(StoredValue::CLValue(cl_value)) => { + cl_value + .into_t::() + .map(Some) + .map_err(|error| AccountStorageError::Decode { + key, + expected: "evm::Hash", + error: error.to_string(), + }) + } + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key, + expected: "StoredValue::CLValue(evm::Hash)", + found: stored_value.type_name(), + }), + None => Ok(None), + } +} + +fn cl_value_to_key(key: Key, cl_value: CLValue) -> Result { + cl_value + .into_t::() + .map_err(|error| AccountStorageError::Decode { + key, + expected: "Key", + error: error.to_string(), + }) +} diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs index 7d27931a4e..ad88f289a7 100644 --- a/executor/evm/src/db.rs +++ b/executor/evm/src/db.rs @@ -11,7 +11,7 @@ use revm::{ state::{AccountInfo, Bytecode}, }; -use crate::{tx, BlockHashProvider, DbError}; +use crate::{account_state, tx, BlockHashProvider, DbError}; pub(crate) struct CasperDb<'a, R, B> where @@ -57,32 +57,25 @@ where fn basic(&mut self, address: Address) -> Result, Self::Error> { let address = tx::from_revm_address(address); - let key = Key::Evm(evm::EvmAddr::Account(address)); - match self.tracking_copy.read(&key)? { - Some(StoredValue::Evm(evm::EvmValue::Account(account))) => { - let balance = self.balance(account.main_purse())?; - Ok(Some(AccountInfo { - balance, - nonce: account.nonce(), - code_hash: tx::to_revm_hash(account.code_hash()), - account_id: None, - code: None, - })) - } - Some(stored_value) => Err(DbError::TypeMismatch { - key: Box::new(key), - expected: "StoredValue::Evm(Account)", - found: stored_value.type_name(), - }), - None => Ok(None), - } + let Some(account) = account_state::read_account_metadata(self.tracking_copy, address)? + else { + return Ok(None); + }; + let balance = self.balance(account.main_purse)?; + Ok(Some(AccountInfo { + balance, + nonce: account.nonce, + code_hash: tx::to_revm_hash(account.code_hash), + account_id: None, + code: None, + })) } fn code_by_hash(&mut self, code_hash: B256) -> Result { let code_hash = tx::from_revm_hash(code_hash); let key = Key::Evm(evm::EvmAddr::ByteCode(code_hash)); match self.tracking_copy.read(&key)? { - Some(StoredValue::Evm(evm::EvmValue::ByteCode(byte_code))) => { + Some(StoredValue::ByteCode(byte_code)) => { if !byte_code.kind().is_evm() { return Err(DbError::TypeMismatch { key: Box::new(key), @@ -94,7 +87,7 @@ where } Some(stored_value) => Err(DbError::TypeMismatch { key: Box::new(key), - expected: "StoredValue::Evm(ByteCode)", + expected: "StoredValue::ByteCode", found: stored_value.type_name(), }), None => Ok(Bytecode::default()), @@ -110,12 +103,17 @@ where let slot = tx::from_revm_storage_word(index); let key = Key::Evm(evm::EvmAddr::Storage(evm::StorageAddr::new(address, slot))); match self.tracking_copy.read(&key)? { - Some(StoredValue::Evm(evm::EvmValue::Storage(value))) => { - Ok(tx::to_revm_storage_word(value.value())) - } + Some(StoredValue::CLValue(cl_value)) => cl_value + .into_t::() + .map(tx::to_revm_storage_word) + .map_err(|error| DbError::ValueDecode { + key: Box::new(key), + expected: "U256", + error: error.to_string(), + }), Some(stored_value) => Err(DbError::TypeMismatch { key: Box::new(key), - expected: "StoredValue::Evm(Storage)", + expected: "StoredValue::CLValue(U256)", found: stored_value.type_name(), }), None => Ok(U256::ZERO), diff --git a/executor/evm/src/error.rs b/executor/evm/src/error.rs index 2936f5b91e..9e92fd06f2 100644 --- a/executor/evm/src/error.rs +++ b/executor/evm/src/error.rs @@ -3,7 +3,7 @@ use casper_storage::tracking_copy::TrackingCopyError; use casper_types::Key; -use crate::BlockHashProviderError; +use crate::{account_state::AccountStorageError, BlockHashProviderError}; /// Result type returned by the EVM executor. pub type Result = core::result::Result; @@ -55,6 +55,16 @@ pub enum DbError { /// Actual stored-value shape. found: String, }, + /// A Casper CLValue failed to decode as an expected EVM account field. + #[error("failed to decode {expected} at {key}: {error}")] + ValueDecode { + /// Global-state key that was read. + key: Box, + /// Expected decoded type. + expected: &'static str, + /// Decode error text. + error: String, + }, /// A Casper balance does not fit into EVM U256. #[error("Casper balance at {key} does not fit into EVM U256")] BalanceOverflow { @@ -79,4 +89,44 @@ pub enum DbError { }, } +impl From for DbError { + fn from(error: AccountStorageError) -> Self { + match error { + AccountStorageError::TrackingCopy(error) => DbError::TrackingCopy(error), + AccountStorageError::TypeMismatch { + key, + expected, + found, + } => DbError::TypeMismatch { + key: Box::new(key), + expected, + found, + }, + AccountStorageError::Decode { + key, + expected, + error, + } => DbError::ValueDecode { + key: Box::new(key), + expected, + error, + }, + AccountStorageError::MissingAccount { + identity_key, + account_key, + } => DbError::TypeMismatch { + key: Box::new(identity_key), + expected: "existing linked account", + found: format!("missing {account_key}"), + }, + } + } +} + +impl From for Error { + fn from(error: AccountStorageError) -> Self { + Error::State(error.to_string()) + } +} + impl revm::database_interface::DBErrorMarker for DbError {} diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs index 8add0f4fd2..f978ce71e1 100644 --- a/executor/evm/src/lib.rs +++ b/executor/evm/src/lib.rs @@ -3,6 +3,7 @@ //! This crate provides a small execution API over `TrackingCopy` and keeps //! `revm` details behind internal adapter modules. +mod account_state; mod block_hash; mod db; mod error; diff --git a/executor/evm/src/state.rs b/executor/evm/src/state.rs index bc6fb7e106..f925ba7c66 100644 --- a/executor/evm/src/state.rs +++ b/executor/evm/src/state.rs @@ -10,7 +10,7 @@ use revm::{ state::{Account, EvmState}, }; -use crate::{tx, Error}; +use crate::{account_state, tx, Error}; pub(crate) fn apply(tracking_copy: &mut TrackingCopy, state: EvmState) -> Result<(), Error> where @@ -67,9 +67,12 @@ where let account_key = Key::Evm(evm::EvmAddr::Account(address)); if account.is_selfdestructed() { - let main_purse = existing_main_purse(tracking_copy, &account_key)? - .unwrap_or_else(|| evm::deterministic_purse(address)); - prune_account(tracking_copy, address, account_key, main_purse)?; + // Selfdestruct removes EVM metadata and storage, but linked Casper + // accounts remain Casper accounts. Only EVM-native purse balances are + // pruned below. + let identity = account_state::read_account_identity(tracking_copy, address)?; + let main_purse = existing_main_purse(tracking_copy, address, identity)?; + prune_account(tracking_copy, address, account_key, identity, main_purse)?; return Ok(()); } @@ -80,26 +83,23 @@ where Key::Evm(evm::EvmAddr::ByteCode(tx::from_revm_hash( account.info.code_hash, ))), - StoredValue::Evm(evm::EvmValue::ByteCode(ByteCode::new( - ByteCodeKind::EvmPrague, - bytes.to_vec(), - ))), + StoredValue::ByteCode(ByteCode::new(ByteCodeKind::EvmPrague, bytes.to_vec())), ); } } - let main_purse = existing_main_purse(tracking_copy, &account_key)? - .unwrap_or_else(|| evm::deterministic_purse(address)); + let identity = account_state::read_account_identity(tracking_copy, address)?; + let main_purse = existing_main_purse(tracking_copy, address, identity)?; let code_hash = tx::from_revm_hash(account.info.code_hash); - tracking_copy.write( - account_key, - StoredValue::Evm(evm::EvmValue::Account(evm::Account::new( - account.info.nonce, - code_hash, - main_purse, - ))), - ); + // Executor never creates a `Key::Account` bridge. Runtime applies that + // policy before execution. For accounts without such a bridge, ensure revm + // state changes have an EVM-native purse identity to attach balances to. + if !matches!(identity, Some(account_state::AccountIdentity::Account(_))) { + account_state::write_account_identity(tracking_copy, address, Key::URef(main_purse))?; + } + account_state::write_nonce(tracking_copy, address, account.info.nonce)?; + account_state::write_code_hash(tracking_copy, address, code_hash)?; write_balance(tracking_copy, main_purse, account.info.balance)?; for (slot, value) in account.changed_storage_slots() { @@ -107,15 +107,14 @@ where address, tx::from_revm_storage_word(*slot), ))); + // Storage slots are plain CLValue(U256) under their split storage key. + // Zero writes prune the slot, matching Ethereum's empty-storage model. if value.present_value.is_zero() { tracking_copy.prune(key); } else { - tracking_copy.write( - key, - StoredValue::Evm(evm::EvmValue::Storage(evm::StorageValue::new( - tx::from_revm_storage_word(value.present_value), - ))), - ); + let storage_value = CLValue::from_t(tx::from_revm_storage_word(value.present_value)) + .map_err(|error| Error::State(error.to_string()))?; + tracking_copy.write(key, StoredValue::CLValue(storage_value)); } } @@ -126,6 +125,7 @@ fn prune_account( tracking_copy: &mut TrackingCopy, address: evm::Address, account_key: Key, + identity: Option, main_purse: casper_types::URef, ) -> Result<(), Error> where @@ -137,28 +137,35 @@ where for key in storage_keys { tracking_copy.prune(key); } - tracking_copy.prune(Key::Balance(main_purse.addr())); + if !matches!(identity, Some(account_state::AccountIdentity::Account(_))) { + tracking_copy.prune(Key::Balance(main_purse.addr())); + } tracking_copy.prune(account_key); + tracking_copy.prune(Key::Evm(evm::EvmAddr::Nonce(address))); + tracking_copy.prune(Key::Evm(evm::EvmAddr::CodeHash(address))); Ok(()) } fn existing_main_purse( tracking_copy: &mut TrackingCopy, - account_key: &Key, -) -> Result, Error> + address: evm::Address, + identity: Option, +) -> Result where R: StateReader, { - match tracking_copy - .read(account_key) - .map_err(|error| Error::State(error.to_string()))? - { - Some(StoredValue::Evm(evm::EvmValue::Account(account))) => Ok(Some(account.main_purse())), - Some(stored_value) => Err(Error::State(format!( - "unexpected stored value for {account_key}: expected StoredValue::Evm(Account), found {}", - stored_value.type_name() - ))), - None => Ok(None), + match identity { + // Linked accounts use their Casper main purse. If the linked account is + // unexpectedly missing, fall back to the deterministic purse so pruning + // stays local to EVM state rather than deleting unrelated balances. + Some(account_state::AccountIdentity::Account(account_hash)) => Ok( + account_state::account_main_purse(tracking_copy, account_hash)? + .unwrap_or_else(|| evm::deterministic_purse(address)), + ), + // EVM-native accounts and contracts keep balances under the identity + // purse chosen by runtime/native-transfer initialization. + Some(account_state::AccountIdentity::Purse(main_purse)) => Ok(main_purse), + None => Ok(evm::deterministic_purse(address)), } } diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 58eff9abdb..c77efaf5a2 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -17,9 +17,10 @@ use casper_storage::{ TrackingCopy, }; use casper_types::{ - evm, BlockHash, CLValue, ChainspecRegistry, Digest, GenesisAccount, GenesisConfig, - HoldBalanceHandling, Key, Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, - StoredValue, SystemConfig, Timestamp, WasmConfig, U256 as CasperU256, U512, + contracts::NamedKeys, evm, AccessRights, Account, BlockHash, CLValue, ChainspecRegistry, + Digest, GenesisAccount, GenesisConfig, HoldBalanceHandling, Key, Motes, ProtocolVersion, + PublicKey, SecretKey, StorageCosts, StoredValue, SystemConfig, Timestamp, URef, WasmConfig, + U256 as CasperU256, U512, }; use revm::bytecode::opcode; @@ -324,7 +325,7 @@ fn read_storage>( )))) .expect("storage read should not fail") { - Some(StoredValue::Evm(evm::EvmValue::Storage(value))) => Some(value.value()), + Some(StoredValue::CLValue(value)) => value.into_t::().ok(), Some(other) => panic!("unexpected storage value: {other:?}"), None => None, } @@ -338,7 +339,18 @@ fn read_balance>( .read(&Key::Evm(evm::EvmAddr::Account(address))) .expect("account read should not fail") { - Some(StoredValue::Evm(evm::EvmValue::Account(account))) => account.main_purse(), + Some(StoredValue::CLValue(value)) => match value.into_t::().unwrap() { + Key::URef(uref) => uref, + Key::Account(account_hash) => match tracking_copy + .read(&Key::Account(account_hash)) + .expect("linked account read should not fail") + { + Some(StoredValue::Account(account)) => account.main_purse(), + Some(other) => panic!("unexpected linked account value: {other:?}"), + None => return U512::zero(), + }, + other => panic!("unexpected EVM account identity key: {other:?}"), + }, Some(other) => panic!("unexpected account value: {other:?}"), None => return U512::zero(), }; @@ -360,11 +372,15 @@ fn seed_evm_balance>( let main_purse = evm::deterministic_purse(address); tracking_copy.write( Key::Evm(evm::EvmAddr::Account(address)), - StoredValue::Evm(evm::EvmValue::Account(evm::Account::new( - 0, - EMPTY_CODE_HASH, - main_purse, - ))), + StoredValue::CLValue(CLValue::from_t(Key::URef(main_purse)).unwrap()), + ); + tracking_copy.write( + Key::Evm(evm::EvmAddr::Nonce(address)), + StoredValue::CLValue(CLValue::from_t(0u64).unwrap()), + ); + tracking_copy.write( + Key::Evm(evm::EvmAddr::CodeHash(address)), + StoredValue::CLValue(CLValue::from_t(EMPTY_CODE_HASH).unwrap()), ); tracking_copy.write( Key::Balance(main_purse.addr()), @@ -372,6 +388,20 @@ fn seed_evm_balance>( ); } +fn read_evm_nonce>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> u64 { + match tracking_copy + .read(&Key::Evm(evm::EvmAddr::Nonce(address))) + .expect("nonce read should not fail") + { + Some(StoredValue::CLValue(value)) => value.into_t::().unwrap(), + Some(other) => panic!("unexpected nonce value: {other:?}"), + None => 0, + } +} + #[test] fn blockhash_uses_supplied_provider() { let executor = executor(evm::EvmSpec::Prague); @@ -785,6 +815,103 @@ fn signed_transactions_require_configured_chain_id() { )); } +#[test] +fn signed_transaction_sender_uses_linked_casper_account_identity() { + let executor = executor(evm::EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let transaction = legacy_transaction(Some(7)); + let signer = transaction + .signer() + .expect("transaction should have a signer"); + let account_hash = signer.to_account_hash(); + let main_purse = URef::new([9; 32], AccessRights::READ_ADD_WRITE); + let initial_balance = U512::from(1_000_000u64); + + tracking_copy.write( + Key::Account(account_hash), + StoredValue::Account(Account::create(account_hash, NamedKeys::new(), main_purse)), + ); + tracking_copy.write( + Key::Balance(main_purse.addr()), + StoredValue::CLValue(CLValue::from_t(initial_balance).unwrap()), + ); + tracking_copy.write( + Key::Evm(evm::EvmAddr::Account(transaction.from())), + StoredValue::CLValue(CLValue::from_t(Key::Account(account_hash)).unwrap()), + ); + + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(transaction.clone()), + }; + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("EVM execution should succeed"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(read_evm_nonce(&mut tracking_copy, transaction.from()), 1); + assert_eq!( + read_balance(&mut tracking_copy, transaction.from()), + initial_balance + ); + match tracking_copy + .read(&Key::Evm(evm::EvmAddr::Account(transaction.from()))) + .expect("identity read should not fail") + { + Some(StoredValue::CLValue(value)) => { + assert_eq!(value.into_t::().unwrap(), Key::Account(account_hash)); + } + other => panic!("unexpected EVM identity value: {other:?}"), + } +} + +#[test] +fn signed_transaction_sender_keeps_evm_native_identity() { + let executor = executor(evm::EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let transaction = legacy_transaction(Some(7)); + let signer = transaction + .signer() + .expect("transaction should have a signer"); + let account_hash = signer.to_account_hash(); + let initial_balance = U512::from(1_000_000u64); + + seed_evm_balance(&mut tracking_copy, transaction.from(), initial_balance); + + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(transaction.clone()), + }; + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("EVM execution should succeed"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(read_evm_nonce(&mut tracking_copy, transaction.from()), 1); + assert_eq!( + read_balance(&mut tracking_copy, transaction.from()), + initial_balance + ); + assert_eq!( + tracking_copy + .read(&Key::Account(account_hash)) + .expect("account read should not fail"), + None + ); + match tracking_copy + .read(&Key::Evm(evm::EvmAddr::Account(transaction.from()))) + .expect("identity read should not fail") + { + Some(StoredValue::CLValue(value)) => { + assert_eq!( + value.into_t::().unwrap(), + Key::URef(evm::deterministic_purse(transaction.from())) + ); + } + other => panic!("unexpected EVM identity value: {other:?}"), + } +} + #[test] fn checked_calls_enforce_transaction_validation() { let executor = executor(evm::EvmSpec::Prague); diff --git a/node/src/components/contract_runtime/error.rs b/node/src/components/contract_runtime/error.rs index 09af7a8736..5fb5b6618b 100644 --- a/node/src/components/contract_runtime/error.rs +++ b/node/src/components/contract_runtime/error.rs @@ -8,12 +8,13 @@ use thiserror::Error; use casper_execution_engine::engine_state::Error as EngineStateError; use casper_storage::{ data_access_layer::{ - forced_undelegate::ForcedUndelegateError, BlockRewardsError, FeeError, StepError, + forced_undelegate::ForcedUndelegateError, BalanceIdentifierFromInitiatorError, + BlockRewardsError, FeeError, StepError, }, global_state::error::Error as GlobalStateError, tracking_copy::TrackingCopyError, }; -use casper_types::{bytesrepr, CLValueError, Digest, EraId, PublicKey, U512}; +use casper_types::{bytesrepr, evm, CLValueError, Digest, EraId, PublicKey, U512}; use crate::{ components::contract_runtime::ExecutionPreState, @@ -176,9 +177,25 @@ pub enum BlockExecutionError { /// Invalid transaction variant. #[error("Invalid transaction variant")] InvalidTransactionVariant, + /// EVM initiators are only valid for EVM transaction variants. + #[error("EVM initiator address {address:?} is only valid for EVM transactions")] + EvmInitiatorForNonEvmTransaction { + /// The EVM initiator address found on a non-EVM transaction. + address: evm::Address, + }, /// Invalid transaction arguments. #[error("Invalid transaction arguments")] InvalidTransactionArgs, #[error("Data Access Layer conflicts with chainspec setting: {0}")] InvalidAESetting(bool), } + +impl From for BlockExecutionError { + fn from(error: BalanceIdentifierFromInitiatorError) -> Self { + match error { + BalanceIdentifierFromInitiatorError::EvmAddress(address) => { + BlockExecutionError::EvmInitiatorForNonEvmTransaction { address } + } + } + } +} diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 14637fe88d..11850be845 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -35,9 +35,13 @@ use casper_storage::{ StateProvider, StateReader, }, system::runtime_native::Config as NativeRuntimeConfig, + tracking_copy::{TrackingCopyEntityExt, TrackingCopyError}, + TrackingCopy, }; use casper_types::{ + account::{Account, AccountHash}, bytesrepr::{self, ToBytes, U32_SERIALIZED_LENGTH}, + contracts::NamedKeys, evm::{ Address as EvmAddress, HaltReason as EvmHaltReason, Receipt as EvmReceipt, ReceiptStatus as EvmReceiptStatus, @@ -46,8 +50,8 @@ use casper_types::{ system::handle_payment::ARG_AMOUNT, BlockHash, BlockHeader, BlockTime, BlockV2, CLValue, Chainspec, ChecksumRegistry, Digest, EntityAddr, EraEndV2, EraId, FeeHandling, Gas, InvalidTransaction, InvalidTransactionV1, Key, - ProtocolVersion, PublicKey, RefundHandling, TimeDiff, Transaction, TransactionEntryPoint, - AUCTION_LANE_ID, MINT_LANE_ID, U512, + ProtocolVersion, PublicKey, RefundHandling, StoredValue, TimeDiff, Transaction, + TransactionEntryPoint, AUCTION_LANE_ID, MINT_LANE_ID, U512, }; use super::{ @@ -83,6 +87,304 @@ fn evm_precondition_receipt(effective_gas_price: u128) -> EvmReceipt { } } +#[derive(Clone, Debug)] +struct EvmOriginResolution { + // Concrete payer selected before payment checks. This is deliberately a + // Casper balance identifier, not an EVM-specific balance mode, so the rest + // of block execution can use the normal hold/refund/fee machinery. + balance_identifier: BalanceIdentifier, + // State mutation to perform later, inside the same tracking copy as EVM + // execution. Origin resolution itself is read-only so a rejected + // transaction does not create accounts or links as a side effect. + identity_plan: EvmIdentityPlan, +} + +/// Deferred write needed to make an EVM sender's identity explicit in global state. +/// +/// The runtime makes this decision because it has both pieces of context the +/// executor should not need: the recovered transaction signer and the Casper +/// account view at the current state root. +#[derive(Clone, Copy, Debug)] +enum EvmIdentityPlan { + /// No identity write is needed. Either the identity already exists, or the + /// address must remain EVM-native. + None, + /// The EVM address has no identity pointer yet, but the recovered signer + /// already has a Casper account. Link the address to that account hash. + LinkExisting { + address: EvmAddress, + account_hash: AccountHash, + }, + /// Neither an identity pointer nor a Casper account exists for the + /// recovered signer. Create the Casper account and then link the EVM + /// address to it. + CreateAccount { + address: EvmAddress, + account_hash: AccountHash, + main_purse: casper_types::URef, + }, +} + +/// Resolves the payer and any deferred identity write for a signed EVM transaction. +/// +/// This function only reads state. That matters because it runs before payment +/// preconditions are known to pass. If execution is later allowed, the returned +/// [`EvmIdentityPlan`] is applied in the tracking copy used for EVM execution. +fn resolve_evm_origin( + scratch_state: &ScratchGlobalState, + state_root_hash: Digest, + protocol_version: ProtocolVersion, + transaction: &casper_types::evm::Transaction, +) -> Result { + let address = transaction.from(); + // The signer gives us a Casper `AccountHash` preimage from the secp256k1 + // public key. That account hash is not derivable from the 20-byte EVM + // address alone, so identity linking must happen while the signed + // transaction is available. + let signer = transaction + .signer() + .map_err(|error| BlockExecutionError::TransactionConversion(error.to_string()))?; + let account_hash = signer.to_account_hash(); + // Native EVM identities use a deterministic purse derived from the EVM + // address. Linked Casper accounts use the account's existing main purse + // instead, so the same key pair can spend the same funds from Casper and + // Ethereum-style transaction paths. + let deterministic_purse = casper_types::evm::deterministic_purse(address); + let mut tracking_copy = scratch_state + .tracking_copy(state_root_hash)? + .ok_or(BlockExecutionError::RootNotFound(state_root_hash))?; + + // `EvmAddr::Account` is now only an identity pointer. It is either + // `Key::Account` for a linked Casper account or `Key::URef` for an + // EVM-native purse identity. + let identity_key = Key::Evm(casper_types::evm::EvmAddr::Account(address)); + match tracking_copy + .read(&identity_key) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))? + { + Some(StoredValue::CLValue(cl_value)) => { + let key = cl_value + .into_t::() + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + match key { + // Existing bridge records are authoritative. Once an EVM + // address is linked, the payer is the linked Casper account's + // main purse. + Key::Account(account_hash) => Ok(EvmOriginResolution { + balance_identifier: BalanceIdentifier::Account(account_hash), + identity_plan: EvmIdentityPlan::None, + }), + // Existing EVM-native identities keep paying from their stored + // purse. We may still plan an upgrade to a Casper link, but + // only when doing so cannot steal a contract identity or move + // balances between distinct purses. + Key::URef(purse) => { + let identity_plan = resolve_evm_native_identity_plan( + &mut tracking_copy, + protocol_version, + address, + account_hash, + purse, + deterministic_purse, + )?; + Ok(EvmOriginResolution { + balance_identifier: BalanceIdentifier::Purse(purse), + identity_plan, + }) + } + other => Err(BlockExecutionError::PaymentError(format!( + "invalid EVM account identity key: {other}" + ))), + } + } + Some(stored_value) => Err(BlockExecutionError::PaymentError(format!( + "unexpected stored value for {identity_key}: expected StoredValue::CLValue(Key), found {}", + stored_value.type_name() + ))), + None => { + // No identity pointer plus non-empty EVM code means this address is + // already a contract/runtime-created EVM account. Contracts do not + // have a signing key, so they must remain EVM-native. + if evm_account_has_code(&mut tracking_copy, address)? { + return Ok(EvmOriginResolution { + balance_identifier: BalanceIdentifier::Purse(deterministic_purse), + identity_plan: EvmIdentityPlan::None, + }); + } + match account_main_purse(&mut tracking_copy, protocol_version, account_hash)? { + // A Casper account exists for the recovered signer, but the EVM + // address has not been seen before. Use the account for payment + // immediately and write the bridge only if execution proceeds. + Some(_) => Ok(EvmOriginResolution { + balance_identifier: BalanceIdentifier::Account(account_hash), + identity_plan: EvmIdentityPlan::LinkExisting { + address, + account_hash, + }, + }), + // First use of this signing pair on both sides. Runtime will + // create a Casper account whose main purse is the deterministic + // EVM purse, then write the bridge record. + None => Ok(EvmOriginResolution { + balance_identifier: BalanceIdentifier::Purse(deterministic_purse), + identity_plan: EvmIdentityPlan::CreateAccount { + address, + account_hash, + main_purse: deterministic_purse, + }, + }), + } + } + } +} + +fn resolve_evm_native_identity_plan( + tracking_copy: &mut TrackingCopy, + protocol_version: ProtocolVersion, + address: EvmAddress, + account_hash: AccountHash, + purse: casper_types::URef, + deterministic_purse: casper_types::URef, +) -> Result +where + R: StateReader, +{ + // Do not overwrite contract identities, and do not turn an arbitrary purse + // identity into a Casper account link. The only EVM-native identity that is + // safe to link is the deterministic purse for this address. + if evm_account_has_code(tracking_copy, address)? || purse.addr() != deterministic_purse.addr() { + return Ok(EvmIdentityPlan::None); + } + + match account_main_purse(tracking_copy, protocol_version, account_hash)? { + // If the recovered Casper account already uses the same deterministic + // purse, replacing the pointer with `Key::Account` preserves the balance + // location and lets Casper-native flows see the account identity. + Some(main_purse) if main_purse.addr() == purse.addr() => { + Ok(EvmIdentityPlan::LinkExisting { + address, + account_hash, + }) + } + // A Casper account exists, but its main purse differs from the existing + // EVM-native purse. Keep the EVM-native identity to avoid moving funds + // or changing ownership semantics behind the user's back. + Some(_) => Ok(EvmIdentityPlan::None), + // No Casper account exists yet, so creating one backed by the existing + // deterministic purse preserves balances while giving the signer a + // Casper account identity. + None => Ok(EvmIdentityPlan::CreateAccount { + address, + account_hash, + main_purse: purse, + }), + } +} + +fn evm_account_has_code( + tracking_copy: &mut TrackingCopy, + address: EvmAddress, +) -> Result +where + R: StateReader, +{ + // Code hash is the cheap contract/EOA discriminator for an EVM address. A + // non-empty code hash means the address is not a user-controlled signing + // identity, so runtime must not create or link a Casper account for it. + let key = Key::Evm(casper_types::evm::EvmAddr::CodeHash(address)); + match tracking_copy + .read(&key) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))? + { + Some(StoredValue::CLValue(cl_value)) => { + let code_hash = cl_value + .into_t::() + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + Ok(code_hash != casper_types::evm::EMPTY_CODE_HASH) + } + Some(stored_value) => Err(BlockExecutionError::PaymentError(format!( + "unexpected stored value for {key}: expected StoredValue::CLValue(evm::Hash), found {}", + stored_value.type_name() + ))), + None => Ok(false), + } +} + +fn account_main_purse( + tracking_copy: &mut TrackingCopy, + protocol_version: ProtocolVersion, + account_hash: AccountHash, +) -> Result, BlockExecutionError> +where + R: StateReader, +{ + // Runtime footprints cover both legacy `StoredValue::Account` accounts and + // addressable-entity-backed accounts, so this is the authoritative account + // existence check for identity linking. + match tracking_copy.runtime_footprint_by_account_hash(protocol_version, account_hash) { + Ok((_, entity)) => entity + .main_purse() + .map(Some) + .ok_or_else(|| BlockExecutionError::PaymentError("missing account main purse".into())), + Err(TrackingCopyError::KeyNotFound(_)) => Ok(None), + Err(error) => Err(BlockExecutionError::PaymentError(error.to_string())), + } +} + +fn apply_evm_identity_plan( + tracking_copy: &mut TrackingCopy, + protocol_version: ProtocolVersion, + plan: EvmIdentityPlan, +) -> Result<(), BlockExecutionError> +where + R: StateReader, +{ + // Identity writes are intentionally delayed until after payment + // preconditions pass. They are applied to the same tracking copy as EVM + // execution so the identity record and nonce/code/storage updates commit or + // discard together. + match plan { + EvmIdentityPlan::None => Ok(()), + EvmIdentityPlan::LinkExisting { + address, + account_hash, + } => write_evm_identity(tracking_copy, address, Key::Account(account_hash)), + EvmIdentityPlan::CreateAccount { + address, + account_hash, + main_purse, + } => { + // Another transaction in the same block may have already created + // the account through this scratch state. Avoid recreating it, but + // still write the EVM identity pointer below. + if account_main_purse(tracking_copy, protocol_version, account_hash)?.is_none() { + let account = Account::create(account_hash, NamedKeys::new(), main_purse); + tracking_copy + .create_addressable_entity_from_account(account, protocol_version) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + } + write_evm_identity(tracking_copy, address, Key::Account(account_hash)) + } + } +} + +fn write_evm_identity( + tracking_copy: &mut TrackingCopy, + address: EvmAddress, + identity: Key, +) -> Result<(), BlockExecutionError> +where + R: StateReader, +{ + // Keep the bridge record minimal: a CLValue containing the identity `Key`. + // Nonce, code hash, bytecode, and storage live under their own EVM keys. + let key = Key::Evm(casper_types::evm::EvmAddr::Account(address)); + let cl_value = CLValue::from_t(identity) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + tracking_copy.write(key, StoredValue::CLValue(cl_value)); + Ok(()) +} + /// Executes a finalized block. #[allow(clippy::too_many_arguments)] pub fn execute_finalized_block( @@ -255,6 +557,12 @@ pub fn execute_finalized_block( let transaction_hash = stored_transaction.hash(); let authorization_keys = stored_transaction.authorization_keys(); + if !is_evm { + if let Some(address) = initiator_addr.evm_address() { + return Err(BlockExecutionError::EvmInitiatorForNonEvmTransaction { address }); + } + } + /* we solve for halting state using a `gas limit` which is the maximum amount of computation we will allow a given transaction to consume. the transaction itself @@ -348,6 +656,17 @@ pub fn execute_finalized_block( let is_custom_payment = !is_standard_payment && transaction.is_custom_payment(); let is_v1_wasm = transaction.is_v1_wasm(); let is_v2_wasm = transaction.is_v2_wasm(); + let evm_origin = if let Some(evm_transaction) = evm_transaction { + Some(resolve_evm_origin( + &scratch_state, + state_root_hash, + protocol_version, + evm_transaction, + )?) + } else { + None + }; + let refund_purse_active = is_custom_payment; if refund_purse_active { // if custom payment before doing any processing, initialize the initiator's main purse @@ -360,7 +679,7 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleRefundMode::SetRefundPurse { - target: Box::new(initiator_addr.clone().into()), + target: Box::new(initiator_addr.clone().try_into()?), }, ); let handle_refund_result = scratch_state.handle_refund(handle_refund_request); @@ -382,7 +701,10 @@ pub fn execute_finalized_block( let initial_balance_result = scratch_state.balance(BalanceRequest::new( state_root_hash, protocol_version, - initiator_addr.clone().into(), + evm_origin.as_ref().map_or_else( + || initiator_addr.clone().try_into(), + |origin| Ok(origin.balance_identifier.clone()), + )?, balance_handling, ProofHandling::NoProofs, )); @@ -412,15 +734,16 @@ pub fn execute_finalized_block( } let mut balance_identifier = { - if let Some(evm_transaction) = evm_transaction { + if let Some(origin) = evm_origin.as_ref() { // EVM transactions intentionally do not participate in Casper custom payment // or refund-purse setup. Ethereum payloads carry a gas limit and gas price fields, // but this chain still owns the fee/refund policy through the same chainspec - // settings used by Deploy and V1/V2 transactions. The EVM sender's main purse is - // therefore the payer for the processing hold, refund calculation, and final fee - // handling, while revm runs with gas fee charging disabled and only mutates EVM - // nonce, code, storage, logs, creates, and value transfers. - BalanceIdentifier::Evm(evm_transaction.from()) + // settings used by Deploy and native Transaction::V1 payloads. The EVM sender's + // main purse is therefore the payer for the processing hold, refund + // calculation, and final fee handling, while revm runs with gas fee + // charging disabled and only mutates EVM nonce, code, storage, + // logs, creates, and value transfers. + origin.balance_identifier.clone() } else if is_standard_payment { let contract_might_pay = addressable_entity_enabled && transaction.is_contract_by_hash_invocation(); @@ -431,7 +754,7 @@ pub fn execute_finalized_block( Ok(None) => { // the initiating account pays using its main purse trace!(%transaction_hash, "direct invocation with account payment"); - initiator_addr.clone().into() + initiator_addr.clone().try_into()? } Err(err) => { trace!(%transaction_hash, "failed to resolve contract self payment"); @@ -447,13 +770,13 @@ pub fn execute_finalized_block( } else { // the initiating account pays using its main purse trace!(%transaction_hash, "account session with standard payment"); - initiator_addr.clone().into() + initiator_addr.clone().try_into()? } } else if is_v2_wasm { // vm2 does not support custom payment, so it MUST be standard payment // if transaction runtime is v2 then the initiating account will pay using // the refund purse - initiator_addr.clone().into() + initiator_addr.clone().try_into()? } else if is_custom_payment { // this is the custom payment flow // the initiating account will pay, but wants to do so with a different purse or @@ -501,7 +824,7 @@ pub fn execute_finalized_block( authorization_keys.clone(), BalanceIdentifierTransferArgs::new( None, - initiator_addr.clone().into(), + initiator_addr.clone().try_into()?, BalanceIdentifier::Payment, baseline_motes_amount, None, @@ -723,6 +1046,18 @@ pub fn execute_finalized_block( let mut tracking_copy = scratch_state .tracking_copy(state_root_hash)? .ok_or(BlockExecutionError::RootNotFound(state_root_hash))?; + if let Some(origin) = evm_origin.as_ref() { + // Apply the deferred bridge/account creation only now, + // after balance preconditions have allowed execution. + // This keeps rejected EVM transactions from mutating + // identity state and makes the identity write atomic + // with the revm state transition below. + apply_evm_identity_plan( + &mut tracking_copy, + protocol_version, + origin.identity_plan, + )?; + } let outcome = EvmExecutor::new(chainspec.evm_config) .execute_with_block_hash_provider( &mut tracking_copy, diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index 27fb4c96a3..0522feea99 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -40,19 +40,75 @@ use crate::{ pub(crate) use config::Config; pub(crate) use error::{DeployParameterFailure, Error, ParameterFailure}; -pub(crate) use event::{Event, EventMetadata}; +pub(crate) use event::{ + Event, EventMetadata, EvmAccountLookup, EvmBalanceSource, EvmCodeHashLookup, EvmNonceLookup, +}; const COMPONENT_NAME: &str = "transaction_acceptor"; const ARG_TARGET: &str = "target"; -fn evm_account_from_query_result(query_result: QueryResult) -> Option { +fn evm_account_lookup_from_query_result(query_result: QueryResult) -> EvmAccountLookup { match query_result { QueryResult::Success { value, .. } => match *value { - StoredValue::Evm(evm::EvmValue::Account(account)) => Some(account), - _ => None, + StoredValue::CLValue(cl_value) => match cl_value.into_t::() { + Ok(Key::Account(account_hash)) => EvmAccountLookup::Account(account_hash), + Ok(Key::URef(uref)) => EvmAccountLookup::Purse(uref), + Ok(other) => { + EvmAccountLookup::Invalid(format!("invalid EVM account identity key: {other}")) + } + Err(error) => EvmAccountLookup::Invalid(format!( + "failed to decode EVM account identity key: {error}" + )), + }, + stored_value => EvmAccountLookup::Invalid(format!( + "expected StoredValue::CLValue(Key), found {}", + stored_value.type_name() + )), }, - QueryResult::RootNotFound | QueryResult::ValueNotFound(_) | QueryResult::Failure(_) => None, + QueryResult::RootNotFound | QueryResult::ValueNotFound(_) | QueryResult::Failure(_) => { + EvmAccountLookup::Missing + } + } +} + +fn evm_nonce_from_query_result(query_result: QueryResult) -> EvmNonceLookup { + match query_result { + QueryResult::Success { value, .. } => match *value { + StoredValue::CLValue(cl_value) => match cl_value.into_t::() { + Ok(nonce) => EvmNonceLookup::Value(nonce), + Err(error) => { + EvmNonceLookup::Invalid(format!("failed to decode EVM nonce: {error}")) + } + }, + stored_value => EvmNonceLookup::Invalid(format!( + "expected StoredValue::CLValue(u64), found {}", + stored_value.type_name() + )), + }, + QueryResult::RootNotFound | QueryResult::ValueNotFound(_) | QueryResult::Failure(_) => { + EvmNonceLookup::Missing + } + } +} + +fn evm_code_hash_from_query_result(query_result: QueryResult) -> EvmCodeHashLookup { + match query_result { + QueryResult::Success { value, .. } => match *value { + StoredValue::CLValue(cl_value) => match cl_value.into_t::() { + Ok(code_hash) => EvmCodeHashLookup::Value(code_hash), + Err(error) => { + EvmCodeHashLookup::Invalid(format!("failed to decode EVM code hash: {error}")) + } + }, + stored_value => EvmCodeHashLookup::Invalid(format!( + "expected StoredValue::CLValue(evm::Hash), found {}", + stored_value.type_name() + )), + }, + QueryResult::RootNotFound | QueryResult::ValueNotFound(_) | QueryResult::Failure(_) => { + EvmCodeHashLookup::Missing + } } } @@ -224,6 +280,10 @@ impl TransactionAcceptor { }; if let Some(evm_transaction) = event_metadata.meta_transaction.as_evm() { + // EVM senders are validated from the EVM identity record first. A + // Casper account lookup would be wrong here because an EVM address + // may be either linked to a Casper account or backed by an + // EVM-native purse. let query_request = QueryRequest::new( *block_header.state_root_hash(), Key::Evm(evm::EvmAddr::Account(evm_transaction.from())), @@ -234,7 +294,7 @@ impl TransactionAcceptor { .event(move |query_result| Event::GetEvmAccountResult { event_metadata, block_header, - maybe_account: evm_account_from_query_result(query_result), + account: evm_account_lookup_from_query_result(query_result), }); } @@ -261,26 +321,264 @@ impl TransactionAcceptor { effect_builder: EffectBuilder, event_metadata: Box, block_header: Box, - maybe_account: Option, + account: EvmAccountLookup, ) -> Effects { - let account = match maybe_account { - Some(account) => account, - None => { - let initiator_addr = event_metadata.transaction.initiator_addr(); - let error = Error::parameter_failure( - &block_header, - ParameterFailure::UnknownBalance { initiator_addr }, + let evm_transaction = event_metadata + .meta_transaction + .as_evm() + .expect("EVM account lookup should only be used for EVM transactions"); + + match account { + // Existing identity pointers select the balance source directly. + // The nonce remains under `EvmAddr::Nonce`, so it is queried after + // identity resolution regardless of whether the payer is a Casper + // account or an EVM-native purse. + EvmAccountLookup::Account(account_hash) => self.query_evm_nonce( + effect_builder, + event_metadata, + block_header, + EvmBalanceSource::Account(account_hash), + ), + EvmAccountLookup::Purse(uref) => self.query_evm_nonce( + effect_builder, + event_metadata, + block_header, + EvmBalanceSource::Purse(uref), + ), + EvmAccountLookup::Invalid(error_message) => { + let error = Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::Decode(error_message), + )); + self.reject_transaction(effect_builder, *event_metadata, error) + } + EvmAccountLookup::Missing => { + // A missing identity pointer does not necessarily mean all EVM + // metadata is missing. Runtime still checks split nonce and + // code-hash records before deciding whether the address can be + // linked to a Casper account or must remain EVM-native. + let query_request = QueryRequest::new( + *block_header.state_root_hash(), + Key::Evm(evm::EvmAddr::Nonce(evm_transaction.from())), + vec![], + ); + effect_builder + .query_global_state(query_request) + .event( + move |query_result| Event::GetMissingEvmIdentityNonceResult { + event_metadata, + block_header, + nonce: evm_nonce_from_query_result(query_result), + }, + ) + } + } + } + + fn handle_get_missing_evm_identity_nonce_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + nonce: EvmNonceLookup, + ) -> Effects { + let evm_transaction = event_metadata + .meta_transaction + .as_evm() + .expect("missing EVM identity nonce lookup should only be used for EVM transactions"); + let expected_nonce = match nonce { + EvmNonceLookup::Value(nonce) => nonce, + EvmNonceLookup::Missing => 0, + EvmNonceLookup::Invalid(error_message) => { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::Decode(error_message), + )), + ); + } + }; + let query_request = QueryRequest::new( + *block_header.state_root_hash(), + Key::Evm(evm::EvmAddr::CodeHash(evm_transaction.from())), + vec![], + ); + effect_builder + .query_global_state(query_request) + .event( + move |query_result| Event::GetMissingEvmIdentityCodeHashResult { + event_metadata, + block_header, + expected_nonce, + code_hash: evm_code_hash_from_query_result(query_result), + }, + ) + } + + fn handle_get_missing_evm_identity_code_hash_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + expected_nonce: u64, + code_hash: EvmCodeHashLookup, + ) -> Effects { + let evm_transaction = event_metadata.meta_transaction.as_evm().expect( + "missing EVM identity code-hash lookup should only be used for EVM transactions", + ); + let address = evm_transaction.from(); + let code_hash = match code_hash { + EvmCodeHashLookup::Value(code_hash) => code_hash, + EvmCodeHashLookup::Missing => evm::EMPTY_CODE_HASH, + EvmCodeHashLookup::Invalid(error_message) => { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::Decode(error_message), + )), ); - return self.reject_transaction(effect_builder, *event_metadata, error); } }; + if code_hash != evm::EMPTY_CODE_HASH { + return self.validate_evm_nonce_and_balance( + effect_builder, + event_metadata, + block_header, + expected_nonce, + EvmBalanceSource::Purse(evm::deterministic_purse(address)), + ); + } + + let account_hash = match evm_transaction.signer() { + Ok(signer) => signer.to_account_hash(), + Err(error) => { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm(error)), + ); + } + }; + let entity_addr = EntityAddr::Account(account_hash.value()); + // If the recovered signer already has a Casper account, client balance + // validation should use that account. Otherwise it uses the + // deterministic EVM purse, matching the account-creation plan runtime + // will apply only after payment preconditions pass. + effect_builder + .get_addressable_entity(*block_header.state_root_hash(), entity_addr) + .event(move |result| Event::GetEvmAccountEntityResult { + event_metadata, + block_header, + expected_nonce, + account_hash, + maybe_entity: result.into_option(), + }) + } + + fn handle_get_evm_account_entity_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + expected_nonce: u64, + account_hash: AccountHash, + maybe_entity: Option, + ) -> Effects { let evm_transaction = event_metadata .meta_transaction .as_evm() - .expect("EVM account lookup should only be used for EVM transactions"); - let expected = account.nonce(); + .expect("EVM account entity lookup should only be used for EVM transactions"); + // This is still a read-only acceptor decision. It does not create the + // Casper account or write `EvmAddr::Account`; it only picks the balance + // source that runtime will use when it evaluates the same origin. + let balance_source = if maybe_entity.is_some() { + EvmBalanceSource::Account(account_hash) + } else { + EvmBalanceSource::Purse(evm::deterministic_purse(evm_transaction.from())) + }; + self.validate_evm_nonce_and_balance( + effect_builder, + event_metadata, + block_header, + expected_nonce, + balance_source, + ) + } + + fn query_evm_nonce( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + balance_source: EvmBalanceSource, + ) -> Effects { + let evm_transaction = event_metadata + .meta_transaction + .as_evm() + .expect("EVM nonce lookup should only be used for EVM transactions"); + let query_request = QueryRequest::new( + *block_header.state_root_hash(), + Key::Evm(evm::EvmAddr::Nonce(evm_transaction.from())), + vec![], + ); + effect_builder + .query_global_state(query_request) + .event(move |query_result| Event::GetEvmNonceResult { + event_metadata, + block_header, + balance_source, + nonce: evm_nonce_from_query_result(query_result), + }) + } + + fn handle_get_evm_nonce_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + balance_source: EvmBalanceSource, + nonce: EvmNonceLookup, + ) -> Effects { + let expected_nonce = match nonce { + EvmNonceLookup::Value(nonce) => nonce, + EvmNonceLookup::Missing => 0, + EvmNonceLookup::Invalid(error_message) => { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::Decode(error_message), + )), + ); + } + }; + self.validate_evm_nonce_and_balance( + effect_builder, + event_metadata, + block_header, + expected_nonce, + balance_source, + ) + } + + fn validate_evm_nonce_and_balance( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + expected: u64, + balance_source: EvmBalanceSource, + ) -> Effects { + let evm_transaction = event_metadata + .meta_transaction + .as_evm() + .expect("EVM account validation should only be used for EVM transactions"); let actual = evm_transaction.nonce(); + // EVM nonce validation is independent of the identity pointer. Linking + // an EVM address to a Casper account does not change the EVM replay + // counter. if actual != expected { return self.reject_transaction( effect_builder, @@ -292,13 +590,22 @@ impl TransactionAcceptor { } if event_metadata.source.is_client() { - let balance_request = BalanceRequest::from_purse( - *block_header.state_root_hash(), - block_header.protocol_version(), - account.main_purse(), - BalanceHandling::Available, - ProofHandling::NoProofs, - ); + let balance_request = match balance_source { + EvmBalanceSource::Purse(main_purse) => BalanceRequest::from_purse( + *block_header.state_root_hash(), + block_header.protocol_version(), + main_purse, + BalanceHandling::Available, + ProofHandling::NoProofs, + ), + EvmBalanceSource::Account(account_hash) => BalanceRequest::from_account_hash( + *block_header.state_root_hash(), + block_header.protocol_version(), + account_hash, + BalanceHandling::Available, + ProofHandling::NoProofs, + ), + }; effect_builder .get_balance(balance_request) .event(move |balance_result| Event::GetBalanceResult { @@ -1156,12 +1463,60 @@ impl Component for TransactionAcceptor { Event::GetEvmAccountResult { event_metadata, block_header, - maybe_account, + account, } => self.handle_get_evm_account_result( effect_builder, event_metadata, block_header, - maybe_account, + account, + ), + Event::GetEvmNonceResult { + event_metadata, + block_header, + balance_source, + nonce, + } => self.handle_get_evm_nonce_result( + effect_builder, + event_metadata, + block_header, + balance_source, + nonce, + ), + Event::GetMissingEvmIdentityNonceResult { + event_metadata, + block_header, + nonce, + } => self.handle_get_missing_evm_identity_nonce_result( + effect_builder, + event_metadata, + block_header, + nonce, + ), + Event::GetMissingEvmIdentityCodeHashResult { + event_metadata, + block_header, + expected_nonce, + code_hash, + } => self.handle_get_missing_evm_identity_code_hash_result( + effect_builder, + event_metadata, + block_header, + expected_nonce, + code_hash, + ), + Event::GetEvmAccountEntityResult { + event_metadata, + block_header, + expected_nonce, + account_hash, + maybe_entity, + } => self.handle_get_evm_account_entity_result( + effect_builder, + event_metadata, + block_header, + expected_nonce, + account_hash, + maybe_entity, ), Event::GetContractResult { event_metadata, diff --git a/node/src/components/transaction_acceptor/event.rs b/node/src/components/transaction_acceptor/event.rs index 3919ea90c9..e913cb1c01 100644 --- a/node/src/components/transaction_acceptor/event.rs +++ b/node/src/components/transaction_acceptor/event.rs @@ -3,8 +3,9 @@ use std::fmt::{self, Display, Formatter}; use serde::Serialize; use casper_types::{ - contracts::ProtocolVersionMajor, evm, AddressableEntity, AddressableEntityHash, BlockHeader, - EntityVersion, Package, PackageHash, Timestamp, Transaction, U512, + account::AccountHash, contracts::ProtocolVersionMajor, evm, AddressableEntity, + AddressableEntityHash, BlockHeader, EntityVersion, Package, PackageHash, Timestamp, + Transaction, URef, U512, }; use super::{Error, Source}; @@ -38,6 +39,50 @@ impl EventMetadata { } } +/// Result of looking up the identity record for an EVM address. +#[derive(Clone, Debug, Serialize)] +pub(crate) enum EvmAccountLookup { + /// Identity pointer to a Casper account. + Account(AccountHash), + /// Identity pointer to an EVM-native purse. + Purse(URef), + /// The EVM account identity record exists but is malformed. + Invalid(String), + /// No EVM account identity exists yet. + Missing, +} + +/// Source used for EVM client balance checks. +#[derive(Clone, Copy, Debug, Serialize)] +pub(crate) enum EvmBalanceSource { + /// Check the given purse directly. + Purse(URef), + /// Check the main purse of a Casper account. + Account(AccountHash), +} + +/// Result of looking up a split EVM nonce record. +#[derive(Clone, Debug, Serialize)] +pub(crate) enum EvmNonceLookup { + /// Nonce record exists and decoded successfully. + Value(u64), + /// No nonce record exists. + Missing, + /// The nonce record exists but is malformed. + Invalid(String), +} + +/// Result of looking up a split EVM code-hash record. +#[derive(Clone, Debug, Serialize)] +pub(crate) enum EvmCodeHashLookup { + /// Code-hash record exists and decoded successfully. + Value(evm::Hash), + /// No code-hash record exists. + Missing, + /// The code-hash record exists but is malformed. + Invalid(String), +} + /// `TransactionAcceptor` events. #[derive(Debug, Serialize)] pub(crate) enum Event { @@ -82,7 +127,35 @@ pub(crate) enum Event { GetEvmAccountResult { event_metadata: Box, block_header: Box, - maybe_account: Option, + account: EvmAccountLookup, + }, + /// The result of querying global state for an EVM account nonce. + GetEvmNonceResult { + event_metadata: Box, + block_header: Box, + balance_source: EvmBalanceSource, + nonce: EvmNonceLookup, + }, + /// The result of querying nonce for an EVM transaction whose identity pointer is missing. + GetMissingEvmIdentityNonceResult { + event_metadata: Box, + block_header: Box, + nonce: EvmNonceLookup, + }, + /// The result of querying code hash for an EVM transaction whose identity pointer is missing. + GetMissingEvmIdentityCodeHashResult { + event_metadata: Box, + block_header: Box, + expected_nonce: u64, + code_hash: EvmCodeHashLookup, + }, + /// The result of querying the Casper account matching a missing EVM identity. + GetEvmAccountEntityResult { + event_metadata: Box, + block_header: Box, + expected_nonce: u64, + account_hash: AccountHash, + maybe_entity: Option, }, /// The result of querying global state for a `Contract` to verify the executable logic. GetContractResult { @@ -183,12 +256,40 @@ impl Display for Event { ) } Event::GetEvmAccountResult { event_metadata, .. } => { + write!( + formatter, + "verifying EVM account identity to validate transaction with hash {}", + event_metadata.transaction.hash() + ) + } + Event::GetEvmNonceResult { event_metadata, .. } => { write!( formatter, "verifying EVM account nonce to validate transaction with hash {}", event_metadata.transaction.hash() ) } + Event::GetMissingEvmIdentityNonceResult { event_metadata, .. } => { + write!( + formatter, + "verifying missing EVM identity nonce to validate transaction with hash {}", + event_metadata.transaction.hash() + ) + } + Event::GetMissingEvmIdentityCodeHashResult { event_metadata, .. } => { + write!( + formatter, + "verifying missing EVM identity code hash to validate transaction with hash {}", + event_metadata.transaction.hash() + ) + } + Event::GetEvmAccountEntityResult { event_metadata, .. } => { + write!( + formatter, + "verifying EVM signer account to validate transaction with hash {}", + event_metadata.transaction.hash() + ) + } Event::GetContractResult { event_metadata, block_header, diff --git a/node/src/components/transaction_acceptor/tests.rs b/node/src/components/transaction_acceptor/tests.rs index 5c4604cc20..3c2a3eb6a4 100644 --- a/node/src/components/transaction_acceptor/tests.rs +++ b/node/src/components/transaction_acceptor/tests.rs @@ -219,6 +219,7 @@ enum TestScenario { FromPeerSessionContractPackage(TxnType, ContractPackageScenario), FromClientInvalidTransaction(TxnType), FromClientEvmInvalidNonce, + FromClientEvmMissingIdentityWithCodeHash, FromClientInvalidTransactionZeroPayment(TxnType), FromClientSlightlyFutureDatedTransaction(TxnType), FromClientFutureDatedTransaction(TxnType), @@ -284,6 +285,7 @@ impl TestScenario { | TestScenario::InvalidFieldsFromPeer => Source::Peer(NodeId::random(rng)), TestScenario::FromClientInvalidTransaction(_) | TestScenario::FromClientEvmInvalidNonce + | TestScenario::FromClientEvmMissingIdentityWithCodeHash | TestScenario::FromClientInvalidTransactionZeroPayment(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) | TestScenario::FromClientFutureDatedTransaction(_) @@ -338,7 +340,9 @@ impl TestScenario { txn.invalidate(); Transaction::from(txn) } - TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromClientEvmInvalidNonce => { + TestScenario::FromPeerEvmInvalidNonce + | TestScenario::FromClientEvmInvalidNonce + | TestScenario::FromClientEvmMissingIdentityWithCodeHash => { Transaction::from(signed_evm_legacy_transaction(1)) } TestScenario::FromClientInvalidTransactionZeroPayment(TxnType::V1) => { @@ -891,6 +895,7 @@ impl TestScenario { | TestScenario::FromClientRepeatedValidTransaction(_) | TestScenario::FromClientValidTransaction(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) + | TestScenario::FromClientEvmMissingIdentityWithCodeHash | TestScenario::FromClientSignedByAdmin(..) => true, TestScenario::FromPeerInvalidTransaction(_) | TestScenario::FromPeerEvmInvalidNonce @@ -986,7 +991,9 @@ impl TestScenario { fn is_evm(&self) -> bool { matches!( self, - TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromClientEvmInvalidNonce + TestScenario::FromPeerEvmInvalidNonce + | TestScenario::FromClientEvmInvalidNonce + | TestScenario::FromClientEvmMissingIdentityWithCodeHash ) } } @@ -1091,12 +1098,19 @@ impl reactor::Reactor for Reactor { let query_result = if let Key::Evm(evm::EvmAddr::Account(address)) = query_request.key() { - let main_purse = evm::deterministic_purse(address); - QueryResult::Success { - value: Box::new(StoredValue::Evm(evm::EvmValue::Account( - evm::Account::new(0, evm::EMPTY_CODE_HASH, main_purse), - ))), - proofs: vec![], + if matches!( + self.test_scenario, + TestScenario::FromClientEvmMissingIdentityWithCodeHash + ) { + QueryResult::ValueNotFound("missing EVM identity".to_string()) + } else { + let main_purse = evm::deterministic_purse(address); + QueryResult::Success { + value: Box::new(StoredValue::CLValue( + CLValue::from_t(Key::URef(main_purse)).unwrap(), + )), + proofs: vec![], + } } } else if let Key::Hash(_) | Key::SmartContract(_) = query_request.key() { match &self.test_scenario { @@ -1167,8 +1181,36 @@ impl reactor::Reactor for Reactor { self.test_scenario ), } + } else if let Key::Evm(evm::EvmAddr::Nonce(_)) = query_request.key() { + let nonce = if matches!( + self.test_scenario, + TestScenario::FromClientEvmMissingIdentityWithCodeHash + ) { + 1u64 + } else { + 0u64 + }; + QueryResult::Success { + value: Box::new(StoredValue::CLValue(CLValue::from_t(nonce).unwrap())), + proofs: vec![], + } + } else if let Key::Evm(evm::EvmAddr::CodeHash(_)) = query_request.key() { + let code_hash = if matches!( + self.test_scenario, + TestScenario::FromClientEvmMissingIdentityWithCodeHash + ) { + evm::Hash::new([0x11; evm::HASH_LENGTH]) + } else { + evm::EMPTY_CODE_HASH + }; + QueryResult::Success { + value: Box::new(StoredValue::CLValue( + CLValue::from_t(code_hash).unwrap(), + )), + proofs: vec![], + } } else { - panic!("expect only queries using Key::Package variant"); + panic!("unexpected query: {query_request:?}"); }; responder.respond(query_result).ignore() } @@ -1185,9 +1227,6 @@ impl reactor::Reactor for Reactor { | BalanceIdentifier::PenalizedAccount(account_hash) => { Key::Account(*account_hash) } - BalanceIdentifier::Evm(address) => { - Key::Evm(evm::EvmAddr::Account(*address)) - } BalanceIdentifier::Entity(entity_addr) => { Key::AddressableEntity(*entity_addr) } @@ -1737,6 +1776,7 @@ async fn run_transaction_acceptor_without_timeout( // `AcceptedNewTransaction` announcement with the appropriate source. TestScenario::FromClientValidTransaction(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) + | TestScenario::FromClientEvmMissingIdentityWithCodeHash | TestScenario::FromClientSignedByAdmin(_) => { matches!( event, @@ -2037,6 +2077,13 @@ async fn should_reject_evm_transaction_with_invalid_nonce_from_client() { )) } +#[tokio::test] +async fn should_accept_missing_evm_identity_with_split_nonce_and_code_hash() { + let result = + run_transaction_acceptor(TestScenario::FromClientEvmMissingIdentityWithCodeHash).await; + assert!(result.is_ok()) +} + #[tokio::test] async fn should_reject_invalid_transaction_v1_zero_payment_from_client() { let result = run_transaction_acceptor(TestScenario::FromClientInvalidTransactionZeroPayment( diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index 4fff5ea9cb..f03576f4cb 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -934,11 +934,15 @@ fn seed_evm_account(fixture: &mut TestFixture, address: evm::Address, balance: U let values_to_write = vec![ ( Key::Evm(evm::EvmAddr::Account(address)), - StoredValue::Evm(evm::EvmValue::Account(evm::Account::new( - 0, - EMPTY_CODE_HASH, - main_purse, - ))), + StoredValue::CLValue(CLValue::from_t(Key::URef(main_purse)).unwrap()), + ), + ( + Key::Evm(evm::EvmAddr::Nonce(address)), + StoredValue::CLValue(CLValue::from_t(0u64).unwrap()), + ), + ( + Key::Evm(evm::EvmAddr::CodeHash(address)), + StoredValue::CLValue(CLValue::from_t(EMPTY_CODE_HASH).unwrap()), ), ( Key::Balance(main_purse.addr()), @@ -972,7 +976,46 @@ fn seed_evm_account(fixture: &mut TestFixture, address: evm::Address, balance: U } } -fn evm_balance(fixture: &TestFixture, address: evm::Address, block_height: u64) -> U512 { +struct EvmAccountView { + nonce: u64, + main_purse: URef, +} + +impl EvmAccountView { + fn nonce(&self) -> u64 { + self.nonce + } + + fn main_purse(&self) -> URef { + self.main_purse + } +} + +fn evm_identity_at(fixture: &mut TestFixture, block_height: u64, address: evm::Address) -> Key { + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let block_header = runner + .main_reactor() + .storage() + .read_block_header_by_height(block_height, true) + .expect("failure to read block header") + .expect("should have header"); + let state_root_hash = *block_header.state_root_hash(); + match query_global_state( + fixture, + state_root_hash, + Key::Evm(evm::EvmAddr::Account(address)), + ) { + Some(value) => match *value { + StoredValue::CLValue(cl_value) => cl_value + .into_t::() + .expect("EVM identity should decode to a key"), + value => panic!("expected EVM identity, got {value:?}"), + }, + value => panic!("expected EVM identity, got {value:?}"), + } +} + +fn evm_balance(fixture: &mut TestFixture, address: evm::Address, block_height: u64) -> U512 { let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); let protocol_version = fixture.chainspec.protocol_version(); let block_header = runner @@ -981,14 +1024,17 @@ fn evm_balance(fixture: &TestFixture, address: evm::Address, block_height: u64) .read_block_header_by_height(block_height, true) .expect("failure to read block header") .expect("should have header"); + let state_root_hash = *block_header.state_root_hash(); + let main_purse = evm_account_at(fixture, block_height, address).main_purse(); + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); let result = runner .main_reactor() .contract_runtime() .data_access_layer() - .balance(BalanceRequest::new( - *block_header.state_root_hash(), + .balance(BalanceRequest::from_purse( + state_root_hash, protocol_version, - BalanceIdentifier::Evm(address), + main_purse, BalanceHandling::Total, ProofHandling::NoProofs, )); @@ -1001,7 +1047,7 @@ fn evm_account_at( fixture: &mut TestFixture, block_height: u64, address: evm::Address, -) -> evm::Account { +) -> EvmAccountView { let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); let block_header = runner .main_reactor() @@ -1010,17 +1056,34 @@ fn evm_account_at( .expect("failure to read block header") .expect("should have header"); let state_root_hash = *block_header.state_root_hash(); - match query_global_state( + let identity = evm_identity_at(fixture, block_height, address); + let main_purse = match identity { + Key::URef(uref) => uref, + Key::Account(account_hash) => { + match query_global_state(fixture, state_root_hash, Key::Account(account_hash)) { + Some(value) => match *value { + StoredValue::Account(account) => account.main_purse(), + value => panic!("expected linked account, got {value:?}"), + }, + value => panic!("expected linked account, got {value:?}"), + } + } + value => panic!("unexpected EVM identity key: {value:?}"), + }; + let nonce = match query_global_state( fixture, state_root_hash, - Key::Evm(evm::EvmAddr::Account(address)), + Key::Evm(evm::EvmAddr::Nonce(address)), ) { Some(value) => match *value { - StoredValue::Evm(evm::EvmValue::Account(account)) => account, - value => panic!("expected EVM account, got {value:?}"), + StoredValue::CLValue(cl_value) => { + cl_value.into_t::().expect("nonce should decode") + } + value => panic!("expected EVM nonce, got {value:?}"), }, - value => panic!("expected EVM account, got {value:?}"), - } + None => 0, + }; + EvmAccountView { nonce, main_purse } } fn alloy_address_to_evm_address(address: AlloyAddress) -> evm::Address { @@ -1089,10 +1152,19 @@ async fn should_execute_evm_transaction_and_store_receipt() { assert_eq!(execution_result.receipt.logs[0].topics, vec![EVM_LOG_TOPIC]); assert!(execution_result.receipt.logs[0].data.is_empty()); - let final_balance = evm_balance(&test.fixture, sender, block_height); + let final_balance = evm_balance(&mut test.fixture, sender, block_height); assert_eq!(final_balance, initial_balance - execution_result.cost); let account = evm_account_at(&mut test.fixture, block_height, sender); assert_eq!(account.nonce(), 1); + let signer_account_hash = evm_transaction + .signer() + .expect("EVM transaction should have a signer") + .to_account_hash(); + assert_eq!( + evm_identity_at(&mut test.fixture, block_height, sender), + Key::Account(signer_account_hash) + ); + assert_eq!(account.main_purse(), evm::deterministic_purse(sender)); assert!( block_height > highest_block.height(), "EVM transaction should be included in a later block" @@ -1148,7 +1220,7 @@ async fn should_apply_casper_refund_handling_to_evm_transaction() { max_fee_amount - consumed_fee_amount ); - let final_balance = evm_balance(&test.fixture, sender, block_height); + let final_balance = evm_balance(&mut test.fixture, sender, block_height); assert_eq!(final_balance, initial_balance - consumed_fee_amount); } @@ -1200,7 +1272,7 @@ async fn should_reject_evm_transaction_when_value_and_fee_exceed_balance() { assert_eq!(execution_result.refund, U512::zero()); assert!(execution_result.effects.is_empty()); - let final_balance = evm_balance(&test.fixture, sender, block_height); + let final_balance = evm_balance(&mut test.fixture, sender, block_height); assert_eq!(final_balance, U512::from(EVM_INITIAL_BALANCE)); let (_node_id, runner) = test.fixture.network.nodes().iter().next().unwrap(); @@ -1322,7 +1394,11 @@ async fn should_transfer_to_evm_address_with_native_transfer() { let expected_purse = evm::deterministic_purse(recipient); assert_eq!(account.main_purse(), expected_purse); assert_eq!( - evm_balance(&test.fixture, recipient, block_height), + evm_identity_at(&mut test.fixture, block_height, recipient), + Key::URef(expected_purse) + ); + assert_eq!( + evm_balance(&mut test.fixture, recipient, block_height), U512::from(transfer_amount) ); diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index 0a56f36144..366bf07f2d 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -113,6 +113,7 @@ impl MetaTransaction { match self { MetaTransaction::Deploy(meta_deploy) => !meta_deploy.deploy().is_transfer(), MetaTransaction::V1(v1_txn) => *v1_txn.target() != TransactionTarget::Native, + MetaTransaction::Evm(_) => false, } } diff --git a/storage/src/data_access_layer.rs b/storage/src/data_access_layer.rs index a34c16442e..7af13031ca 100644 --- a/storage/src/data_access_layer.rs +++ b/storage/src/data_access_layer.rs @@ -56,8 +56,8 @@ mod trie; pub use addressable_entity::{AddressableEntityRequest, AddressableEntityResult}; pub use auction::{AuctionMethod, BiddingRequest, BiddingResult}; pub use balance::{ - BalanceHolds, BalanceHoldsWithProof, BalanceIdentifier, BalanceRequest, BalanceResult, - GasHoldBalanceHandling, ProofHandling, ProofsResult, + BalanceHolds, BalanceHoldsWithProof, BalanceIdentifier, BalanceIdentifierFromInitiatorError, + BalanceRequest, BalanceResult, GasHoldBalanceHandling, ProofHandling, ProofsResult, }; pub use balance_hold::{ BalanceHoldError, BalanceHoldKind, BalanceHoldMode, BalanceHoldRequest, BalanceHoldResult, diff --git a/storage/src/data_access_layer/balance.rs b/storage/src/data_access_layer/balance.rs index 1e5ea6b088..6d7d5ba33c 100644 --- a/storage/src/data_access_layer/balance.rs +++ b/storage/src/data_access_layer/balance.rs @@ -9,14 +9,14 @@ use casper_types::{ HANDLE_PAYMENT, }, AccessRights, BlockTime, Digest, EntityAddr, HoldBalanceHandling, InitiatorAddr, Key, - ProtocolVersion, PublicKey, StoredValue, StoredValueTypeMismatch, TimeDiff, URef, URefAddr, - U512, + ProtocolVersion, PublicKey, StoredValue, TimeDiff, URef, URefAddr, U512, }; use itertools::Itertools; use num_rational::Ratio; use num_traits::CheckedMul; use std::{ collections::{btree_map::Entry, BTreeMap}, + convert::TryFrom, fmt::{Display, Formatter}, }; use tracing::error; @@ -62,8 +62,6 @@ pub enum BalanceIdentifier { Public(PublicKey), /// Use main purse of entity from account hash. Account(AccountHash), - /// Use main purse backing an EVM account. - Evm(evm::Address), /// Use main purse of entity. Entity(EntityAddr), /// Use purse at Key::Purse(URefAddr). @@ -74,6 +72,42 @@ pub enum BalanceIdentifier { PenalizedPayment, } +/// Error converting a transaction initiator into a balance identifier. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum BalanceIdentifierFromInitiatorError { + /// EVM initiators require EVM origin resolution before they can identify a purse. + EvmAddress(evm::Address), +} + +impl Display for BalanceIdentifierFromInitiatorError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + match self { + BalanceIdentifierFromInitiatorError::EvmAddress(address) => { + write!( + formatter, + "EVM initiator address {address:?} cannot directly identify a balance" + ) + } + } + } +} + +impl TryFrom for BalanceIdentifier { + type Error = BalanceIdentifierFromInitiatorError; + + fn try_from(value: InitiatorAddr) -> Result { + match value { + InitiatorAddr::PublicKey(public_key) => Ok(BalanceIdentifier::Public(public_key)), + InitiatorAddr::AccountHash(account_hash) => { + Ok(BalanceIdentifier::Account(account_hash)) + } + InitiatorAddr::EvmAddress(address) => { + Err(BalanceIdentifierFromInitiatorError::EvmAddress(address)) + } + } + } +} + impl BalanceIdentifier { /// Returns underlying uref addr from balance identifier, if any. pub fn as_purse_addr(&self) -> Option { @@ -82,7 +116,6 @@ impl BalanceIdentifier { BalanceIdentifier::Purse(uref) => Some(uref.addr()), BalanceIdentifier::Public(_) | BalanceIdentifier::Account(_) - | BalanceIdentifier::Evm(_) | BalanceIdentifier::PenalizedAccount(_) | BalanceIdentifier::PenalizedPayment | BalanceIdentifier::Entity(_) @@ -122,21 +155,6 @@ impl BalanceIdentifier { Err(tce) => return Err(tce), } } - BalanceIdentifier::Evm(address) => { - let key = Key::Evm(evm::EvmAddr::Account(*address)); - match tc.read(&key)? { - Some(StoredValue::Evm(evm::EvmValue::Account(account))) => account.main_purse(), - Some(stored_value) => { - return Err(TrackingCopyError::TypeMismatch( - StoredValueTypeMismatch::new( - "StoredValue::Evm(Account)".to_string(), - stored_value.type_name(), - ), - )); - } - None => return Err(TrackingCopyError::KeyNotFound(key)), - } - } BalanceIdentifier::Entity(entity_addr) => { match tc.runtime_footprint_by_entity_addr(*entity_addr) { Ok(entity) => entity @@ -207,16 +225,6 @@ impl Default for BalanceIdentifier { } } -impl From for BalanceIdentifier { - fn from(value: InitiatorAddr) -> Self { - match value { - InitiatorAddr::PublicKey(public_key) => BalanceIdentifier::Public(public_key), - InitiatorAddr::AccountHash(account_hash) => BalanceIdentifier::Account(account_hash), - InitiatorAddr::EvmAddress(address) => BalanceIdentifier::Evm(address), - } - } -} - /// Processing hold balance handling. #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub struct ProcessingHoldBalanceHandling {} diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index 41058ae317..8af12d9b99 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -2242,18 +2242,39 @@ pub trait StateProvider: Send + Sync + Sized { } } TransferTargetMode::CreateEvmAccount(address) => { + // Native transfers to a missing 20-byte target cannot derive a + // Casper `AccountHash`, because no Ethereum signature/public key + // is part of the transfer. Initialize the minimal EVM-native + // identity instead: deterministic purse, zero nonce, empty code, + // and zero balance before the transfer credits it. let main_purse = evm::deterministic_purse(address); let balance = match CLValue::from_t(U512::zero()) { Ok(balance) => balance, Err(error) => return TransferResult::Failure(TransferError::CLValue(error)), }; + let identity = match CLValue::from_t(Key::URef(main_purse)) { + Ok(identity) => identity, + Err(error) => return TransferResult::Failure(TransferError::CLValue(error)), + }; + let nonce = match CLValue::from_t(0u64) { + Ok(nonce) => nonce, + Err(error) => return TransferResult::Failure(TransferError::CLValue(error)), + }; + let code_hash = match CLValue::from_t(evm::EMPTY_CODE_HASH) { + Ok(code_hash) => code_hash, + Err(error) => return TransferResult::Failure(TransferError::CLValue(error)), + }; tc.borrow_mut().write( Key::Evm(evm::EvmAddr::Account(address)), - StoredValue::Evm(evm::EvmValue::Account(evm::Account::new( - 0, - evm::EMPTY_CODE_HASH, - main_purse, - ))), + StoredValue::CLValue(identity), + ); + tc.borrow_mut().write( + Key::Evm(evm::EvmAddr::Nonce(address)), + StoredValue::CLValue(nonce), + ); + tc.borrow_mut().write( + Key::Evm(evm::EvmAddr::CodeHash(address)), + StoredValue::CLValue(code_hash), ); tc.borrow_mut().write( Key::Balance(main_purse.addr()), diff --git a/storage/src/system/transfer.rs b/storage/src/system/transfer.rs index f0e077c3f8..10e39c2510 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -88,7 +88,11 @@ pub enum TransferTargetMode { /// Main purse of a resolved account. main_purse: URef, }, - /// Native transfer arguments resolved into a transfer to an existing EVM account. + /// Native transfer arguments resolved into a transfer to an existing EVM identity. + /// + /// The identity may point to a linked Casper account main purse or to an + /// EVM-native purse. Transfer records still do not expose an account hash + /// for 20-byte EVM targets. ExistingEvmAccount { /// Main purse of a resolved EVM account. main_purse: URef, @@ -102,7 +106,11 @@ pub enum TransferTargetMode { }, /// Native transfer arguments resolved into a transfer to a new account. CreateAccount(AccountHash), - /// Native transfer arguments resolved into a transfer to a new EVM account. + /// Native transfer arguments resolved into a transfer to a new EVM-native identity. + /// + /// Native transfers do not have an Ethereum signature, so they cannot + /// discover or create a Casper account hash for the 20-byte target. Missing + /// EVM targets therefore get a deterministic purse identity. CreateEvmAccount(evm::Address), } @@ -357,18 +365,46 @@ impl TransferRuntimeArgsBuilder { { let address: evm::Address = self.map_cl_value(cl_value)?; let key = Key::Evm(evm::EvmAddr::Account(address)); - return match tracking_copy.borrow_mut().read(&key)? { - Some(StoredValue::Evm(evm::EvmValue::Account(account))) => { - Ok(TransferTargetMode::ExistingEvmAccount { - main_purse: account.main_purse().with_access_rights(AccessRights::ADD), - }) + let maybe_stored_value = tracking_copy.borrow_mut().read(&key)?; + return match maybe_stored_value { + Some(StoredValue::CLValue(cl_value)) => { + let identity_key = + cl_value.into_t::().map_err(TransferError::CLValue)?; + match identity_key { + // Existing EVM identity linked to a Casper account: + // credit the account's main purse so native and EVM + // sends converge on the same funds. + Key::Account(account_hash) => { + let (_, entity) = tracking_copy + .borrow_mut() + .runtime_footprint_by_account_hash( + protocol_version, + account_hash, + )?; + let main_purse = entity + .main_purse() + .ok_or(TransferError::InvalidPurse)? + .with_access_rights(AccessRights::ADD); + Ok(TransferTargetMode::ExistingEvmAccount { main_purse }) + } + // Existing EVM-native identity: credit its backing + // purse without attempting to infer a Casper + // account hash from the 20-byte address. + Key::URef(uref) => Ok(TransferTargetMode::ExistingEvmAccount { + main_purse: uref.with_access_rights(AccessRights::ADD), + }), + other => Err(TransferError::UnexpectedKeyVariant(other)), + } } Some(stored_value) => { Err(TransferError::TypeMismatch(StoredValueTypeMismatch::new( - "StoredValue::Evm(Account)".to_string(), + "StoredValue::CLValue(Key)".to_string(), stored_value.type_name(), ))) } + // A native transfer has no EVM signature/public key. For a + // new 20-byte target, create the EVM-native deterministic + // purse identity and fund that purse. None => Ok(TransferTargetMode::CreateEvmAccount(address)), }; } diff --git a/storage/src/tracking_copy/byte_size.rs b/storage/src/tracking_copy/byte_size.rs index 7866aa6b03..d71443e6c9 100644 --- a/storage/src/tracking_copy/byte_size.rs +++ b/storage/src/tracking_copy/byte_size.rs @@ -48,7 +48,6 @@ impl ByteSize for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.serialized_length(), StoredValue::EntryPoint(entry_point) => entry_point.serialized_length(), StoredValue::RawBytes(raw_bytes) => raw_bytes.serialized_length(), - StoredValue::Evm(value) => value.serialized_length(), } } } diff --git a/storage/src/tracking_copy/mod.rs b/storage/src/tracking_copy/mod.rs index fe8afa674d..d32747b5a8 100644 --- a/storage/src/tracking_copy/mod.rs +++ b/storage/src/tracking_copy/mod.rs @@ -891,11 +891,6 @@ where StoredValue::RawBytes(_) => { return Ok(query.into_not_found_result("RawBytes value found.")); } - StoredValue::Evm(value) => { - return Ok( - query.into_not_found_result(&format!("{} value found.", value.type_name())) - ); - } } } } diff --git a/types/src/evm.rs b/types/src/evm.rs index a314f581c9..231f0e580a 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -10,18 +10,16 @@ mod address; mod config; mod eth_u256; mod evm_addr; -mod evm_value; mod hash; mod receipt; mod topic; mod transaction; -pub use account::{deterministic_purse, Account, StorageAddr, StorageValue, EMPTY_CODE_HASH}; +pub use account::{deterministic_purse, StorageAddr, EMPTY_CODE_HASH}; pub use address::{Address, ADDRESS_LENGTH}; pub use config::{EvmConfig, EvmSpec}; pub use eth_u256::EthU256; pub use evm_addr::EvmAddr; -pub use evm_value::EvmValue; pub use hash::{Hash, HASH_LENGTH}; pub use receipt::{HaltReason, Log, OutOfGasError, Receipt, ReceiptStatus}; pub use topic::Topic; diff --git a/types/src/evm/account.rs b/types/src/evm/account.rs index c45ac3caa4..5b1d77139c 100644 --- a/types/src/evm/account.rs +++ b/types/src/evm/account.rs @@ -20,129 +20,6 @@ pub const EMPTY_CODE_HASH: Hash = Hash::new([ 0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70, ]); -/// EVM account metadata stored in global state. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[serde(deny_unknown_fields)] -pub struct Account { - nonce: u64, - code_hash: Hash, - main_purse: URef, -} - -impl Account { - /// Creates EVM account metadata. - pub const fn new(nonce: u64, code_hash: Hash, main_purse: URef) -> Self { - Account { - nonce, - code_hash, - main_purse, - } - } - - /// Returns the EVM account nonce. - pub const fn nonce(self) -> u64 { - self.nonce - } - - /// Returns the hash of the bytecode associated with this account. - pub const fn code_hash(self) -> Hash { - self.code_hash - } - - /// Returns the Casper main purse backing this EVM account balance. - pub const fn main_purse(self) -> URef { - self.main_purse - } -} - -impl ToBytes for Account { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - let mut bytes = bytesrepr::allocate_buffer(self)?; - self.write_bytes(&mut bytes)?; - Ok(bytes) - } - - fn serialized_length(&self) -> usize { - self.nonce.serialized_length() - + self.code_hash.serialized_length() - + self.main_purse.serialized_length() - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - self.nonce.write_bytes(writer)?; - self.code_hash.write_bytes(writer)?; - self.main_purse.write_bytes(writer) - } -} - -impl FromBytes for Account { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (nonce, remainder) = u64::from_bytes(bytes)?; - let (code_hash, remainder) = Hash::from_bytes(remainder)?; - let (main_purse, remainder) = URef::from_bytes(remainder)?; - Ok((Account::new(nonce, code_hash, main_purse), remainder)) - } -} - -/// EVM storage value stored in global state. -#[derive( - Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, -)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] -pub struct StorageValue(#[cfg_attr(feature = "json-schema", schemars(with = "String"))] U256); - -impl StorageValue { - /// Creates an EVM storage value from a 256-bit word. - pub const fn new(value: U256) -> Self { - StorageValue(value) - } - - /// Returns the 256-bit word stored in this slot. - pub const fn value(self) -> U256 { - self.0 - } - - /// Returns `true` when all bytes are zero. - pub fn is_zero(&self) -> bool { - self.0.is_zero() - } -} - -impl From for StorageValue { - fn from(value: U256) -> Self { - StorageValue::new(value) - } -} - -impl From for U256 { - fn from(value: StorageValue) -> Self { - value.value() - } -} - -impl ToBytes for StorageValue { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - self.0.to_bytes() - } - - fn serialized_length(&self) -> usize { - self.0.serialized_length() - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - self.0.write_bytes(writer) - } -} - -impl FromBytes for StorageValue { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - U256::from_bytes(bytes).map(|(value, remainder)| (StorageValue::new(value), remainder)) - } -} - /// Global-state address for one EVM account storage slot. #[derive( Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, diff --git a/types/src/evm/evm_addr.rs b/types/src/evm/evm_addr.rs index 460baf3f94..c244c920d1 100644 --- a/types/src/evm/evm_addr.rs +++ b/types/src/evm/evm_addr.rs @@ -14,12 +14,16 @@ use crate::bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}; #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub enum EvmAddr { - /// EVM account metadata address. + /// EVM account identity address. Account(Address), /// EVM contract bytecode address, keyed by code hash. ByteCode(Hash), /// EVM contract storage slot address. Storage(StorageAddr), + /// EVM account nonce address. + Nonce(Address), + /// EVM account code hash address. + CodeHash(Address), } impl EvmAddr { @@ -29,6 +33,10 @@ impl EvmAddr { pub const BYTE_CODE_TAG: u8 = 1; /// Inner tag for EVM storage addresses. pub const STORAGE_TAG: u8 = 2; + /// Inner tag for EVM account nonce addresses. + pub const NONCE_TAG: u8 = 3; + /// Inner tag for EVM account code hash addresses. + pub const CODE_HASH_TAG: u8 = 4; } impl ToBytes for EvmAddr { @@ -44,6 +52,8 @@ impl ToBytes for EvmAddr { EvmAddr::Account(address) => address.serialized_length(), EvmAddr::ByteCode(hash) => hash.serialized_length(), EvmAddr::Storage(addr) => addr.serialized_length(), + EvmAddr::Nonce(address) => address.serialized_length(), + EvmAddr::CodeHash(address) => address.serialized_length(), } } @@ -61,6 +71,14 @@ impl ToBytes for EvmAddr { writer.push(Self::STORAGE_TAG); addr.write_bytes(writer) } + EvmAddr::Nonce(address) => { + writer.push(Self::NONCE_TAG); + address.write_bytes(writer) + } + EvmAddr::CodeHash(address) => { + writer.push(Self::CODE_HASH_TAG); + address.write_bytes(writer) + } } } } @@ -75,6 +93,10 @@ impl FromBytes for EvmAddr { .map(|(hash, remainder)| (EvmAddr::ByteCode(hash), remainder)), Self::STORAGE_TAG => StorageAddr::from_bytes(remainder) .map(|(addr, remainder)| (EvmAddr::Storage(addr), remainder)), + Self::NONCE_TAG => Address::from_bytes(remainder) + .map(|(address, remainder)| (EvmAddr::Nonce(address), remainder)), + Self::CODE_HASH_TAG => Address::from_bytes(remainder) + .map(|(address, remainder)| (EvmAddr::CodeHash(address), remainder)), _ => Err(bytesrepr::Error::Formatting), } } @@ -83,10 +105,12 @@ impl FromBytes for EvmAddr { #[cfg(any(feature = "testing", test))] impl rand::distributions::Distribution for rand::distributions::Standard { fn sample(&self, rng: &mut R) -> EvmAddr { - match rng.gen_range(0..=2) { + match rng.gen_range(0..=4) { 0 => EvmAddr::Account(Address::new(rng.gen())), 1 => EvmAddr::ByteCode(Hash::new(rng.gen())), 2 => EvmAddr::Storage(StorageAddr::new(Address::new(rng.gen()), rng.gen())), + 3 => EvmAddr::Nonce(Address::new(rng.gen())), + 4 => EvmAddr::CodeHash(Address::new(rng.gen())), _ => unreachable!(), } } @@ -108,5 +132,7 @@ mod tests { address, U256::MAX, ))); + bytesrepr::test_serialization_roundtrip(&EvmAddr::Nonce(address)); + bytesrepr::test_serialization_roundtrip(&EvmAddr::CodeHash(address)); } } diff --git a/types/src/evm/evm_value.rs b/types/src/evm/evm_value.rs deleted file mode 100644 index b52f355daa..0000000000 --- a/types/src/evm/evm_value.rs +++ /dev/null @@ -1,109 +0,0 @@ -use alloc::vec::Vec; - -#[cfg(feature = "datasize")] -use datasize::DataSize; -#[cfg(feature = "json-schema")] -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use super::{Account, StorageValue}; -use crate::{ - bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - ByteCode, -}; - -/// EVM value stored in global state. -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] -pub enum EvmValue { - /// EVM account metadata. - Account(Account), - /// EVM contract bytecode. - ByteCode(ByteCode), - /// EVM contract storage value for one slot. - Storage(StorageValue), -} - -impl EvmValue { - const ACCOUNT_TAG: u8 = 0; - const BYTE_CODE_TAG: u8 = 1; - const STORAGE_TAG: u8 = 2; - - /// Returns a short type name for diagnostics. - pub fn type_name(&self) -> &'static str { - match self { - EvmValue::Account(_) => "EvmAccount", - EvmValue::ByteCode(_) => "EvmByteCode", - EvmValue::Storage(_) => "EvmStorage", - } - } -} - -impl ToBytes for EvmValue { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - let mut bytes = bytesrepr::allocate_buffer(self)?; - self.write_bytes(&mut bytes)?; - Ok(bytes) - } - - fn serialized_length(&self) -> usize { - U8_SERIALIZED_LENGTH - + match self { - EvmValue::Account(account) => account.serialized_length(), - EvmValue::ByteCode(byte_code) => byte_code.serialized_length(), - EvmValue::Storage(value) => value.serialized_length(), - } - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - match self { - EvmValue::Account(account) => { - writer.push(Self::ACCOUNT_TAG); - account.write_bytes(writer) - } - EvmValue::ByteCode(byte_code) => { - writer.push(Self::BYTE_CODE_TAG); - byte_code.write_bytes(writer) - } - EvmValue::Storage(value) => { - writer.push(Self::STORAGE_TAG); - value.write_bytes(writer) - } - } - } -} - -impl FromBytes for EvmValue { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (tag, remainder) = u8::from_bytes(bytes)?; - match tag { - Self::ACCOUNT_TAG => Account::from_bytes(remainder) - .map(|(account, remainder)| (EvmValue::Account(account), remainder)), - Self::BYTE_CODE_TAG => ByteCode::from_bytes(remainder) - .map(|(byte_code, remainder)| (EvmValue::ByteCode(byte_code), remainder)), - Self::STORAGE_TAG => StorageValue::from_bytes(remainder) - .map(|(value, remainder)| (EvmValue::Storage(value), remainder)), - _ => Err(bytesrepr::Error::Formatting), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{bytesrepr, evm, testing::TestRng, ByteCodeKind, U256}; - use rand::Rng; - - #[test] - fn bytesrepr_roundtrip() { - let rng = &mut TestRng::new(); - let account = Account::new(1, evm::EMPTY_CODE_HASH, rng.gen()); - let byte_code = ByteCode::new(ByteCodeKind::EvmPrague, vec![0x60, 0x00]); - let storage = StorageValue::new(U256::MAX); - - bytesrepr::test_serialization_roundtrip(&EvmValue::Account(account)); - bytesrepr::test_serialization_roundtrip(&EvmValue::ByteCode(byte_code)); - bytesrepr::test_serialization_roundtrip(&EvmValue::Storage(storage)); - } -} diff --git a/types/src/evm/hash.rs b/types/src/evm/hash.rs index 0b2a93a9bc..240bacfcb4 100644 --- a/types/src/evm/hash.rs +++ b/types/src/evm/hash.rs @@ -12,7 +12,7 @@ use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Seria use crate::{ bytesrepr::{self, FromBytes, ToBytes}, - Digest, + CLType, CLTyped, Digest, }; /// The number of bytes in an EVM 256-bit hash. @@ -65,6 +65,12 @@ impl Display for Hash { } } +impl CLTyped for Hash { + fn cl_type() -> CLType { + CLType::ByteArray(HASH_LENGTH as u32) + } +} + #[cfg(feature = "json-schema")] impl JsonSchema for Hash { fn schema_name() -> String { diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index 9d194f85cd..d5eaba9c41 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -674,6 +674,11 @@ impl Transaction { &self.approvals } + /// Returns the single public key that signed this EVM transaction. + pub fn signer(&self) -> Result<&PublicKey, TransactionError> { + Ok(self.single_approval()?.signer()) + } + /// Returns this transaction with a replacement approval set. /// /// The stored Ethereum transaction hash is intentionally left unchanged; diff --git a/types/src/execution/transform_kind.rs b/types/src/execution/transform_kind.rs index 995c88ccb6..461739d4c7 100644 --- a/types/src/execution/transform_kind.rs +++ b/types/src/execution/transform_kind.rs @@ -208,11 +208,6 @@ impl TransformKindV2 { let found = "EntryPoint".to_string(); Err(StoredValueTypeMismatch::new(expected, found).into()) } - StoredValue::Evm(value) => { - let expected = "Contract or Account".to_string(); - let found = value.type_name().to_string(); - Err(StoredValueTypeMismatch::new(expected, found).into()) - } }, TransformKindV2::Failure(error) => Err(error), } diff --git a/types/src/gens.rs b/types/src/gens.rs index 4902a13164..6fef440972 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -327,6 +327,10 @@ pub fn evm_addr_arb() -> impl Strategy { (prop::array::uniform20(any::()), u256_arb()).prop_map(|(address, slot)| { evm::EvmAddr::Storage(evm::StorageAddr::new(evm::Address::new(address), slot)) }), + prop::array::uniform20(any::()) + .prop_map(|bytes| evm::EvmAddr::Nonce(evm::Address::new(bytes))), + prop::array::uniform20(any::()) + .prop_map(|bytes| evm::EvmAddr::CodeHash(evm::Address::new(bytes))), ] } @@ -998,24 +1002,6 @@ pub fn stored_value_arb() -> impl Strategy { message_summary_arb().prop_map(StoredValue::Message), named_key_value_arb().prop_map(StoredValue::NamedKey), collection::vec(any::(), 0..1000).prop_map(StoredValue::RawBytes), - (any::(), u8_slice_32(), uref_arb()).prop_map(|(nonce, code_hash, main_purse)| { - StoredValue::Evm(crate::evm::EvmValue::Account(crate::evm::Account::new( - nonce, - crate::evm::Hash::new(code_hash), - main_purse, - ))) - }), - collection::vec(any::(), 0..1000).prop_map(|bytes| { - StoredValue::Evm(crate::evm::EvmValue::ByteCode(ByteCode::new( - ByteCodeKind::EvmPrague, - bytes, - ))) - }), - u256_arb().prop_map(|value| { - StoredValue::Evm(crate::evm::EvmValue::Storage( - crate::evm::StorageValue::new(value), - )) - }), ] .prop_map(|stored_value| // The following match statement is here only to make sure @@ -1042,7 +1028,6 @@ pub fn stored_value_arb() -> impl Strategy { StoredValue::Prepayment(_) => stored_value, StoredValue::EntryPoint(_) => stored_value, StoredValue::RawBytes(_) => stored_value, - StoredValue::Evm(_) => stored_value, }) } diff --git a/types/src/key.rs b/types/src/key.rs index da22c4c081..bd0d320e78 100644 --- a/types/src/key.rs +++ b/types/src/key.rs @@ -87,6 +87,8 @@ const REWARDS_HANDLING_PREFIX: &str = "rewards-handling-"; const EVM_ACCOUNT_PREFIX: &str = "evm-account-"; const EVM_BYTE_CODE_PREFIX: &str = "evm-byte-code-"; const EVM_STORAGE_PREFIX: &str = "evm-storage-"; +const EVM_NONCE_PREFIX: &str = "evm-nonce-"; +const EVM_CODE_HASH_PREFIX: &str = "evm-code-hash-"; const EVM_STORAGE_FORMATTED_LENGTH: usize = EVM_ADDRESS_LENGTH + KEY_HASH_LENGTH; /// The number of bytes in a Blake2b hash @@ -445,6 +447,10 @@ pub enum FromStrError { EvmByteCode(String), /// EVM storage key parse error. EvmStorage(String), + /// EVM nonce key parse error. + EvmNonce(String), + /// EVM code hash key parse error. + EvmCodeHash(String), RewardsHandling(String), /// Unknown prefix. UnknownPrefix, @@ -540,6 +546,12 @@ impl Display for FromStrError { FromStrError::EvmStorage(error) => { write!(f, "evm-storage-key from string error: {}", error) } + FromStrError::EvmNonce(error) => { + write!(f, "evm-nonce-key from string error: {}", error) + } + FromStrError::EvmCodeHash(error) => { + write!(f, "evm-code-hash-key from string error: {}", error) + } FromStrError::RewardsHandling(error) => { write!(f, "rewards-handling-key from string error: {}", error) @@ -735,6 +747,12 @@ impl Key { u256_to_padded_hex(addr.slot()) ) } + Key::Evm(EvmAddr::Nonce(address)) => { + format!("{}{}", EVM_NONCE_PREFIX, address.to_hex_string()) + } + Key::Evm(EvmAddr::CodeHash(address)) => { + format!("{}{}", EVM_CODE_HASH_PREFIX, address.to_hex_string()) + } } } @@ -1098,6 +1116,22 @@ impl Key { )))); } + if let Some(hex) = input.strip_prefix(EVM_NONCE_PREFIX) { + let bytes = checksummed_hex::decode(hex) + .map_err(|error| FromStrError::EvmNonce(error.to_string()))?; + let address = <[u8; EVM_ADDRESS_LENGTH]>::try_from(bytes.as_ref()) + .map_err(|error| FromStrError::EvmNonce(error.to_string()))?; + return Ok(Key::Evm(EvmAddr::Nonce(EvmAddress::new(address)))); + } + + if let Some(hex) = input.strip_prefix(EVM_CODE_HASH_PREFIX) { + let bytes = checksummed_hex::decode(hex) + .map_err(|error| FromStrError::EvmCodeHash(error.to_string()))?; + let address = <[u8; EVM_ADDRESS_LENGTH]>::try_from(bytes.as_ref()) + .map_err(|error| FromStrError::EvmCodeHash(error.to_string()))?; + return Ok(Key::Evm(EvmAddr::CodeHash(EvmAddress::new(address)))); + } + Err(FromStrError::UnknownPrefix) } @@ -1598,6 +1632,10 @@ impl Display for Key { Key::Evm(EvmAddr::Storage(addr)) => { write!(f, "Key::Evm(Storage({}-{}))", addr.address(), addr.slot()) } + Key::Evm(EvmAddr::Nonce(address)) => write!(f, "Key::Evm(Nonce({}))", address), + Key::Evm(EvmAddr::CodeHash(address)) => { + write!(f, "Key::Evm(CodeHash({}))", address) + } } } } diff --git a/types/src/stored_value.rs b/types/src/stored_value.rs index 8bf2a244b5..89d229fe5e 100644 --- a/types/src/stored_value.rs +++ b/types/src/stored_value.rs @@ -22,7 +22,6 @@ use crate::{ contract_messages::{MessageChecksum, MessageTopicSummary}, contract_wasm::ContractWasm, contracts::{Contract, ContractPackage}, - evm, package::Package, system::{ auction::{Bid, BidKind, EraInfo, Unbond, UnbondingPurse, WithdrawPurse}, @@ -79,8 +78,6 @@ pub enum StoredValueTag { EntryPoint = 19, /// Raw bytes. RawBytes = 20, - /// EVM account, bytecode, or storage value. - Evm = 21, } /// A value stored in Global State. @@ -136,8 +133,6 @@ pub enum StoredValue { /// Raw bytes. Similar to a [`crate::StoredValue::CLValue`] but does not incur overhead of a /// [`crate::CLValue`] and [`crate::CLType`]. RawBytes(#[cfg_attr(feature = "json-schema", schemars(with = "String"))] Vec), - /// EVM account, bytecode, or storage value. - Evm(evm::EvmValue), } impl StoredValue { @@ -297,26 +292,10 @@ impl StoredValue { } } - /// Returns EVM account metadata if this is an EVM account value. - pub fn as_evm_account(&self) -> Option<&evm::Account> { - match self { - StoredValue::Evm(evm::EvmValue::Account(account)) => Some(account), - _ => None, - } - } - /// Returns EVM bytecode if this is an EVM bytecode value. pub fn as_evm_byte_code(&self) -> Option<&ByteCode> { match self { - StoredValue::Evm(evm::EvmValue::ByteCode(byte_code)) => Some(byte_code), - _ => None, - } - } - - /// Returns an EVM storage value if this is an EVM storage value. - pub fn as_evm_storage(&self) -> Option<&evm::StorageValue> { - match self { - StoredValue::Evm(evm::EvmValue::Storage(value)) => Some(value), + StoredValue::ByteCode(byte_code) => Some(byte_code), _ => None, } } @@ -475,7 +454,6 @@ impl StoredValue { StoredValue::Prepayment(_) => "Prepayment".to_string(), StoredValue::EntryPoint(_) => "EntryPoint".to_string(), StoredValue::RawBytes(_) => "RawBytes".to_string(), - StoredValue::Evm(value) => value.type_name().to_string(), } } @@ -503,7 +481,6 @@ impl StoredValue { StoredValue::Prepayment(_) => StoredValueTag::Prepayment, StoredValue::EntryPoint(_) => StoredValueTag::EntryPoint, StoredValue::RawBytes(_) => StoredValueTag::RawBytes, - StoredValue::Evm(_) => StoredValueTag::Evm, } } @@ -812,7 +789,6 @@ impl ToBytes for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.serialized_length(), StoredValue::EntryPoint(entry_point_value) => entry_point_value.serialized_length(), StoredValue::RawBytes(bytes) => bytes.serialized_length(), - StoredValue::Evm(value) => value.serialized_length(), } } @@ -842,7 +818,6 @@ impl ToBytes for StoredValue { StoredValue::Prepayment(prepayment_kind) => prepayment_kind.write_bytes(writer), StoredValue::EntryPoint(entry_point_value) => entry_point_value.write_bytes(writer), StoredValue::RawBytes(bytes) => bytes.write_bytes(writer), - StoredValue::Evm(value) => value.write_bytes(writer), } } } @@ -914,8 +889,6 @@ impl FromBytes for StoredValue { let (bytes, remainder) = Bytes::from_bytes(remainder)?; Ok((StoredValue::RawBytes(bytes.into()), remainder)) } - tag if tag == StoredValueTag::Evm as u8 => evm::EvmValue::from_bytes(remainder) - .map(|(value, remainder)| (StoredValue::Evm(value), remainder)), _ => Err(Error::Formatting), } } @@ -959,7 +932,6 @@ pub mod serde_helpers { Prepayment(&'a PrepaymentKind), EntryPoint(&'a EntryPointValue), RawBytes(Bytes), - Evm(&'a evm::EvmValue), } /// A value stored in Global State. @@ -1016,8 +988,6 @@ pub mod serde_helpers { /// Raw bytes. Similar to a [`crate::StoredValue::CLValue`] but does not incur overhead of /// a [`crate::CLValue`] and [`crate::CLType`]. RawBytes(Bytes), - /// EVM account, bytecode, or storage value. - Evm(evm::EvmValue), } impl<'a> From<&'a StoredValue> for HumanReadableSerHelper<'a> { @@ -1056,7 +1026,6 @@ pub mod serde_helpers { StoredValue::RawBytes(bytes) => { HumanReadableSerHelper::RawBytes(bytes.as_slice().into()) } - StoredValue::Evm(value) => HumanReadableSerHelper::Evm(value), } } } @@ -1124,7 +1093,6 @@ pub mod serde_helpers { HumanReadableDeserHelper::Prepayment(prepayment_kind) => { StoredValue::Prepayment(prepayment_kind) } - HumanReadableDeserHelper::Evm(value) => StoredValue::Evm(value), }) } } diff --git a/types/src/transaction/initiator_addr.rs b/types/src/transaction/initiator_addr.rs index e59cddc8e6..0d1b5ebe22 100644 --- a/types/src/transaction/initiator_addr.rs +++ b/types/src/transaction/initiator_addr.rs @@ -51,11 +51,14 @@ pub enum InitiatorAddr { } impl InitiatorAddr { - /// Returns the Casper account hash, if this initiator has one. + /// Returns the Casper account hash carried by this initiator, if it has one. /// - /// EVM initiators do not have a native Casper account hash. EVM-aware code - /// should use [`InitiatorAddr::evm_address`] or - /// [`crate::Transaction::evm_initiator_addr`]. + /// EVM transaction initiators carry only a 20-byte EVM address. That address + /// may later resolve to a linked Casper account hash through global state and + /// signature context, but the mapping is not intrinsic to the initiator value. + /// EVM-aware code should use [`InitiatorAddr::evm_address`] or + /// [`crate::Transaction::evm_initiator_addr`] and perform explicit EVM origin + /// resolution where the transaction signer and state root are available. pub fn account_hash(&self) -> Option { match self { InitiatorAddr::PublicKey(public_key) => Some(public_key.to_account_hash()), From 8c17891a2fb0f9b71e7490a12c2f1e77ace6fae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 21 May 2026 16:11:55 +0200 Subject: [PATCH 14/17] Add core EIP-7702 set-code transaction support Introduce type 0x04 EVM transaction decoding, serialization, signing, verification, config checks, and executor translation. Store signed authorization-list items as transaction data while keeping Casper approvals limited to the outer Ethereum signature. Pass EIP-7702 authorization lists through to revm so Prague execution handles authority recovery, delegation writes, invalid-entry skipping, nonce updates, and delegation clearing. Keep existing Casper policies for blob transactions, non-empty access lists, and non-zero priority fees. Normalize missing dynamic priority fees to zero in the executor to match validation. Update sidecar dependency patches, receipt projection, raw transaction tests, and JSON schema snapshots for type 0x04 transactions. Document EIP-7702 support and devnet verification steps, and add coverage for type-4 decoding, round trips, config compliance, executor semantics, and sidecar raw transaction handling. --- EVM.md | 167 +++++++- executor/evm/Cargo.toml | 2 +- executor/evm/src/tx.rs | 35 +- executor/evm/tests/executor.rs | 358 +++++++++++++++++- .../src/types/transaction/meta_transaction.rs | 108 +++++- .../transaction/meta_transaction/meta_evm.rs | 11 +- types/src/evm.rs | 6 +- types/src/evm/transaction.rs | 323 +++++++++++++++- types/tests/evm_transaction.rs | 164 +++++++- 9 files changed, 1122 insertions(+), 52 deletions(-) diff --git a/EVM.md b/EVM.md index 5fd6583a7f..1cef6b50a4 100644 --- a/EVM.md +++ b/EVM.md @@ -21,7 +21,7 @@ Only EIPs referenced by this document or the current code are listed here. | [EIP-2930][eip-2930] | | Optional access-list transaction type. Empty access-list transactions decode; non-empty access lists are rejected for now. | | [EIP-1559][eip-1559] | | Dynamic-fee transaction type with max fee and priority fee. Casper accepts this envelope for tooling compatibility only when the priority fee is zero. | | [EIP-4844][eip-4844] | | Blob transaction support. Rejected because blob sidecars, blob gas, and KZG data are not modeled. | -| [EIP-7702][eip-7702] | | Set-code transactions for EOAs. Rejected because authorization-list processing and account-code mutation are not implemented. | +| [EIP-7702][eip-7702] | | Set-code transactions for EOAs. Type `0x04` transactions are accepted with non-empty authorization lists; Casper still rejects non-empty access lists and non-zero priority fees. | ## Current Scope @@ -41,6 +41,8 @@ Implemented in this workspace: - Binary-port `Simulate` for read-only `eth_call` support. - Native Casper transfers to 20-byte EVM addresses when `[evm].enabled = true`, creating or funding the corresponding EVM-native purse identity. +- [EIP-7702][eip-7702] type `0x04` set-code transactions, with authorization + lists passed through to `revm` for Prague execution. Implemented in the sidecar workspace for validation: @@ -62,8 +64,8 @@ Not implemented yet: `eth_getFilterChanges`, `eth_getFilterLogs`, `eth_uninstallFilter`, and `eth_subscribe`. - [EIP-4844][eip-4844] blob transactions. -- [EIP-7702][eip-7702] set-code transactions. - Non-empty [EIP-2930][eip-2930]/[EIP-1559][eip-1559] access lists. +- Non-empty [EIP-7702][eip-7702] access lists and non-zero priority fees. - EVM log indexing optimized for historical queries. ## Transaction Shape @@ -75,6 +77,7 @@ a raw signed RLP blob. The EVM transaction is stored as: - Casper envelope metadata: `timestamp` and `ttl`. - Decoded unsigned Ethereum payload fields: `kind`, `chain_id`, `nonce`, gas fields, `value`, `input`, and `to`. +- [EIP-7702][eip-7702] authorization-list items when `kind` is `Eip7702`. - Claimed/recovered EVM sender address: `from`. - Ethereum signed transaction hash: `hash`. - Exactly one Casper `Approval` containing the Ethereum secp256k1 signature. @@ -112,16 +115,18 @@ The current `eth_sendRawTransaction` flow is: 3. `from_signed_rlp` decodes the Ethereum envelope. 4. It rejects unsupported transaction forms: - [EIP-4844][eip-4844] blob transactions. - - [EIP-7702][eip-7702] set-code transactions. - Non-empty access lists. - Unknown typed transactions. -5. It extracts the unsigned Ethereum payload fields. -6. It recovers the secp256k1 public key and EVM address. -7. It converts the Ethereum signature into one Casper `Approval`: +5. For [EIP-7702][eip-7702] type `0x04`, it requires a non-empty + authorization list and a call target, then stores authorization tuples as + EVM transaction data. +6. It extracts the unsigned Ethereum payload fields. +7. It recovers the secp256k1 public key and EVM address. +8. It converts the Ethereum signature into one Casper `Approval`: - `Approval.signer` is the recovered secp256k1 public key. - `Approval.signature` is the 64-byte secp256k1 signature. -8. It stores the Ethereum signed transaction hash. -9. Sidecar wraps the value as `Transaction::Evm` and submits it to node over +9. It stores the Ethereum signed transaction hash. +10. Sidecar wraps the value as `Transaction::Evm` and submits it to node over the existing binary-port transaction submission path. Node does not receive the raw RLP blob for `eth_sendRawTransaction`. Node @@ -755,6 +760,152 @@ Expected output: 1 ``` +### Verify EIP-7702 Set-Code + +The deployed `Counter` contract can also be used as the delegate target for an +[EIP-7702][eip-7702] set-code transaction. This verifies the full path through +off-the-shelf Ethereum tooling: + +- `cast wallet sign-auth` signs the authorization tuple. +- `cast send --auth` submits a type `0x04` transaction through + `eth_sendRawTransaction`. +- `cast receipt` sees the projected receipt as `type: 0x4`. +- A later transaction without `--auth` still executes the delegated code, + proving the delegation persisted in EVM state. + +Use `user-1` as the fee payer and a separate authority EOA as the account +whose code is delegated. The authority key does not need to be funded; it only +signs the EIP-7702 authorization. The `user-1` account pays for the transaction. + +```bash +export RPC_URL=http://127.0.0.1:11101/rpc +export USER_PRIVATE_KEY=0xb6cc5d5faa7c3c37db4bf9a1566023aaa9a1d716fe78ed1a6fb79a690b9400e8 +export USER_ADDRESS=0x24790C4849cCAE43c0c1749e2C5b8d00Cc63AB80 + +export AUTHORITY_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945381cf28caaa54a7dc353d821cabcd789def +export AUTHORITY_ADDRESS=$(cast wallet address --private-key "$AUTHORITY_PRIVATE_KEY") +export COUNTER_ADDRESS=0x6c0704679CA22b83778Ef815607359cf6F5352B6 + +export USER_NONCE=$(cast nonce "$USER_ADDRESS" --rpc-url "$RPC_URL") +``` + +On a fresh devnet where the deploy and increment examples above were run, +`USER_NONCE` should be `2`. If only the deployment was run, it should be `1`. +The authority nonce should be `0` before its first authorization: + +```bash +cast nonce "$AUTHORITY_ADDRESS" --rpc-url "$RPC_URL" +``` + +Expected output: + +```text +0 +``` + +Sign the authorization for the authority account to delegate to the deployed +`Counter` code. The chain ID is the decimal form of `0x435350ff`. + +```bash +export SET_CODE_AUTH=$(cast wallet sign-auth "$COUNTER_ADDRESS" \ + --private-key "$AUTHORITY_PRIVATE_KEY" \ + --chain 1129533695 \ + --nonce 0) +``` + +Submit the set-code transaction. Do not pass `--legacy`; the authorization list +causes Foundry to build an EIP-7702 transaction. Pass +`--priority-gas-price 0` because Casper currently rejects non-zero priority +fees. + +```bash +cast send "$AUTHORITY_ADDRESS" \ + 'increment()' \ + --rpc-url "$RPC_URL" \ + --private-key "$USER_PRIVATE_KEY" \ + --auth "$SET_CODE_AUTH" \ + --gas-price 1000000 \ + --priority-gas-price 0 \ + --gas-limit 300000 \ + --nonce "$USER_NONCE" \ + --json | tee /tmp/casper-eip7702-set-code.json +``` + +Expected receipt checks: + +```bash +export SET_CODE_TX_HASH=$(jq -r .transactionHash /tmp/casper-eip7702-set-code.json) + +cast receipt "$SET_CODE_TX_HASH" \ + --rpc-url "$RPC_URL" \ + --json | tee /tmp/casper-eip7702-set-code-receipt.json + +jq '{type,status,gasUsed,effectiveGasPrice,from,to,logs: [.logs[] | {address,topics,data}]}' \ + /tmp/casper-eip7702-set-code-receipt.json +``` + +Expected highlights: + +```text +type 0x4 +status 0x1 +effectiveGasPrice 0xf4240 +from 0x24790c4849ccae43c0c1749e2c5b8d00cc63ab80 +to $AUTHORITY_ADDRESS +logs[0].address $AUTHORITY_ADDRESS +logs[0].topics[0] 0x59950fb23669ee30425f6d79758e75fae698a6c88b2982f2980638d8bcd9397d +logs[0].topics[1] 0x00000000000000000000000024790c4849ccae43c0c1749e2c5b8d00cc63ab80 +logs[0].data 0x0000000000000000000000000000000000000000000000000000000000000001 +``` + +Reading `get()` through the authority address should now execute delegated +`Counter` code and return `1`. The deployed `Counter` contract has separate +storage; the authority's counter starts from zero even if the original +`Counter` was incremented earlier. + +```bash +cast call "$AUTHORITY_ADDRESS" \ + 'get()(uint256)' \ + --rpc-url "$RPC_URL" + +cast nonce "$AUTHORITY_ADDRESS" --rpc-url "$RPC_URL" +``` + +Expected output: + +```text +1 +1 +``` + +Finally, prove that the delegation persists after the set-code transaction by +calling the authority again with a normal legacy transaction and no +authorization list: + +```bash +export USER_NONCE=$((USER_NONCE + 1)) + +cast send "$AUTHORITY_ADDRESS" \ + 'increment()' \ + --rpc-url "$RPC_URL" \ + --private-key "$USER_PRIVATE_KEY" \ + --legacy \ + --gas-price 1000000 \ + --gas-limit 100000 \ + --nonce "$USER_NONCE" \ + --json | tee /tmp/casper-eip7702-persisted-delegation.json + +cast call "$AUTHORITY_ADDRESS" \ + 'get()(uint256)' \ + --rpc-url "$RPC_URL" +``` + +Expected output from the final `cast call`: + +```text +2 +``` + ### Confirm Fees The native transfer debits devnet `user-1` and credits the EVM identity's diff --git a/executor/evm/Cargo.toml b/executor/evm/Cargo.toml index 033758d08d..4b3fd5dcde 100644 --- a/executor/evm/Cargo.toml +++ b/executor/evm/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/casper-network/casper-node/tree/dev/executor/ev license = "Apache-2.0" [dependencies] +alloy-eips = { version = "=1.4.2", default-features = false, features = ["k256"] } casper-storage = { version = "5.0.0", path = "../../storage" } casper-types = { version = "7.0.0", path = "../../types", features = ["std"] } revm = { version = "38", features = ["dev", "optional_fee_charge"] } @@ -16,5 +17,4 @@ thiserror = "2" [dev-dependencies] alloy-consensus = { version = "=1.0.41", default-features = false, features = ["k256"] } -alloy-eips = { version = "=1.4.2", default-features = false, features = ["k256"] } alloy-primitives = { version = "=1.5.7", default-features = false, features = ["rlp", "sha3-keccak"] } diff --git a/executor/evm/src/tx.rs b/executor/evm/src/tx.rs index fac5e2a424..e79c0d1762 100644 --- a/executor/evm/src/tx.rs +++ b/executor/evm/src/tx.rs @@ -1,5 +1,8 @@ //! Translation from Casper-owned EVM requests into revm transaction environments. +use alloy_eips::eip7702::{ + Authorization as RevmAuthorization, SignedAuthorization as RevmSignedAuthorization, +}; use casper_types::{evm, BlockHash, U256 as CasperU256}; use revm::{ context::TxEnv, @@ -26,6 +29,8 @@ pub(crate) fn build_tx_env(config: &evm::EvmConfig, kind: &ExecuteKind) -> Resul .unwrap_or_else(|| transaction.max_fee_per_gas()), ), evm::TransactionKind::Eip1559 => { + let max_priority_fee_per_gas = + Some(transaction.max_priority_fee_per_gas().unwrap_or(0)); // Preserve the EIP-1559 fields when translating into // revm. Node config compliance currently only admits // zero-priority-fee EIP-1559 transactions because Casper @@ -34,7 +39,22 @@ pub(crate) fn build_tx_env(config: &evm::EvmConfig, kind: &ExecuteKind) -> Resul // typed-transaction adapter. builder .max_fee_per_gas(transaction.max_fee_per_gas()) - .gas_priority_fee(transaction.max_priority_fee_per_gas()) + .gas_priority_fee(max_priority_fee_per_gas) + } + evm::TransactionKind::Eip7702 => { + let max_priority_fee_per_gas = + Some(transaction.max_priority_fee_per_gas().unwrap_or(0)); + builder + .max_fee_per_gas(transaction.max_fee_per_gas()) + .gas_priority_fee(max_priority_fee_per_gas) + .tx_type(Some(evm::EIP7702_TRANSACTION_TYPE_ID)) + .authorization_list_signed( + transaction + .authorization_list() + .iter() + .map(to_revm_authorization) + .collect(), + ) } }; @@ -69,6 +89,19 @@ pub(crate) fn to_revm_address(address: evm::Address) -> Address { Address::from(address.value()) } +fn to_revm_authorization(authorization: &evm::SetCodeAuthorization) -> RevmSignedAuthorization { + RevmSignedAuthorization::new_unchecked( + RevmAuthorization { + chain_id: to_revm_u256(authorization.chain_id), + address: to_revm_address(authorization.address), + nonce: authorization.nonce, + }, + authorization.y_parity, + to_revm_u256(authorization.r), + to_revm_u256(authorization.s), + ) +} + pub(crate) fn from_revm_address(address: Address) -> evm::Address { evm::Address::new(address.into_array()) } diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index c77efaf5a2..551acc67c5 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -1,8 +1,13 @@ use std::path::PathBuf; -use alloy_consensus::{SignableTransaction, TxEnvelope, TxLegacy}; -use alloy_eips::eip2718::Encodable2718; -use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, U256}; +use alloy_consensus::{crypto::secp256k1, SignableTransaction, TxEip7702, TxEnvelope, TxLegacy}; +use alloy_eips::{ + eip2718::Encodable2718, + eip7702::{ + Authorization as AlloyAuthorization, SignedAuthorization as AlloySignedAuthorization, + }, +}; +use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, B256, U256}; use casper_executor_evm::{ BlockContext, BlockHashProvider, BlockHashProviderResult, CallRequest, CallValidation, Error, EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, EMPTY_CODE_HASH, @@ -17,13 +22,17 @@ use casper_storage::{ TrackingCopy, }; use casper_types::{ - contracts::NamedKeys, evm, AccessRights, Account, BlockHash, CLValue, ChainspecRegistry, - Digest, GenesisAccount, GenesisConfig, HoldBalanceHandling, Key, Motes, ProtocolVersion, - PublicKey, SecretKey, StorageCosts, StoredValue, SystemConfig, Timestamp, URef, WasmConfig, - U256 as CasperU256, U512, + bytesrepr::{FromBytes, ToBytes}, + contracts::NamedKeys, + evm, AccessRights, Account, BlockHash, CLValue, ChainspecRegistry, Digest, GenesisAccount, + GenesisConfig, HoldBalanceHandling, Key, Motes, ProtocolVersion, PublicKey, SecretKey, + StorageCosts, StoredValue, SystemConfig, Timestamp, URef, WasmConfig, U256 as CasperU256, U512, }; use revm::bytecode::opcode; +const SIGNING_SECRET: [u8; 32] = [7; 32]; +const AUTHORIZATION_SECRET: [u8; 32] = [8; 32]; + fn tracking_copy() -> (TrackingCopy, impl Send) { let accounts = (1u8..=3) .map(|seed| { @@ -155,6 +164,27 @@ fn blockhash_contract_init_code() -> Vec { init_code_returning(runtime) } +fn return_word_contract_init_code(value: u8) -> Vec { + let runtime = vec![ + opcode::PUSH1, + value, + opcode::PUSH1, + 0, + opcode::MSTORE, + opcode::PUSH1, + 32, + opcode::PUSH1, + 0, + opcode::RETURN, + ]; + init_code_returning(runtime) +} + +fn reverting_contract_init_code() -> Vec { + let runtime = vec![opcode::PUSH1, 0, opcode::PUSH1, 0, opcode::REVERT]; + init_code_returning(runtime) +} + fn call_request( from: evm::Address, to: Option, @@ -214,6 +244,33 @@ fn execute_call>( outcome } +fn execute_transaction>( + executor: &EvmExecutor, + tracking_copy: &mut TrackingCopy, + transaction: evm::Transaction, +) -> casper_executor_evm::ExecutionOutcome { + executor + .execute( + tracking_copy, + ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(transaction), + }, + ) + .expect("EVM transaction execution should succeed") +} + +fn deploy_code>( + executor: &EvmExecutor, + tracking_copy: &mut TrackingCopy, + from: evm::Address, + code: Vec, +) -> evm::Address { + execute_call(executor, tracking_copy, from, None, code) + .created_contract_address + .expect("deploy should return a contract address") +} + fn deploy>( executor: &EvmExecutor, tracking_copy: &mut TrackingCopy, @@ -290,6 +347,14 @@ fn decode_hex(hex: &str) -> Vec { .collect() } +fn to_alloy_address(address: evm::Address) -> AlloyAddress { + AlloyAddress::from(address.value()) +} + +fn alloy_address_to_evm(address: AlloyAddress) -> evm::Address { + evm::Address::new(address.into_array()) +} + fn legacy_transaction(chain_id: Option) -> evm::Transaction { let tx = TxLegacy { chain_id, @@ -310,6 +375,96 @@ fn legacy_transaction(chain_id: Option) -> evm::Transaction { .expect("transaction should decode") } +fn eip7702_transaction( + to: evm::Address, + delegate: evm::Address, + authorization_nonce: u64, + transaction_nonce: u64, + input: Vec, +) -> (evm::Transaction, evm::Address) { + let authorization = signed_authorization(delegate, authorization_nonce); + let authority = alloy_address_to_evm( + authorization + .recover_authority() + .expect("authorization should recover authority"), + ); + let tx = TxEip7702 { + chain_id: 7, + nonce: transaction_nonce, + gas_limit: 1_000_000, + max_fee_per_gas: 1, + max_priority_fee_per_gas: 0, + to: to_alloy_address(to), + value: U256::ZERO, + access_list: Default::default(), + authorization_list: vec![authorization], + input: input.into(), + }; + let signature = secp256k1::sign_message(B256::from(SIGNING_SECRET), tx.signature_hash()) + .expect("transaction signing should succeed"); + let envelope: TxEnvelope = tx.into_signed(signature).into(); + let transaction = evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + casper_types::TimeDiff::from_seconds(60), + ) + .expect("transaction should decode"); + (transaction, authority) +} + +fn eip7702_transaction_without_priority_fee(transaction: evm::Transaction) -> evm::Transaction { + assert_eq!(transaction.max_priority_fee_per_gas(), Some(0)); + let mut bytes = transaction + .to_bytes() + .expect("transaction should serialize"); + let mut offset = 0; + offset += transaction.timestamp().serialized_length(); + offset += transaction.ttl().serialized_length(); + offset += transaction.hash().serialized_length(); + offset += transaction.from().serialized_length(); + offset += transaction.kind().serialized_length(); + offset += transaction.to().serialized_length(); + offset += transaction.nonce().serialized_length(); + offset += transaction.gas_limit().serialized_length(); + offset += transaction.gas_price().serialized_length(); + offset += transaction.max_fee_per_gas().serialized_length(); + + assert_eq!(bytes[offset], 1); + let some_priority_length = transaction.max_priority_fee_per_gas().serialized_length(); + let none_priority = Option::::None + .to_bytes() + .expect("none priority fee should serialize"); + bytes.splice(offset..offset + some_priority_length, none_priority); + + let (transaction, remainder) = + evm::Transaction::from_bytes(&bytes).expect("transaction should deserialize"); + assert!(remainder.is_empty()); + assert_eq!(transaction.max_priority_fee_per_gas(), None); + transaction +} + +fn signed_authorization(delegate: evm::Address, nonce: u64) -> AlloySignedAuthorization { + let authorization = AlloyAuthorization { + chain_id: U256::from(7), + address: to_alloy_address(delegate), + nonce, + }; + let signature = secp256k1::sign_message( + B256::from(AUTHORIZATION_SECRET), + authorization.signature_hash(), + ) + .expect("authorization signing should succeed"); + authorization.into_signed(signature) +} + +fn authorization_authority() -> evm::Address { + alloy_address_to_evm( + signed_authorization(evm::Address::ZERO, 0) + .recover_authority() + .expect("authorization should recover authority"), + ) +} + fn legacy_transaction_without_chain_id() -> evm::Transaction { legacy_transaction(None) } @@ -402,6 +557,40 @@ fn read_evm_nonce>( } } +fn read_code_hash>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> evm::Hash { + match tracking_copy + .read(&Key::Evm(evm::EvmAddr::CodeHash(address))) + .expect("code hash read should not fail") + { + Some(StoredValue::CLValue(value)) => value.into_t::().unwrap(), + Some(other) => panic!("unexpected code hash value: {other:?}"), + None => EMPTY_CODE_HASH, + } +} + +fn read_code>( + tracking_copy: &mut TrackingCopy, + code_hash: evm::Hash, +) -> Option> { + match tracking_copy + .read(&Key::Evm(evm::EvmAddr::ByteCode(code_hash))) + .expect("bytecode read should not fail") + { + Some(StoredValue::ByteCode(byte_code)) => Some(byte_code.bytes().to_vec()), + Some(other) => panic!("unexpected bytecode value: {other:?}"), + None => None, + } +} + +fn delegation_code(delegate: evm::Address) -> Vec { + let mut code = vec![0xef, 0x01, 0x00]; + code.extend_from_slice(delegate.as_bytes()); + code +} + #[test] fn blockhash_uses_supplied_provider() { let executor = executor(evm::EvmSpec::Prague); @@ -450,6 +639,161 @@ fn blockhash_uses_supplied_provider() { assert_eq!(outcome.output.as_slice(), block_hash_for_height(1).as_ref()); } +#[test] +fn eip7702_authorization_installs_delegation_and_executes_delegate_code() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + return_word_contract_init_code(42), + ); + let (transaction, recovered_authority) = + eip7702_transaction(authority, delegate, 0, 0, Vec::new()); + assert_eq!(recovered_authority, authority); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(decode_word(&outcome.output), 42); + let code_hash = read_code_hash(&mut tracking_copy, authority); + assert_ne!(code_hash, EMPTY_CODE_HASH); + assert_eq!( + read_code(&mut tracking_copy, code_hash), + Some(delegation_code(delegate)) + ); +} + +#[test] +fn eip7702_missing_priority_fee_defaults_to_zero_for_execution() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + return_word_contract_init_code(42), + ); + let (transaction, _) = eip7702_transaction(authority, delegate, 0, 0, Vec::new()); + let transaction = eip7702_transaction_without_priority_fee(transaction); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(decode_word(&outcome.output), 42); +} + +#[test] +fn eip7702_delegation_persists_when_call_reverts() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + reverting_contract_init_code(), + ); + let (transaction, _) = eip7702_transaction(authority, delegate, 0, 0, Vec::new()); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + + assert_eq!(outcome.status, ExecutionStatus::Revert); + let code_hash = read_code_hash(&mut tracking_copy, authority); + assert_eq!( + read_code(&mut tracking_copy, code_hash), + Some(delegation_code(delegate)) + ); +} + +#[test] +fn eip7702_stale_authorization_is_skipped() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + return_word_contract_init_code(42), + ); + let (transaction, _) = eip7702_transaction(authority, delegate, 1, 0, Vec::new()); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert!(outcome.output.is_empty()); + assert_eq!(read_evm_nonce(&mut tracking_copy, authority), 0); + assert_eq!( + read_code_hash(&mut tracking_copy, authority), + EMPTY_CODE_HASH + ); +} + +#[test] +fn eip7702_zero_address_authorization_clears_delegation() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + return_word_contract_init_code(42), + ); + let (transaction, _) = eip7702_transaction(authority, delegate, 0, 0, Vec::new()); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + assert_eq!(outcome.status, ExecutionStatus::Success); + + let (clear_transaction, _) = + eip7702_transaction(authority, evm::Address::ZERO, 1, 1, Vec::new()); + let outcome = execute_transaction(&executor, &mut tracking_copy, clear_transaction); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!( + read_code_hash(&mut tracking_copy, authority), + EMPTY_CODE_HASH + ); +} + #[test] fn counter_supports_committed_and_discarded_execution() { let executor = executor(evm::EvmSpec::Prague); diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index 366bf07f2d..880a6d6162 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -543,8 +543,8 @@ pub(crate) fn calculate_transaction_lane_for_transaction( #[cfg(test)] mod tests { use super::*; - use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope, TxLegacy}; - use alloy_eips::eip2718::Encodable2718; + use alloy_consensus::{SignableTransaction, TxEip1559, TxEip7702, TxEnvelope, TxLegacy}; + use alloy_eips::{eip2718::Encodable2718, eip7702::Authorization as AlloyAuthorization}; use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, U256}; use casper_types::TransactionLaneDefinition; @@ -697,6 +697,66 @@ mod tests { )); } + #[test] + fn evm_config_compliance_accepts_eip7702() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID, BASE_FEE.into(), 0, 60_000), + ); + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()) + .expect("valid EIP-7702 transaction should be config compliant"); + } + + #[test] + fn evm_config_compliance_rejects_eip7702_mismatched_chain_id() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID + 1, BASE_FEE.into(), 0, 60_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::ChainIdMismatch { + expected: CHAIN_ID, + actual + })) if actual == CHAIN_ID + 1 + )); + } + + #[test] + fn evm_config_compliance_rejects_eip7702_max_fee_below_base_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID, u128::from(BASE_FEE - 1), 0, 60_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee + })) if max_fee_per_gas == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + + #[test] + fn evm_config_compliance_rejects_eip7702_non_zero_priority_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID, BASE_FEE.into(), 1, 60_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + evm::TransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas: 1 + } + )) + )); + } + #[test] fn evm_config_compliance_rejects_gas_limit_above_block_limit() { let chainspec = chainspec(); @@ -714,6 +774,23 @@ mod tests { )); } + #[test] + fn evm_config_compliance_rejects_eip7702_gas_limit_above_block_limit() { + let chainspec = chainspec(); + let gas_limit = chainspec.evm_config.block_gas_limit + 1; + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID, BASE_FEE.into(), 0, gas_limit), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit: actual_gas_limit, + block_gas_limit + })) if actual_gas_limit == gas_limit && block_gas_limit == chainspec.evm_config.block_gas_limit + )); + } + #[test] fn evm_config_compliance_rejects_invalid_approval() { let chainspec = chainspec(); @@ -801,6 +878,33 @@ mod tests { signed_transaction(tx.into_signed(Signature::test_signature()).into()) } + fn eip7702_transaction( + chain_id: u64, + max_fee_per_gas: u128, + max_priority_fee_per_gas: u128, + gas_limit: u64, + ) -> evm::Transaction { + let authorization = AlloyAuthorization { + chain_id: U256::from(chain_id), + address: AlloyAddress::from([2u8; 20]), + nonce: 0, + } + .into_signed(Signature::test_signature()); + let tx = TxEip7702 { + chain_id, + nonce: 0, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to: AlloyAddress::from([1u8; 20]), + value: U256::ZERO, + access_list: Default::default(), + authorization_list: vec![authorization], + input: Default::default(), + }; + signed_transaction(tx.into_signed(Signature::test_signature()).into()) + } + fn signed_transaction(envelope: TxEnvelope) -> evm::Transaction { evm::Transaction::from_signed_rlp( envelope.encoded_2718(), diff --git a/node/src/types/transaction/meta_transaction/meta_evm.rs b/node/src/types/transaction/meta_transaction/meta_evm.rs index 2e416a2b44..58581114b4 100644 --- a/node/src/types/transaction/meta_transaction/meta_evm.rs +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -129,12 +129,13 @@ impl MetaEvmTransaction { }); } } - evm::TransactionKind::Eip1559 => { + evm::TransactionKind::Eip1559 | evm::TransactionKind::Eip7702 => { // `max_fee_per_gas` is still meaningful on Casper as the user's - // EIP-1559 total price cap. It must at least cover the configured - // EVM base fee; with the priority fee forced to zero below, this - // cap is what lets Ethereum tooling submit type-2 transactions - // without implying transaction priority based on gas parameters. + // dynamic-fee total price cap. It must at least cover the + // configured EVM base fee; with the priority fee forced to zero + // below, this cap is what lets Ethereum tooling submit typed + // dynamic-fee transactions without implying transaction + // priority based on gas parameters. let max_fee_per_gas = transaction.max_fee_per_gas(); if max_fee_per_gas < base_fee { return Err(evm::TransactionError::MaxFeePerGasBelowBaseFee { diff --git a/types/src/evm.rs b/types/src/evm.rs index 231f0e580a..e7569b5c3a 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -24,7 +24,7 @@ pub use hash::{Hash, HASH_LENGTH}; pub use receipt::{HaltReason, Log, OutOfGasError, Receipt, ReceiptStatus}; pub use topic::Topic; pub use transaction::{ - Transaction, TransactionError, TransactionHash, TransactionKind, EIP1559_TRANSACTION_TYPE_ID, - EIP2930_TRANSACTION_TYPE_ID, EIP4844_TRANSACTION_TYPE_ID, EIP7702_TRANSACTION_TYPE_ID, - LEGACY_TRANSACTION_TYPE_ID, + SetCodeAuthorization, Transaction, TransactionError, TransactionHash, TransactionKind, + EIP1559_TRANSACTION_TYPE_ID, EIP2930_TRANSACTION_TYPE_ID, EIP4844_TRANSACTION_TYPE_ID, + EIP7702_TRANSACTION_TYPE_ID, LEGACY_TRANSACTION_TYPE_ID, }; diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index d5eaba9c41..afef12ef12 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -9,12 +9,15 @@ use core::fmt::{self, Display, Formatter}; use alloy_consensus::{ constants::{EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID}, transaction::SignerRecoverable, - SignableTransaction, Transaction as AlloyTransaction, TxEip1559, TxEip2930, TxEnvelope, - TxLegacy, TypedTransaction, + SignableTransaction, Transaction as AlloyTransaction, TxEip1559, TxEip2930, TxEip7702, + TxEnvelope, TxLegacy, TypedTransaction, }; use alloy_eips::{ eip2718::{Decodable2718, Encodable2718}, eip2930::AccessList, + eip7702::{ + Authorization as AlloyAuthorization, SignedAuthorization as AlloyAuthorizationListItem, + }, }; use alloy_primitives::{ keccak256, Address as AlloyAddress, Bytes as AlloyBytes, Signature as AlloySignature, @@ -159,6 +162,8 @@ pub enum TransactionKind { Eip2930, /// An EIP-1559 dynamic-fee transaction. Eip1559, + /// An EIP-7702 set-code transaction. + Eip7702, } impl TransactionKind { @@ -168,6 +173,7 @@ impl TransactionKind { TransactionKind::Legacy => LEGACY_TRANSACTION_TYPE_ID, TransactionKind::Eip2930 => EIP2930_TRANSACTION_TYPE_ID, TransactionKind::Eip1559 => EIP1559_TRANSACTION_TYPE_ID, + TransactionKind::Eip7702 => EIP7702_TRANSACTION_TYPE_ID, } } @@ -182,6 +188,7 @@ impl Display for TransactionKind { TransactionKind::Legacy => formatter.write_str("legacy"), TransactionKind::Eip2930 => formatter.write_str("eip2930"), TransactionKind::Eip1559 => formatter.write_str("eip1559"), + TransactionKind::Eip7702 => formatter.write_str("eip7702"), } } } @@ -208,12 +215,106 @@ impl FromBytes for TransactionKind { 0 => TransactionKind::Legacy, 1 => TransactionKind::Eip2930, 2 => TransactionKind::Eip1559, + EIP7702_TRANSACTION_TYPE_ID => TransactionKind::Eip7702, _ => return Err(bytesrepr::Error::Formatting), }; Ok((kind, remainder)) } } +/// A signed EIP-7702 authorization-list item. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct SetCodeAuthorization { + /// Chain ID that scopes the authorization; zero follows EIP-7702 wildcard semantics. + pub chain_id: U256, + /// Address whose code the authorized account delegates to. + pub address: Address, + /// Nonce expected on the authorizing account. + pub nonce: u64, + /// secp256k1 signature recovery parity. + pub y_parity: u8, + /// secp256k1 signature `r` value. + pub r: U256, + /// secp256k1 signature `s` value. + pub s: U256, +} + +impl SetCodeAuthorization { + fn from_alloy(value: &AlloyAuthorizationListItem) -> Self { + SetCodeAuthorization { + chain_id: alloy_u256_to_casper(*value.chain_id()), + address: alloy_address_to_address(*value.address()), + nonce: value.nonce(), + y_parity: value.y_parity(), + r: alloy_u256_to_casper(value.r()), + s: alloy_u256_to_casper(value.s()), + } + } + + fn to_alloy(&self) -> AlloyAuthorizationListItem { + AlloyAuthorizationListItem::new_unchecked( + AlloyAuthorization { + chain_id: casper_u256_to_alloy(self.chain_id), + address: to_alloy_address(self.address), + nonce: self.nonce, + }, + self.y_parity, + casper_u256_to_alloy(self.r), + casper_u256_to_alloy(self.s), + ) + } +} + +impl ToBytes for SetCodeAuthorization { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.chain_id.serialized_length() + + self.address.serialized_length() + + self.nonce.serialized_length() + + self.y_parity.serialized_length() + + self.r.serialized_length() + + self.s.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.chain_id.write_bytes(writer)?; + self.address.write_bytes(writer)?; + self.nonce.write_bytes(writer)?; + self.y_parity.write_bytes(writer)?; + self.r.write_bytes(writer)?; + self.s.write_bytes(writer) + } +} + +impl FromBytes for SetCodeAuthorization { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (chain_id, remainder) = U256::from_bytes(bytes)?; + let (address, remainder) = Address::from_bytes(remainder)?; + let (nonce, remainder) = u64::from_bytes(remainder)?; + let (y_parity, remainder) = u8::from_bytes(remainder)?; + let (r, remainder) = U256::from_bytes(remainder)?; + let (s, remainder) = U256::from_bytes(remainder)?; + Ok(( + SetCodeAuthorization { + chain_id, + address, + nonce, + y_parity, + r, + s, + }, + remainder, + )) + } +} + /// Errors returned while decoding or validating EVM transactions. #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] @@ -227,6 +328,12 @@ pub enum TransactionError { UnsupportedTransactionType(u8), /// The transaction contains an access list, which this first-pass executor does not model. UnsupportedAccessList, + /// Only EIP-7702 transactions may carry a set-code authorization list. + UnexpectedAuthorizationList, + /// An EIP-7702 transaction must contain at least one authorization. + EmptyAuthorizationList, + /// An EIP-7702 transaction must call an existing target and cannot create a contract. + MissingSetCodeTarget, /// A chain ID was required by the transaction envelope but was missing. MissingChainId, /// A gas price was required by the transaction envelope but was missing. @@ -305,6 +412,15 @@ impl Display for TransactionError { TransactionError::UnsupportedAccessList => { formatter.write_str("unsupported EVM transaction access list") } + TransactionError::UnexpectedAuthorizationList => { + formatter.write_str("unexpected EVM set-code authorization list") + } + TransactionError::EmptyAuthorizationList => { + formatter.write_str("missing EVM set-code authorization list") + } + TransactionError::MissingSetCodeTarget => { + formatter.write_str("missing EVM set-code transaction target") + } TransactionError::MissingChainId => formatter.write_str("missing EVM chain ID"), TransactionError::MissingGasPrice => formatter.write_str("missing EVM gas price"), TransactionError::MissingTransactionLane => { @@ -410,6 +526,7 @@ pub struct Transaction { value: U256, input: Vec, chain_id: Option, + authorization_list: Vec, approvals: BTreeSet, } @@ -430,6 +547,7 @@ struct TransactionSerHelper<'a> { value: U256, input: &'a Vec, chain_id: Option, + authorization_list: &'a Vec, approvals: &'a BTreeSet, } @@ -450,6 +568,7 @@ struct TransactionDeserHelper { value: U256, input: Vec, chain_id: Option, + authorization_list: Vec, approvals: BTreeSet, } @@ -471,6 +590,7 @@ impl Serialize for Transaction { value: self.value, input: &self.input, chain_id: self.chain_id, + authorization_list: &self.authorization_list, approvals: &self.approvals, } .serialize(serializer) @@ -496,6 +616,7 @@ impl<'de> Deserialize<'de> for Transaction { value: helper.value, input: helper.input, chain_id: helper.chain_id, + authorization_list: helper.authorization_list, approvals: helper.approvals, }; transaction.verify().map_err(de::Error::custom)?; @@ -524,20 +645,6 @@ impl Transaction { )); } - if matches!(raw_signed_rlp.first(), Some(&EIP7702_TRANSACTION_TYPE_ID)) { - // EIP-7702 lets EOAs temporarily behave like they have delegated - // code by attaching an `authorization_list`; the protocol - // processes those authorizations before execution and writes - // delegation indicators like `0xef0100 || address` into account - // code. Our current transaction type does not store an - // authorization list, and the executor/state adapter does not - // implement that pre-execution account-code mutation and nonce - // logic. - return Err(TransactionError::UnsupportedTransactionType( - raw_signed_rlp[0], - )); - } - let mut encoded = raw_signed_rlp.as_slice(); let envelope = TxEnvelope::decode_2718(&mut encoded) .map_err(|error| TransactionError::Decode(format!("{error:?}")))?; @@ -559,6 +666,8 @@ impl Transaction { TransactionKind::Eip2930 } else if envelope.is_eip1559() { TransactionKind::Eip1559 + } else if envelope.is_eip7702() { + TransactionKind::Eip7702 } else { return Err(TransactionError::UnsupportedTransactionType( envelope.tx_type() as u8, @@ -576,6 +685,22 @@ impl Transaction { let from = envelope .recover_signer() .map_err(|error| TransactionError::SenderRecovery(format!("{error:?}")))?; + let authorization_list = match envelope.as_eip7702() { + Some(transaction) => { + if transaction.tx().authorization_list.is_empty() { + // Keep raw decode errors precise before constructing a + // transaction that `verify` would reject anyway. + return Err(TransactionError::EmptyAuthorizationList); + } + transaction + .tx() + .authorization_list + .iter() + .map(SetCodeAuthorization::from_alloy) + .collect() + } + None => Vec::new(), + }; Ok(Transaction { timestamp, ttl, @@ -591,6 +716,7 @@ impl Transaction { value: alloy_u256_to_casper(envelope.value()), input: envelope.input().to_vec(), chain_id: envelope.chain_id(), + authorization_list, approvals, }) } @@ -771,6 +897,11 @@ impl Transaction { self.chain_id } + /// Returns the EIP-7702 set-code authorization list. + pub fn authorization_list(&self) -> &[SetCodeAuthorization] { + &self.authorization_list + } + /// Returns the effective gas price at the supplied block base fee. /// /// Legacy and EIP-2930 transactions use their signed gas price directly. @@ -788,7 +919,7 @@ impl Transaction { TransactionKind::Legacy | TransactionKind::Eip2930 => { self.gas_price.unwrap_or(self.max_fee_per_gas) } - TransactionKind::Eip1559 => { + TransactionKind::Eip1559 | TransactionKind::Eip7702 => { let max_priority_fee_per_gas = self.max_priority_fee_per_gas.unwrap_or(0); let priority_fee = self.max_fee_per_gas.saturating_sub(u128::from(base_fee)); if priority_fee > max_priority_fee_per_gas { @@ -835,7 +966,27 @@ impl Transaction { Ok(unsigned.into_envelope(signature)) } + fn validate_authorization_list(&self) -> Result<(), TransactionError> { + match self.kind { + TransactionKind::Eip7702 => { + if self.authorization_list.is_empty() { + return Err(TransactionError::EmptyAuthorizationList); + } + if self.to.is_none() { + return Err(TransactionError::MissingSetCodeTarget); + } + } + TransactionKind::Legacy | TransactionKind::Eip2930 | TransactionKind::Eip1559 => { + if !self.authorization_list.is_empty() { + return Err(TransactionError::UnexpectedAuthorizationList); + } + } + } + Ok(()) + } + fn unsigned_transaction(&self) -> Result { + self.validate_authorization_list()?; let to = match self.to { Some(address) => AlloyTxKind::Call(to_alloy_address(address)), None => AlloyTxKind::Create, @@ -873,6 +1024,25 @@ impl Transaction { access_list: AccessList::default(), input, })), + TransactionKind::Eip7702 => { + let address = self.to.expect("EIP-7702 target validated above"); + Ok(TypedTransaction::Eip7702(TxEip7702 { + chain_id: self.chain_id.ok_or(TransactionError::MissingChainId)?, + nonce: self.nonce, + gas_limit: self.gas_limit, + max_fee_per_gas: self.max_fee_per_gas, + max_priority_fee_per_gas: self.max_priority_fee_per_gas.unwrap_or(0), + to: to_alloy_address(address), + value, + access_list: AccessList::default(), + authorization_list: self + .authorization_list + .iter() + .map(SetCodeAuthorization::to_alloy) + .collect(), + input, + })) + } } } @@ -940,6 +1110,7 @@ impl ToBytes for Transaction { + self.value.serialized_length() + Bytes::from(self.input.clone()).serialized_length() + self.chain_id.serialized_length() + + self.authorization_list.serialized_length() + self.approvals.serialized_length() } @@ -958,6 +1129,7 @@ impl ToBytes for Transaction { self.value.write_bytes(writer)?; Bytes::from(self.input.clone()).write_bytes(writer)?; self.chain_id.write_bytes(writer)?; + self.authorization_list.write_bytes(writer)?; self.approvals.write_bytes(writer) } } @@ -978,6 +1150,7 @@ impl FromBytes for Transaction { let (value, remainder) = U256::from_bytes(remainder)?; let (input, remainder) = Bytes::from_bytes(remainder)?; let (chain_id, remainder) = Option::::from_bytes(remainder)?; + let (authorization_list, remainder) = Vec::::from_bytes(remainder)?; let (approvals, remainder) = BTreeSet::::from_bytes(remainder)?; let transaction = Transaction { timestamp, @@ -994,6 +1167,7 @@ impl FromBytes for Transaction { value, input: input.into(), chain_id, + authorization_list, approvals, }; transaction @@ -1082,3 +1256,118 @@ fn casper_u256_to_alloy(value: U256) -> AlloyU256 { value.to_big_endian(&mut bytes); AlloyU256::from_be_slice(&bytes) } + +#[cfg(test)] +mod tests { + use alloy_consensus::crypto::secp256k1; + + use super::*; + + const SIGNING_SECRET: [u8; 32] = [7; 32]; + const AUTHORIZATION_SECRET: [u8; 32] = [8; 32]; + + #[test] + fn eip7702_transaction_serde_roundtrips_authorization_list() { + let transaction = signed_eip7702_transaction(); + + let serialized = serde_json::to_string(&transaction).expect("transaction should serialize"); + assert!(serialized.contains("authorization_list")); + let deserialized: Transaction = + serde_json::from_str(&serialized).expect("transaction should deserialize"); + + assert_eq!(deserialized, transaction); + assert_eq!( + deserialized.authorization_list(), + transaction.authorization_list() + ); + } + + #[test] + fn non_eip7702_transaction_serde_rejects_authorization_list() { + let mut transaction = signed_legacy_transaction(); + transaction + .authorization_list + .push(set_code_authorization()); + let serialized = serde_json::to_string(&transaction).expect("transaction should serialize"); + + let error = + serde_json::from_str::(&serialized).expect_err("transaction should fail"); + + assert!(error + .to_string() + .contains("unexpected EVM set-code authorization list")); + } + + fn signed_legacy_transaction() -> Transaction { + let tx = TxLegacy { + chain_id: Some(7), + nonce: 3, + gas_price: 1, + gas_limit: 21_000, + to: AlloyTxKind::Call(AlloyAddress::from([4; 20])), + value: AlloyU256::ZERO, + input: AlloyBytes::default(), + }; + let transaction_signature = + secp256k1::sign_message(B256::from(SIGNING_SECRET), tx.signature_hash()) + .expect("transaction signing should succeed"); + let envelope: TxEnvelope = tx.into_signed(transaction_signature).into(); + + Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") + } + + fn signed_eip7702_transaction() -> Transaction { + let authorization = AlloyAuthorization { + chain_id: AlloyU256::from(7), + address: AlloyAddress::from([9; 20]), + nonce: 4, + }; + let authorization_signature = secp256k1::sign_message( + B256::from(AUTHORIZATION_SECRET), + authorization.signature_hash(), + ) + .expect("authorization signing should succeed"); + let tx = TxEip7702 { + chain_id: 7, + nonce: 3, + gas_limit: 70_000, + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 0, + to: AlloyAddress::from([4; 20]), + value: AlloyU256::from(987u64), + access_list: AccessList::default(), + authorization_list: vec![authorization.into_signed(authorization_signature)], + input: AlloyBytes::from(vec![0xde, 0xad]), + }; + let transaction_signature = + secp256k1::sign_message(B256::from(SIGNING_SECRET), tx.signature_hash()) + .expect("transaction signing should succeed"); + let envelope: TxEnvelope = tx.into_signed(transaction_signature).into(); + + Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") + } + + fn set_code_authorization() -> SetCodeAuthorization { + let authorization = AlloyAuthorization { + chain_id: AlloyU256::from(7), + address: AlloyAddress::from([9; 20]), + nonce: 4, + }; + let signature = secp256k1::sign_message( + B256::from(AUTHORIZATION_SECRET), + authorization.signature_hash(), + ) + .expect("authorization signing should succeed"); + SetCodeAuthorization::from_alloy(&authorization.into_signed(signature)) + } +} diff --git a/types/tests/evm_transaction.rs b/types/tests/evm_transaction.rs index ac8204140b..eac8ead70f 100644 --- a/types/tests/evm_transaction.rs +++ b/types/tests/evm_transaction.rs @@ -2,24 +2,28 @@ use std::collections::BTreeSet; use alloy_consensus::{ crypto::secp256k1, transaction::SignerRecoverable, SignableTransaction, TxEip1559, TxEip2930, - TxEnvelope, TxLegacy, + TxEip7702, TxEnvelope, TxLegacy, }; use alloy_eips::{ eip2718::Encodable2718, eip2930::{AccessList, AccessListItem}, + eip7702::{ + Authorization as AlloyAuthorization, SignedAuthorization as AlloySignedAuthorization, + }, }; use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, B256, U256 as AlloyU256}; use casper_types::{ bytesrepr::{FromBytes, ToBytes}, evm::{ self, Address, Hash, Transaction, TransactionError, TransactionKind, - EIP4844_TRANSACTION_TYPE_ID, EIP7702_TRANSACTION_TYPE_ID, + EIP4844_TRANSACTION_TYPE_ID, }, Approval, ApprovalsHash, Digest, PublicKey, SecretKey, TimeDiff, Timestamp, Transaction as CasperTransaction, TransactionHash, U256, }; const SIGNING_SECRET: [u8; 32] = [7; 32]; +const AUTHORIZATION_SECRET: [u8; 32] = [8; 32]; #[test] fn decodes_legacy_signed_rlp() { @@ -83,6 +87,48 @@ fn decodes_eip1559_signed_rlp() { .expect("EIP-1559 transaction should verify"); } +#[test] +fn decodes_eip7702_signed_rlp() { + let signed_transaction = signed_eip7702_transaction(); + let transaction = decode(signed_transaction.raw_rlp.clone()); + + assert_eq!(transaction.kind(), TransactionKind::Eip7702); + assert_eq!(transaction.from(), signed_transaction.sender); + assert_eq!(transaction.to(), Some(address(4))); + assert_eq!(transaction.nonce(), 3); + assert_eq!(transaction.gas_limit(), 70_000); + assert_eq!(transaction.max_fee_per_gas(), 2_000_000_000); + assert_eq!(transaction.max_priority_fee_per_gas(), Some(0)); + assert_eq!(transaction.value(), U256::from(987u64)); + assert_eq!(transaction.input(), &[0xde, 0xad]); + assert_eq!(transaction.chain_id(), Some(7)); + assert_eq!(transaction.authorization_list().len(), 1); + + let expected = &signed_transaction.authorization_list[0]; + let actual = &transaction.authorization_list()[0]; + assert_eq!(actual.chain_id, alloy_u256_to_casper(*expected.chain_id())); + assert_eq!( + actual.address, + alloy_address_to_address(*expected.address()) + ); + assert_eq!(actual.nonce, expected.nonce()); + assert_eq!(actual.y_parity, expected.y_parity()); + assert_eq!(actual.r, alloy_u256_to_casper(expected.r())); + assert_eq!(actual.s, alloy_u256_to_casper(expected.s())); + + transaction + .verify() + .expect("EIP-7702 transaction should verify"); + transaction + .signature_hash() + .expect("EIP-7702 signing hash should be available"); + assert_eq!( + transaction.signed_rlp().unwrap(), + signed_transaction.raw_rlp + ); + bytesrepr_roundtrip(&transaction); +} + #[test] fn unsupported_typed_transactions_are_clear_errors() { let timestamp = Timestamp::zero(); @@ -94,12 +140,6 @@ fn unsupported_typed_transactions_are_clear_errors() { EIP4844_TRANSACTION_TYPE_ID )) ); - assert_eq!( - Transaction::from_signed_rlp(vec![EIP7702_TRANSACTION_TYPE_ID], timestamp, ttl), - Err(TransactionError::UnsupportedTransactionType( - EIP7702_TRANSACTION_TYPE_ID - )) - ); } #[test] @@ -111,6 +151,22 @@ fn non_empty_access_lists_are_rejected() { Transaction::from_signed_rlp(signed_eip2930_with_access_list(), timestamp, ttl), Err(TransactionError::UnsupportedAccessList) ); + assert_eq!( + Transaction::from_signed_rlp(signed_eip7702_with_access_list(), timestamp, ttl), + Err(TransactionError::UnsupportedAccessList) + ); +} + +#[test] +fn empty_eip7702_authorization_lists_are_rejected() { + assert_eq!( + Transaction::from_signed_rlp( + signed_eip7702_with_authorization_list(Vec::new(), AccessList::default()), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ), + Err(TransactionError::EmptyAuthorizationList) + ); } #[test] @@ -175,6 +231,31 @@ fn evm_transaction_sign_replaces_approval_and_recomputes_identity() { assert_eq!(decoded.approvals(), evm_transaction.approvals()); } +#[test] +fn evm_eip7702_transaction_sign_preserves_authorizations() { + let mut transaction = CasperTransaction::from(decode(signed_eip7702_transaction().raw_rlp)); + let CasperTransaction::Evm(evm_transaction) = &transaction else { + panic!("expected EVM transaction"); + }; + let authorization_list = evm_transaction.authorization_list().to_vec(); + let new_secret_key = secp_secret_key([1; SecretKey::SECP256K1_LENGTH]); + + transaction.sign(&new_secret_key); + + let CasperTransaction::Evm(evm_transaction) = transaction else { + panic!("expected EVM transaction"); + }; + assert_eq!(evm_transaction.authorization_list(), authorization_list); + evm_transaction + .verify() + .expect("signed EIP-7702 transaction should verify"); + + let decoded = decode(evm_transaction.signed_rlp().unwrap()); + assert_eq!(decoded.kind(), TransactionKind::Eip7702); + assert_eq!(decoded.authorization_list(), authorization_list); + assert_eq!(decoded.from(), evm_transaction.from()); +} + #[test] #[should_panic(expected = "EVM transactions must be signed with a valid secp256k1 key")] fn evm_transaction_sign_rejects_non_secp256k1_keys() { @@ -241,6 +322,7 @@ fn evm_hashes_round_trip_raw_digest_bytes() { struct SignedTransaction { raw_rlp: Vec, sender: Address, + authorization_list: Vec, } fn decode(bytes: Vec) -> Transaction { @@ -303,6 +385,64 @@ fn signed_eip1559_transaction() -> SignedTransaction { signed_transaction(tx.into_signed(signature).into()) } +fn signed_eip7702_transaction() -> SignedTransaction { + signed_transaction(signed_eip7702_envelope( + vec![signed_authorization(alloy_address(9), 4)], + AccessList::default(), + )) +} + +fn signed_eip7702_with_authorization_list( + authorization_list: Vec, + access_list: AccessList, +) -> Vec { + signed_eip7702_envelope(authorization_list, access_list).encoded_2718() +} + +fn signed_eip7702_with_access_list() -> Vec { + signed_eip7702_with_authorization_list( + vec![signed_authorization(alloy_address(9), 4)], + AccessList(vec![AccessListItem { + address: alloy_address(8), + storage_keys: vec![B256::from([9u8; 32])], + }]), + ) +} + +fn signed_eip7702_envelope( + authorization_list: Vec, + access_list: AccessList, +) -> TxEnvelope { + let tx = TxEip7702 { + chain_id: 7, + nonce: 3, + gas_limit: 70_000, + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 0, + to: alloy_address(4), + value: AlloyU256::from(987u64), + access_list, + authorization_list, + input: vec![0xde, 0xad].into(), + }; + let signature = sign_transaction(&tx); + tx.into_signed(signature).into() +} + +fn signed_authorization(delegate_address: AlloyAddress, nonce: u64) -> AlloySignedAuthorization { + let authorization = AlloyAuthorization { + chain_id: AlloyU256::from(7), + address: delegate_address, + nonce, + }; + let signature = secp256k1::sign_message( + B256::from(AUTHORIZATION_SECRET), + authorization.signature_hash(), + ) + .expect("authorization signing should succeed"); + authorization.into_signed(signature) +} + fn signed_transaction(envelope: TxEnvelope) -> SignedTransaction { let sender = alloy_address_to_address( envelope @@ -312,6 +452,10 @@ fn signed_transaction(envelope: TxEnvelope) -> SignedTransaction { SignedTransaction { raw_rlp: envelope.encoded_2718(), sender, + authorization_list: envelope + .as_eip7702() + .map(|transaction| transaction.tx().authorization_list.clone()) + .unwrap_or_default(), } } @@ -342,6 +486,10 @@ fn alloy_address_to_address(address: AlloyAddress) -> Address { Address::new(address.into_array()) } +fn alloy_u256_to_casper(value: AlloyU256) -> U256 { + U256::from_big_endian(&value.to_be_bytes::<32>()) +} + fn signed_eip2930_with_access_list() -> Vec { let tx = TxEip2930 { chain_id: 7, From 20dee346b9d37095ca9d62af45279fed1c269a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 21 May 2026 18:21:31 +0200 Subject: [PATCH 15/17] Route EVM calls through speculative execution Remove the binary-port EVM call simulation command and route sidecar read-only EVM calls through TrySpeculativeExec. Add an unsigned EVM call marker transaction that carries chain ID and gas price so normal EVM config compliance still applies before execution. Return EVM speculative execution data through a dedicated binary-port result type, and keep unsigned call marker transactions rejected by transaction acceptance so they cannot be submitted as normal network transactions. --- EVM.md | 35 +- binary_port/src/command.rs | 41 +- binary_port/src/lib.rs | 7 +- binary_port/src/response_type.rs | 33 +- .../src/speculative_execution_result.rs | 133 ++++++- binary_port/src/type_wrappers.rs | 369 +----------------- node/BINARY_PORT_PROTOCOL.md | 3 +- node/src/components/binary_port.rs | 57 +-- node/src/components/binary_port/config.rs | 13 - node/src/components/binary_port/event.rs | 10 +- node/src/components/binary_port/metrics.rs | 13 - node/src/components/binary_port/tests.rs | 79 +--- node/src/components/contract_runtime.rs | 27 +- .../components/contract_runtime/operations.rs | 140 +++++-- node/src/components/contract_runtime/types.rs | 1 + node/src/components/network/tasks.rs | 199 +++++----- node/src/components/transaction_acceptor.rs | 14 + node/src/effect.rs | 24 +- node/src/effect/requests.rs | 27 +- .../src/reactor/main_reactor/tests/fixture.rs | 1 - .../src/types/transaction/meta_transaction.rs | 54 +++ .../transaction/meta_transaction/meta_evm.rs | 4 +- .../integration-test/config-example.toml | 6 - resources/local/config.toml | 6 - resources/mainnet/config-example.toml | 6 - resources/production/config-example.toml | 6 - resources/testnet/config-example.toml | 6 - storage/src/system/transfer.rs | 43 +- types/src/evm/transaction.rs | 143 ++++++- 29 files changed, 631 insertions(+), 869 deletions(-) diff --git a/EVM.md b/EVM.md index 1cef6b50a4..976639b0e7 100644 --- a/EVM.md +++ b/EVM.md @@ -6,7 +6,7 @@ execution, and how to reproduce the working Foundry deployment flow. The current scope is intentionally narrow. Casper node can accept and execute `Transaction::Evm` transactions, store EVM execution results, and serve -read-only EVM calls through a binary-port command consumed by sidecar. Native +read-only EVM calls through binary-port speculative execution consumed by sidecar. Native Ethereum JSON-RPC remains a sidecar concern. ## EIP Glossary @@ -38,7 +38,7 @@ Implemented in this workspace: - `casper-executor-evm`, backed by `revm`, with Casper-owned public types. - Contract runtime execution for finalized `Transaction::Evm` values. - Casper fee and refund handling for EVM transactions. -- Binary-port `Simulate` for read-only `eth_call` support. +- Binary-port `TrySpeculativeExec` for read-only `eth_call` support. - Native Casper transfers to 20-byte EVM addresses when `[evm].enabled = true`, creating or funding the corresponding EVM-native purse identity. - [EIP-7702][eip-7702] type `0x04` set-code transactions, with authorization @@ -425,25 +425,36 @@ Sidecar derives those fields from execution info and block transaction order: ## Read-only EVM Calls -`eth_call` uses the node binary-port `Simulate` command, not transaction submission. +`eth_call` uses the node binary-port `TrySpeculativeExec` command, not +`TryAcceptTransaction` submission. -The binary-port request carries: +Sidecar constructs a `Transaction::Evm` with +`evm::Transaction::new_unsigned_call`, which carries: +- chain ID, - `from`, - `to`, - `value`, - input bytes, -- gas limit. - -Node handles the request only when simulation is enabled for the binary port. +- gas limit, +- gas price. + +Node handles the request only when speculative execution is enabled for the binary port. +The unsigned call still passes EVM config compliance checks, including EVM +enablement, chain ID, gas price, and block gas limit. It only +skips signature verification because read-only `eth_call` requests are not +signed Ethereum transactions. The transaction acceptor still rejects this +marker shape so unsigned calls cannot be submitted through `TryAcceptTransaction`. Contract runtime checks out state at the requested/latest block, runs `casper-executor-evm` with: - `ExecuteKind::Call`, - `CallValidation::UncheckedSimulation`, -and returns output, status, and gas used. The tracking-copy effects are -discarded. +and returns output, receipt status, and gas used in +`EvmSpeculativeExecutionResult`, carried by the contract-runtime +`SpeculativeExecutionResult::Evm` variant. +The tracking-copy effects are discarded. ## Block Hashes @@ -525,7 +536,7 @@ cargo build -p casper-sidecar The devnet tool needs a custom asset named `evm` that points at the debug node and sidecar binaries built above, plus the local chainspec and config files from this workspace. Use a node config where -`[binary_port_server].allow_request_simulate = true`; the checked-in local +`[binary_port_server].allow_request_speculative_exec = true`; the checked-in local config defaults this to `false`, so copy `resources/local/config.toml` and enable it in the copy used for this custom asset. @@ -534,7 +545,7 @@ For example: ```bash export EVM_DEVNET_NODE_CONFIG=/tmp/casper-node-evm-devnet-config.toml cp "$CASPER_NODE_WORKSPACE/resources/local/config.toml" "$EVM_DEVNET_NODE_CONFIG" -# Edit $EVM_DEVNET_NODE_CONFIG so allow_request_simulate = true. +# Edit $EVM_DEVNET_NODE_CONFIG so allow_request_speculative_exec = true. ``` From a separate `casper-devnet` checkout, register the asset with: @@ -925,7 +936,7 @@ Node workspace: ```bash cargo check -p casper-node --bin casper-node -cargo test -p casper-binary-port simulation --lib +cargo test -p casper-binary-port --lib ``` Sidecar workspace: diff --git a/binary_port/src/command.rs b/binary_port/src/command.rs index f34e286c2f..a3099028d0 100644 --- a/binary_port/src/command.rs +++ b/binary_port/src/command.rs @@ -5,7 +5,7 @@ use casper_types::{ Transaction, }; -use crate::{get_request::GetRequest, SimulationRequest}; +use crate::get_request::GetRequest; #[cfg(test)] use casper_types::testing::TestRng; @@ -116,11 +116,6 @@ pub enum Command { /// Transaction to execute. transaction: Transaction, }, - /// Request to run a simulation. - Simulate { - /// Simulation request. - request: SimulationRequest, - }, } impl Command { @@ -130,7 +125,6 @@ impl Command { Command::Get(_) => CommandTag::Get, Command::TryAcceptTransaction { .. } => CommandTag::TryAcceptTransaction, Command::TrySpeculativeExec { .. } => CommandTag::TrySpeculativeExec, - Command::Simulate { .. } => CommandTag::Simulate, } } @@ -144,9 +138,6 @@ impl Command { CommandTag::TrySpeculativeExec => Self::TrySpeculativeExec { transaction: Transaction::random(rng), }, - CommandTag::Simulate => Self::Simulate { - request: SimulationRequest::random(rng), - }, } } } @@ -163,7 +154,6 @@ impl ToBytes for Command { Command::Get(inner) => inner.write_bytes(writer), Command::TryAcceptTransaction { transaction } => transaction.write_bytes(writer), Command::TrySpeculativeExec { transaction } => transaction.write_bytes(writer), - Command::Simulate { request } => request.write_bytes(writer), } } @@ -172,7 +162,6 @@ impl ToBytes for Command { Command::Get(inner) => inner.serialized_length(), Command::TryAcceptTransaction { transaction } => transaction.serialized_length(), Command::TrySpeculativeExec { transaction } => transaction.serialized_length(), - Command::Simulate { request } => request.serialized_length(), } } } @@ -194,10 +183,6 @@ impl TryFrom<(CommandTag, &[u8])> for Command { let (transaction, remainder) = FromBytes::from_bytes(bytes)?; (Command::TrySpeculativeExec { transaction }, remainder) } - CommandTag::Simulate => { - let (request, remainder) = FromBytes::from_bytes(bytes)?; - (Command::Simulate { request }, remainder) - } }; if !remainder.is_empty() { return Err(bytesrepr::Error::LeftOverBytes); @@ -216,19 +201,16 @@ pub enum CommandTag { TryAcceptTransaction = 1, /// Request to execute a transaction speculatively. TrySpeculativeExec = 2, - /// Request to run a simulation. - Simulate = 3, } impl CommandTag { /// Creates a random `CommandTag`. #[cfg(test)] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..4) { + match rng.gen_range(0..3) { 0 => CommandTag::Get, 1 => CommandTag::TryAcceptTransaction, 2 => CommandTag::TrySpeculativeExec, - 3 => CommandTag::Simulate, _ => unreachable!(), } } @@ -242,7 +224,6 @@ impl TryFrom for CommandTag { 0 => Ok(CommandTag::Get), 1 => Ok(CommandTag::TryAcceptTransaction), 2 => Ok(CommandTag::TrySpeculativeExec), - 3 => Ok(CommandTag::Simulate), _ => Err(InvalidCommandTag), } } @@ -278,22 +259,4 @@ mod tests { let bytes = val.to_bytes().expect("should serialize"); assert_eq!(Command::try_from((val.tag(), &bytes[..])), Ok(val)); } - - #[test] - fn simulate_request_bytesrepr_roundtrip() { - let rng = &mut TestRng::new(); - - let val = Command::Simulate { - request: SimulationRequest::EvmCall(crate::EvmCallRequest::new( - casper_types::evm::Address::new(rng.gen()), - rng.gen::() - .then(|| casper_types::evm::Address::new(rng.gen())), - casper_types::U256::from_big_endian(&rng.gen::<[u8; 32]>()), - casper_types::bytesrepr::Bytes::from(rng.random_vec(0..64)), - rng.gen(), - )), - }; - let bytes = val.to_bytes().expect("should serialize"); - assert_eq!(Command::try_from((val.tag(), &bytes[..])), Ok(val)); - } } diff --git a/binary_port/src/lib.rs b/binary_port/src/lib.rs index 58a7dbb131..b341c6a2ad 100644 --- a/binary_port/src/lib.rs +++ b/binary_port/src/lib.rs @@ -46,11 +46,10 @@ pub use node_status::NodeStatus; pub use purse_identifier::PurseIdentifier; pub use record_id::{RecordId, UnknownRecordId}; pub use response_type::{PayloadEntity, ResponseType}; -pub use speculative_execution_result::SpeculativeExecutionResult; +pub use speculative_execution_result::{EvmSpeculativeExecutionResult, SpeculativeExecutionResult}; pub use state_request::GlobalStateRequest; pub use type_wrappers::{ AccountInformation, AddressableEntityInformation, ConsensusStatus, ConsensusValidatorChanges, - ContractInformation, DictionaryQueryResult, EvmCallRequest, EvmCallResult, GetTrieFullResult, - LastProgress, NetworkName, ReactorStateName, RewardResponse, SimulationRequest, - SimulationResult, TransactionWithExecutionInfo, Uptime, ValueWithProof, + ContractInformation, DictionaryQueryResult, GetTrieFullResult, LastProgress, NetworkName, + ReactorStateName, RewardResponse, TransactionWithExecutionInfo, Uptime, ValueWithProof, }; diff --git a/binary_port/src/response_type.rs b/binary_port/src/response_type.rs index efaf0181ec..e3c8e06154 100644 --- a/binary_port/src/response_type.rs +++ b/binary_port/src/response_type.rs @@ -18,14 +18,13 @@ use casper_types::{ use crate::{ global_state_query_result::GlobalStateQueryResult, node_status::NodeStatus, - speculative_execution_result::SpeculativeExecutionResult, + speculative_execution_result::{EvmSpeculativeExecutionResult, SpeculativeExecutionResult}, type_wrappers::{ ConsensusStatus, ConsensusValidatorChanges, GetTrieFullResult, LastProgress, NetworkName, ReactorStateName, RewardResponse, }, AccountInformation, AddressableEntityInformation, BalanceResponse, ContractInformation, - DictionaryQueryResult, RecordId, SimulationResult, TransactionWithExecutionInfo, Uptime, - ValueWithProof, + DictionaryQueryResult, RecordId, TransactionWithExecutionInfo, Uptime, ValueWithProof, }; /// A type of the payload being returned in a binary response. @@ -120,8 +119,8 @@ pub enum ResponseType { PackageWithProof, /// Addressable entity information. AddressableEntityInformation, - /// Result of a simulation. - SimulationResult, + /// Result of the EVM speculative execution. + EvmSpeculativeExecutionResult, } impl ResponseType { @@ -231,7 +230,9 @@ impl TryFrom for ResponseType { x if x == ResponseType::AddressableEntityInformation as u8 => { Ok(ResponseType::AddressableEntityInformation) } - x if x == ResponseType::SimulationResult as u8 => Ok(ResponseType::SimulationResult), + x if x == ResponseType::EvmSpeculativeExecutionResult as u8 => { + Ok(ResponseType::EvmSpeculativeExecutionResult) + } _ => Err(()), } } @@ -294,7 +295,9 @@ impl fmt::Display for ResponseType { ResponseType::AddressableEntityInformation => { write!(f, "AddressableEntityInformation") } - ResponseType::SimulationResult => write!(f, "SimulationResult"), + ResponseType::EvmSpeculativeExecutionResult => { + write!(f, "EvmSpeculativeExecutionResult") + } } } } @@ -393,6 +396,10 @@ impl PayloadEntity for SpeculativeExecutionResult { const RESPONSE_TYPE: ResponseType = ResponseType::SpeculativeExecutionResult; } +impl PayloadEntity for EvmSpeculativeExecutionResult { + const RESPONSE_TYPE: ResponseType = ResponseType::EvmSpeculativeExecutionResult; +} + impl PayloadEntity for NodeStatus { const RESPONSE_TYPE: ResponseType = ResponseType::NodeStatus; } @@ -457,10 +464,6 @@ impl PayloadEntity for AddressableEntityInformation { const RESPONSE_TYPE: ResponseType = ResponseType::AddressableEntityInformation; } -impl PayloadEntity for SimulationResult { - const RESPONSE_TYPE: ResponseType = ResponseType::SimulationResult; -} - impl PayloadEntity for Box where T: PayloadEntity, @@ -480,12 +483,4 @@ mod tests { let val = ResponseType::random(rng); assert_eq!(ResponseType::try_from(val as u8), Ok(val)); } - - #[test] - fn simulation_result_response_type_roundtrip() { - assert_eq!( - ResponseType::try_from(ResponseType::SimulationResult as u8), - Ok(ResponseType::SimulationResult) - ); - } } diff --git a/binary_port/src/speculative_execution_result.rs b/binary_port/src/speculative_execution_result.rs index 4fb68c90c6..7d7b8e3a5a 100644 --- a/binary_port/src/speculative_execution_result.rs +++ b/binary_port/src/speculative_execution_result.rs @@ -9,8 +9,9 @@ use rand::distributions::{Alphanumeric, DistString}; #[cfg(any(feature = "testing", test))] use casper_types::testing::TestRng; use casper_types::{ - bytesrepr::{self, FromBytes, ToBytes}, + bytesrepr::{self, Bytes, FromBytes, ToBytes}, contract_messages::Messages, + evm, execution::Effects, BlockHash, Digest, Gas, InvalidTransaction, Transfer, }; @@ -170,6 +171,128 @@ impl FromBytes for SpeculativeExecutionResult { } } +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct EvmSpeculativeExecutionResult { + /// Block hash against which the execution was performed. + block_hash: BlockHash, + /// Gas limit. + limit: Gas, + /// Gas consumed. + consumed: Gas, + /// Execution effects. + effects: Effects, + /// Did the EVM execute successfully? + error: Option, + /// EVM receipt data returned by speculative EVM execution. + evm_receipt: evm::Receipt, + /// EVM return or revert bytes returned by speculative EVM execution. + evm_output: Bytes, +} + +impl EvmSpeculativeExecutionResult { + pub fn new( + block_hash: BlockHash, + limit: Gas, + consumed: Gas, + effects: Effects, + error: Option, + evm_receipt: evm::Receipt, + evm_output: Bytes, + ) -> Self { + EvmSpeculativeExecutionResult { + block_hash, + limit, + consumed, + effects, + error, + evm_receipt, + evm_output, + } + } + + pub fn error(&self) -> Option<&str> { + self.error.as_deref() + } + + pub fn evm_receipt(&self) -> &evm::Receipt { + &self.evm_receipt + } + + pub fn evm_output(&self) -> &[u8] { + self.evm_output.as_ref() + } + + #[cfg(any(feature = "testing", test))] + pub fn random(rng: &mut TestRng) -> Self { + EvmSpeculativeExecutionResult { + block_hash: BlockHash::new(rng.gen()), + limit: Gas::random(rng), + consumed: Gas::random(rng), + effects: Effects::random(rng), + error: if rng.gen() { + None + } else { + let count = rng.gen_range(16..128); + Some(Alphanumeric.sample_string(rng, count)) + }, + evm_receipt: evm::Receipt::random(rng), + evm_output: Bytes::from(rng.random_vec(0..64)), + } + } +} + +impl ToBytes for EvmSpeculativeExecutionResult { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + ToBytes::serialized_length(&self.limit) + + ToBytes::serialized_length(&self.consumed) + + ToBytes::serialized_length(&self.effects) + + ToBytes::serialized_length(&self.error) + + ToBytes::serialized_length(&self.block_hash) + + ToBytes::serialized_length(&self.evm_receipt) + + ToBytes::serialized_length(&self.evm_output) + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.limit.write_bytes(writer)?; + self.consumed.write_bytes(writer)?; + self.effects.write_bytes(writer)?; + self.error.write_bytes(writer)?; + self.block_hash.write_bytes(writer)?; + self.evm_receipt.write_bytes(writer)?; + self.evm_output.write_bytes(writer) + } +} + +impl FromBytes for EvmSpeculativeExecutionResult { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (limit, bytes) = Gas::from_bytes(bytes)?; + let (consumed, bytes) = Gas::from_bytes(bytes)?; + let (effects, bytes) = Effects::from_bytes(bytes)?; + let (error, bytes) = Option::::from_bytes(bytes)?; + let (block_hash, bytes) = BlockHash::from_bytes(bytes)?; + let (evm_receipt, bytes) = evm::Receipt::from_bytes(bytes)?; + let (evm_output, bytes) = Bytes::from_bytes(bytes)?; + Ok(( + EvmSpeculativeExecutionResult { + block_hash, + limit, + consumed, + effects, + error, + evm_receipt, + evm_output, + }, + bytes, + )) + } +} + #[cfg(test)] mod tests { use super::*; @@ -182,4 +305,12 @@ mod tests { let val = SpeculativeExecutionResult::random(rng); bytesrepr::test_serialization_roundtrip(&val); } + + #[test] + fn evm_bytesrepr_roundtrip() { + let rng = &mut TestRng::new(); + + let val = EvmSpeculativeExecutionResult::random(rng); + bytesrepr::test_serialization_roundtrip(&val); + } } diff --git a/binary_port/src/type_wrappers.rs b/binary_port/src/type_wrappers.rs index df37b7acdc..89a14cbf78 100644 --- a/binary_port/src/type_wrappers.rs +++ b/binary_port/src/type_wrappers.rs @@ -2,26 +2,18 @@ use core::{convert::TryFrom, num::TryFromIntError, time::Duration}; use std::collections::BTreeMap; use casper_types::{ - bytesrepr::{self, Bytes, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, + bytesrepr::{self, Bytes, FromBytes, ToBytes}, contracts::ContractHash, - evm, global_state::TrieMerkleProof, system::auction::DelegationRate, Account, AddressableEntity, BlockHash, ByteCode, Contract, ContractWasm, EntityAddr, EraId, ExecutionInfo, Key, PublicKey, StoredValue, TimeDiff, Timestamp, Transaction, ValidatorChange, - U256, U512, + U512, }; use serde::Serialize; -use crate::speculative_execution_result::SpeculativeExecutionResult; - use super::GlobalStateQueryResult; -const SIMULATION_REQUEST_EVM_CALL_TAG: u8 = 0; -const SIMULATION_REQUEST_TRANSACTION_TAG: u8 = 1; -const SIMULATION_RESULT_EVM_CALL_TAG: u8 = 0; -const SIMULATION_RESULT_TRANSACTION_TAG: u8 = 1; - // `bytesrepr` implementations for type wrappers are repetitive, hence this macro helper. We should // get rid of this after we introduce the proper "bytesrepr-derive" proc macro. macro_rules! impl_bytesrepr_for_type_wrapper { @@ -49,303 +41,6 @@ macro_rules! impl_bytesrepr_for_type_wrapper { }; } -/// Request for a read-only EVM call against the latest complete block. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct EvmCallRequest { - from: evm::Address, - to: Option, - value: U256, - input: Bytes, - gas_limit: u64, -} - -impl EvmCallRequest { - /// Constructs a new EVM call request. - pub fn new( - from: evm::Address, - to: Option, - value: U256, - input: Bytes, - gas_limit: u64, - ) -> Self { - Self { - from, - to, - value, - input, - gas_limit, - } - } - - /// Returns the caller address. - pub fn from(&self) -> evm::Address { - self.from - } - - /// Returns the optional target address. - pub fn to(&self) -> Option { - self.to - } - - /// Returns the call value. - pub fn value(&self) -> U256 { - self.value - } - - /// Returns the call input bytes. - pub fn input(&self) -> &[u8] { - self.input.as_ref() - } - - /// Returns the gas limit. - pub fn gas_limit(&self) -> u64 { - self.gas_limit - } -} - -impl ToBytes for EvmCallRequest { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - let mut buffer = bytesrepr::allocate_buffer(self)?; - self.write_bytes(&mut buffer)?; - Ok(buffer) - } - - fn serialized_length(&self) -> usize { - self.from.serialized_length() - + self.to.serialized_length() - + self.value.serialized_length() - + self.input.serialized_length() - + self.gas_limit.serialized_length() - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - self.from.write_bytes(writer)?; - self.to.write_bytes(writer)?; - self.value.write_bytes(writer)?; - self.input.write_bytes(writer)?; - self.gas_limit.write_bytes(writer) - } -} - -impl FromBytes for EvmCallRequest { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (from, remainder) = evm::Address::from_bytes(bytes)?; - let (to, remainder) = Option::::from_bytes(remainder)?; - let (value, remainder) = U256::from_bytes(remainder)?; - let (input, remainder) = Bytes::from_bytes(remainder)?; - let (gas_limit, remainder) = u64::from_bytes(remainder)?; - Ok(( - EvmCallRequest { - from, - to, - value, - input, - gas_limit, - }, - remainder, - )) - } -} - -/// Result of a read-only EVM call. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct EvmCallResult { - status: evm::ReceiptStatus, - output: Bytes, - gas_used: u64, -} - -impl EvmCallResult { - /// Constructs a new EVM call result. - pub fn new(status: evm::ReceiptStatus, output: Bytes, gas_used: u64) -> Self { - Self { - status, - output, - gas_used, - } - } - - /// Returns the call status. - pub fn status(&self) -> evm::ReceiptStatus { - self.status - } - - /// Returns the call output bytes. - pub fn output(&self) -> &[u8] { - self.output.as_ref() - } - - /// Returns gas used by the call. - pub fn gas_used(&self) -> u64 { - self.gas_used - } -} - -impl ToBytes for EvmCallResult { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - let mut buffer = bytesrepr::allocate_buffer(self)?; - self.write_bytes(&mut buffer)?; - Ok(buffer) - } - - fn serialized_length(&self) -> usize { - self.status.serialized_length() - + self.output.serialized_length() - + self.gas_used.serialized_length() - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - self.status.write_bytes(writer)?; - self.output.write_bytes(writer)?; - self.gas_used.write_bytes(writer) - } -} - -impl FromBytes for EvmCallResult { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (status, remainder) = evm::ReceiptStatus::from_bytes(bytes)?; - let (output, remainder) = Bytes::from_bytes(remainder)?; - let (gas_used, remainder) = u64::from_bytes(remainder)?; - Ok(( - EvmCallResult { - status, - output, - gas_used, - }, - remainder, - )) - } -} - -/// Request for a simulation against node state. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub enum SimulationRequest { - /// Read-only EVM call. - EvmCall(EvmCallRequest), - /// Transaction simulation, reserved for future support. - Transaction(Transaction), -} - -impl SimulationRequest { - #[cfg(test)] - pub(crate) fn random(rng: &mut casper_types::testing::TestRng) -> Self { - use rand::Rng; - - if rng.gen() { - SimulationRequest::EvmCall(EvmCallRequest::new( - evm::Address::new(rng.gen()), - rng.gen::().then(|| evm::Address::new(rng.gen())), - U256::from_big_endian(&rng.gen::<[u8; 32]>()), - Bytes::from(rng.random_vec(0..64)), - rng.gen(), - )) - } else { - SimulationRequest::Transaction(Transaction::random(rng)) - } - } -} - -impl ToBytes for SimulationRequest { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - let mut buffer = bytesrepr::allocate_buffer(self)?; - self.write_bytes(&mut buffer)?; - Ok(buffer) - } - - fn serialized_length(&self) -> usize { - U8_SERIALIZED_LENGTH - + match self { - SimulationRequest::EvmCall(request) => request.serialized_length(), - SimulationRequest::Transaction(transaction) => transaction.serialized_length(), - } - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - match self { - SimulationRequest::EvmCall(request) => { - SIMULATION_REQUEST_EVM_CALL_TAG.write_bytes(writer)?; - request.write_bytes(writer) - } - SimulationRequest::Transaction(transaction) => { - SIMULATION_REQUEST_TRANSACTION_TAG.write_bytes(writer)?; - transaction.write_bytes(writer) - } - } - } -} - -impl FromBytes for SimulationRequest { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (tag, remainder) = u8::from_bytes(bytes)?; - match tag { - SIMULATION_REQUEST_EVM_CALL_TAG => { - let (request, remainder) = EvmCallRequest::from_bytes(remainder)?; - Ok((SimulationRequest::EvmCall(request), remainder)) - } - SIMULATION_REQUEST_TRANSACTION_TAG => { - let (transaction, remainder) = Transaction::from_bytes(remainder)?; - Ok((SimulationRequest::Transaction(transaction), remainder)) - } - _ => Err(bytesrepr::Error::Formatting), - } - } -} - -/// Result of a simulation against node state. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub enum SimulationResult { - /// Result of a read-only EVM call. - EvmCall(EvmCallResult), - /// Result of transaction simulation, reserved for future support. - Transaction(SpeculativeExecutionResult), -} - -impl ToBytes for SimulationResult { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - let mut buffer = bytesrepr::allocate_buffer(self)?; - self.write_bytes(&mut buffer)?; - Ok(buffer) - } - - fn serialized_length(&self) -> usize { - U8_SERIALIZED_LENGTH - + match self { - SimulationResult::EvmCall(result) => result.serialized_length(), - SimulationResult::Transaction(result) => result.serialized_length(), - } - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - match self { - SimulationResult::EvmCall(result) => { - SIMULATION_RESULT_EVM_CALL_TAG.write_bytes(writer)?; - result.write_bytes(writer) - } - SimulationResult::Transaction(result) => { - SIMULATION_RESULT_TRANSACTION_TAG.write_bytes(writer)?; - result.write_bytes(writer) - } - } - } -} - -impl FromBytes for SimulationResult { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (tag, remainder) = u8::from_bytes(bytes)?; - match tag { - SIMULATION_RESULT_EVM_CALL_TAG => { - let (result, remainder) = EvmCallResult::from_bytes(remainder)?; - Ok((SimulationResult::EvmCall(result), remainder)) - } - SIMULATION_RESULT_TRANSACTION_TAG => { - let (result, remainder) = SpeculativeExecutionResult::from_bytes(remainder)?; - Ok((SimulationResult::Transaction(result), remainder)) - } - _ => Err(bytesrepr::Error::Formatting), - } - } -} - /// Type representing uptime. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub struct Uptime(u64); @@ -1085,66 +780,6 @@ mod tests { )); } - #[test] - fn evm_call_request_roundtrip() { - let rng = &mut TestRng::new(); - bytesrepr::test_serialization_roundtrip(&EvmCallRequest::new( - evm::Address::new(rng.gen()), - rng.gen::().then(|| evm::Address::new(rng.gen())), - U256::from_big_endian(&rng.gen::<[u8; 32]>()), - Bytes::from(rng.random_vec(0..64)), - rng.gen(), - )); - } - - #[test] - fn evm_call_result_roundtrip() { - let rng = &mut TestRng::new(); - bytesrepr::test_serialization_roundtrip(&EvmCallResult::new( - evm::Receipt::random(rng).status, - Bytes::from(rng.random_vec(0..64)), - rng.gen(), - )); - } - - #[test] - fn simulation_request_evm_call_roundtrip() { - let rng = &mut TestRng::new(); - bytesrepr::test_serialization_roundtrip(&SimulationRequest::EvmCall(EvmCallRequest::new( - evm::Address::new(rng.gen()), - rng.gen::().then(|| evm::Address::new(rng.gen())), - U256::from_big_endian(&rng.gen::<[u8; 32]>()), - Bytes::from(rng.random_vec(0..64)), - rng.gen(), - ))); - } - - #[test] - fn simulation_request_transaction_roundtrip() { - let rng = &mut TestRng::new(); - bytesrepr::test_serialization_roundtrip(&SimulationRequest::Transaction( - Transaction::random(rng), - )); - } - - #[test] - fn simulation_result_evm_call_roundtrip() { - let rng = &mut TestRng::new(); - bytesrepr::test_serialization_roundtrip(&SimulationResult::EvmCall(EvmCallResult::new( - evm::Receipt::random(rng).status, - Bytes::from(rng.random_vec(0..64)), - rng.gen(), - ))); - } - - #[test] - fn simulation_result_transaction_roundtrip() { - let rng = &mut TestRng::new(); - bytesrepr::test_serialization_roundtrip(&SimulationResult::Transaction( - SpeculativeExecutionResult::random(rng), - )); - } - #[test] fn dictionary_query_result_roundtrip() { let rng = &mut TestRng::new(); diff --git a/node/BINARY_PORT_PROTOCOL.md b/node/BINARY_PORT_PROTOCOL.md index 33a4a33dac..52ce277754 100644 --- a/node/BINARY_PORT_PROTOCOL.md +++ b/node/BINARY_PORT_PROTOCOL.md @@ -34,7 +34,7 @@ The Binary Port communication protocol is binary and supports a long lived tcp c ## Request model details -Currently, there are 4 supported types of requests, but the request model can be extended. The request types are: +Currently, there are 3 supported types of requests, but the request model can be extended. The request types are: - A `Get` request, which is one of: - A `Record` request asking for a record with an extensible `RecordId` tag and a key @@ -45,4 +45,3 @@ Currently, there are 4 supported types of requests, but the request model can be - A `Trie` request asking for a trie given a `Digest` - A `TryAcceptTransaction` request for a transaction to be accepted and executed - A `TrySpeculativeExec` request for a transaction to be executed speculatively, without saving the transaction effects in global state -- A `Simulate` request for VM simulation calls, currently supporting read-only EVM calls without saving effects in global state diff --git a/node/src/components/binary_port.rs b/node/src/components/binary_port.rs index f025dea5e5..4fb9d365b9 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -14,11 +14,11 @@ use casper_binary_port::{ AccountInformation, AddressableEntityInformation, BalanceResponse, BinaryMessage, BinaryMessageCodec, BinaryResponse, BinaryResponseAndRequest, Command, CommandHeader, CommandTag, ContractInformation, DictionaryItemIdentifier, DictionaryQueryResult, - EntityIdentifier, EraIdentifier, ErrorCode, EvmCallRequest, GetRequest, GetTrieFullResult, + EntityIdentifier, EraIdentifier, ErrorCode, GetRequest, GetTrieFullResult, GlobalStateEntityQualifier, GlobalStateQueryResult, GlobalStateRequest, InformationRequest, InformationRequestTag, KeyPrefix, NodeStatus, PackageIdentifier, PurseIdentifier, - ReactorStateName, RecordId, ResponseType, RewardResponse, SimulationRequest, SimulationResult, - TransactionWithExecutionInfo, ValueWithProof, + ReactorStateName, RecordId, ResponseType, RewardResponse, TransactionWithExecutionInfo, + ValueWithProof, }; use casper_storage::{ data_access_layer::{ @@ -161,7 +161,6 @@ struct BinaryRequestTerminationDelayValues { get_trie: TimeDiff, accept_transaction: TimeDiff, speculative_exec: TimeDiff, - simulate: TimeDiff, } impl BinaryRequestTerminationDelayValues { @@ -173,7 +172,6 @@ impl BinaryRequestTerminationDelayValues { get_trie: config.get_trie_request_termination_delay, accept_transaction: config.accept_transaction_request_termination_delay, speculative_exec: config.speculative_exec_request_termination_delay, - simulate: config.simulate_request_termination_delay, } } fn get_life_termination_delay(&self, request: &Command) -> TimeDiff { @@ -184,7 +182,6 @@ impl BinaryRequestTerminationDelayValues { Command::Get(GetRequest::Trie { .. }) => self.get_trie, Command::TryAcceptTransaction { .. } => self.accept_transaction, Command::TrySpeculativeExec { .. } => self.speculative_exec, - Command::Simulate { .. } => self.simulate, } } } @@ -225,22 +222,6 @@ where } try_speculative_execution(effect_builder, transaction).await } - Command::Simulate { request } => { - metrics.binary_port_simulate_count.inc(); - if !config.allow_request_simulate { - debug!("received a simulation request while simulation is disabled"); - return BinaryResponse::new_error(ErrorCode::FunctionDisabled); - } - match request { - SimulationRequest::EvmCall(request) => try_evm_call(effect_builder, request).await, - SimulationRequest::Transaction(_) => { - // POC EVM integration only supports read-only EVM call through Simulate. - // Transaction simulation variants are reserved for future Deploy, V1, and EVM - // transaction work. - BinaryResponse::new_error(ErrorCode::UnsupportedRequest) - } - } - } Command::Get(get_req) => { handle_get_request(get_req, effect_builder, config, metrics, protocol_version).await } @@ -1387,8 +1368,10 @@ where None => return BinaryResponse::new_error(ErrorCode::NoCompleteBlocks), }; + let block_hashes = load_recent_evm_block_hashes(effect_builder, tip.height()).await; + let result = effect_builder - .speculatively_execute(Box::new(tip), Box::new(transaction)) + .speculatively_execute(Box::new(tip), block_hashes, Box::new(transaction)) .await; match result { @@ -1399,32 +1382,8 @@ where SpeculativeExecutionResult::WasmV1(spec_exec_result) => { BinaryResponse::from_value(spec_exec_result) } - } -} - -async fn try_evm_call( - effect_builder: EffectBuilder, - request: EvmCallRequest, -) -> BinaryResponse -where - REv: From + From + From, -{ - let tip = match effect_builder - .get_highest_complete_block_header_from_storage() - .await - { - Some(tip) => tip, - None => return BinaryResponse::new_error(ErrorCode::NoCompleteBlocks), - }; - let block_hashes = load_recent_evm_block_hashes(effect_builder, tip.height()).await; - match effect_builder - .evm_call(Box::new(tip), block_hashes, Box::new(request)) - .await - { - Ok(result) => BinaryResponse::from_value(SimulationResult::EvmCall(result)), - Err(error) => { - debug!(%error, "EVM call failed"); - BinaryResponse::new_error(ErrorCode::InternalError) + SpeculativeExecutionResult::Evm(spec_exec_result) => { + BinaryResponse::from_value(spec_exec_result) } } } diff --git a/node/src/components/binary_port/config.rs b/node/src/components/binary_port/config.rs index af8cb059df..fe614b4a21 100644 --- a/node/src/components/binary_port/config.rs +++ b/node/src/components/binary_port/config.rs @@ -32,9 +32,6 @@ const DEFAULT_ACCEPT_TRANSACTION_REQUEST_TERMINATION_DELAY: &str = "24 seconds"; // Default amount of time which is given to a connection to extend it's lifetime when a valid // [`Command::TrySpeculativeExec`] is sent to the node const DEFAULT_SPECULATIVE_EXEC_REQUEST_TERMINATION_DELAY: &str = "0 seconds"; -// Default amount of time which is given to a connection to extend it's lifetime when a valid -// [`Command::Simulate`] is sent to the node -const DEFAULT_SIMULATE_REQUEST_TERMINATION_DELAY: &str = "0 seconds"; /// Binary port server configuration. #[derive(Clone, DataSize, Debug, Deserialize, Serialize)] @@ -53,8 +50,6 @@ pub struct Config { pub allow_request_get_trie: bool, /// Flag used to enable/disable the [`TrySpeculativeExec`] request. pub allow_request_speculative_exec: bool, - /// Flag used to enable/disable the [`Simulate`] request. - pub allow_request_simulate: bool, /// Maximum size of the binary port message. pub max_message_size_bytes: u32, /// Maximum number of connections to the server. @@ -81,9 +76,6 @@ pub struct Config { // The amount of time which is given to a connection to extend it's lifetime when a valid // [`Command::TrySpeculativeExec`] is sent to the node pub speculative_exec_request_termination_delay: TimeDiff, - // The amount of time which is given to a connection to extend it's lifetime when a valid - // [`Command::Simulate`] is sent to the node - pub simulate_request_termination_delay: TimeDiff, } impl Config { @@ -95,7 +87,6 @@ impl Config { allow_request_get_all_values: false, allow_request_get_trie: false, allow_request_speculative_exec: false, - allow_request_simulate: false, max_message_size_bytes: DEFAULT_MAX_MESSAGE_SIZE, max_connections: DEFAULT_MAX_CONNECTIONS, qps_limit: DEFAULT_QPS_LIMIT, @@ -125,10 +116,6 @@ impl Config { DEFAULT_SPECULATIVE_EXEC_REQUEST_TERMINATION_DELAY, ) .unwrap(), - simulate_request_termination_delay: TimeDiff::from_str( - DEFAULT_SIMULATE_REQUEST_TERMINATION_DELAY, - ) - .unwrap(), } } } diff --git a/node/src/components/binary_port/event.rs b/node/src/components/binary_port/event.rs index 108675bae7..429944dd4d 100644 --- a/node/src/components/binary_port/event.rs +++ b/node/src/components/binary_port/event.rs @@ -3,7 +3,7 @@ use std::{ net::SocketAddr, }; -use casper_binary_port::{BinaryResponse, Command, GetRequest, SimulationRequest}; +use casper_binary_port::{BinaryResponse, Command, GetRequest}; use tokio::net::TcpStream; use crate::effect::Responder; @@ -47,14 +47,6 @@ impl Display for Event { Command::TrySpeculativeExec { transaction, .. } => { write!(f, "try speculative exec ({})", transaction.hash()) } - Command::Simulate { request } => match request { - SimulationRequest::EvmCall(request) => { - write!(f, "simulate evm call ({:?})", request.to()) - } - SimulationRequest::Transaction(transaction) => { - write!(f, "simulate transaction ({})", transaction.hash()) - } - }, }, } } diff --git a/node/src/components/binary_port/metrics.rs b/node/src/components/binary_port/metrics.rs index 8e8fc57c80..0d4ff254cb 100644 --- a/node/src/components/binary_port/metrics.rs +++ b/node/src/components/binary_port/metrics.rs @@ -11,9 +11,6 @@ const BINARY_PORT_TRY_SPECULATIVE_EXEC_COUNT_NAME: &str = "binary_port_try_specu const BINARY_PORT_TRY_SPECULATIVE_EXEC_COUNT_HELP: &str = "number of TrySpeculativeExec queries received"; -const BINARY_PORT_SIMULATE_COUNT_NAME: &str = "binary_port_simulate_count"; -const BINARY_PORT_SIMULATE_COUNT_HELP: &str = "number of Simulate queries received"; - const BINARY_PORT_GET_RECORD_COUNT_NAME: &str = "binary_port_get_record_count"; const BINARY_PORT_GET_RECORD_COUNT_HELP: &str = "number of received Get queries for records"; @@ -39,8 +36,6 @@ pub(crate) struct Metrics { pub(super) binary_port_try_accept_transaction_count: IntCounter, /// Number of `TrySpeculativeExec` queries received. pub(super) binary_port_try_speculative_exec_count: IntCounter, - /// Number of `Simulate` queries received. - pub(super) binary_port_simulate_count: IntCounter, /// Number of `Get::Record` queries received. pub(super) binary_port_get_record_count: IntCounter, /// Number of `Get::Information` queries received. @@ -68,11 +63,6 @@ impl Metrics { BINARY_PORT_TRY_SPECULATIVE_EXEC_COUNT_HELP.to_string(), )?; - let binary_port_simulate_count = IntCounter::new( - BINARY_PORT_SIMULATE_COUNT_NAME.to_string(), - BINARY_PORT_SIMULATE_COUNT_HELP.to_string(), - )?; - let binary_port_get_record_count = IntCounter::new( BINARY_PORT_GET_RECORD_COUNT_NAME.to_string(), BINARY_PORT_GET_RECORD_COUNT_HELP.to_string(), @@ -100,7 +90,6 @@ impl Metrics { registry.register(Box::new(binary_port_try_accept_transaction_count.clone()))?; registry.register(Box::new(binary_port_try_speculative_exec_count.clone()))?; - registry.register(Box::new(binary_port_simulate_count.clone()))?; registry.register(Box::new(binary_port_get_record_count.clone()))?; registry.register(Box::new(binary_port_get_info_count.clone()))?; registry.register(Box::new(binary_port_get_state_count.clone()))?; @@ -110,7 +99,6 @@ impl Metrics { Ok(Metrics { binary_port_try_accept_transaction_count, binary_port_try_speculative_exec_count, - binary_port_simulate_count, binary_port_get_record_count, binary_port_get_info_count, binary_port_get_state_count, @@ -125,7 +113,6 @@ impl Drop for Metrics { fn drop(&mut self) { unregister_metric!(self.registry, self.binary_port_try_accept_transaction_count); unregister_metric!(self.registry, self.binary_port_try_speculative_exec_count); - unregister_metric!(self.registry, self.binary_port_simulate_count); unregister_metric!(self.registry, self.binary_port_get_record_count); unregister_metric!(self.registry, self.binary_port_get_info_count); unregister_metric!(self.registry, self.binary_port_get_state_count); diff --git a/node/src/components/binary_port/tests.rs b/node/src/components/binary_port/tests.rs index efd8cf91de..cda56ff013 100644 --- a/node/src/components/binary_port/tests.rs +++ b/node/src/components/binary_port/tests.rs @@ -6,13 +6,12 @@ use rand::Rng; use serde::Serialize; use casper_binary_port::{ - BinaryResponse, Command, EvmCallRequest, GetRequest, GlobalStateEntityQualifier, - GlobalStateRequest, RecordId, SimulationRequest, + BinaryResponse, Command, GetRequest, GlobalStateEntityQualifier, GlobalStateRequest, RecordId, }; use casper_types::{ - bytesrepr::Bytes, evm, BlockHeader, Digest, GlobalStateIdentifier, KeyTag, PublicKey, - Timestamp, Transaction, TransactionV1, U256, + BlockHeader, Digest, GlobalStateIdentifier, KeyTag, PublicKey, Timestamp, Transaction, + TransactionV1, }; use crate::{ @@ -57,7 +56,6 @@ struct TestCase { allow_request_get_all_values: bool, allow_request_get_trie: bool, allow_request_speculative_exec: bool, - allow_request_simulate: bool, request_generator: Either Command, Command>, } @@ -69,7 +67,6 @@ async fn should_enqueue_requests_for_enabled_functions() { allow_request_get_all_values: ENABLED, allow_request_get_trie: rng.gen(), allow_request_speculative_exec: rng.gen(), - allow_request_simulate: rng.gen(), request_generator: Either::Left(|_| all_values_request()), }; @@ -77,7 +74,6 @@ async fn should_enqueue_requests_for_enabled_functions() { allow_request_get_all_values: rng.gen(), allow_request_get_trie: ENABLED, allow_request_speculative_exec: rng.gen(), - allow_request_simulate: rng.gen(), request_generator: Either::Left(|_| trie_request()), }; @@ -85,23 +81,13 @@ async fn should_enqueue_requests_for_enabled_functions() { allow_request_get_all_values: rng.gen(), allow_request_get_trie: rng.gen(), allow_request_speculative_exec: ENABLED, - allow_request_simulate: rng.gen(), request_generator: Either::Left(try_speculative_exec_request), }; - let simulate_evm_call_enabled = TestCase { - allow_request_get_all_values: rng.gen(), - allow_request_get_trie: rng.gen(), - allow_request_speculative_exec: rng.gen(), - allow_request_simulate: ENABLED, - request_generator: Either::Left(simulate_evm_call_request), - }; - for test_case in [ get_all_values_enabled, get_trie_enabled, try_speculative_exec_enabled, - simulate_evm_call_enabled, ] { let (_, mut runner) = run_test_case(test_case, &mut rng).await; @@ -125,7 +111,6 @@ async fn should_return_error_for_disabled_functions() { allow_request_get_all_values: DISABLED, allow_request_get_trie: rng.gen(), allow_request_speculative_exec: rng.gen(), - allow_request_simulate: rng.gen(), request_generator: Either::Left(|_| all_values_request()), }; @@ -133,7 +118,6 @@ async fn should_return_error_for_disabled_functions() { allow_request_get_all_values: rng.gen(), allow_request_get_trie: DISABLED, allow_request_speculative_exec: rng.gen(), - allow_request_simulate: rng.gen(), request_generator: Either::Left(|_| trie_request()), }; @@ -141,23 +125,13 @@ async fn should_return_error_for_disabled_functions() { allow_request_get_all_values: rng.gen(), allow_request_get_trie: rng.gen(), allow_request_speculative_exec: DISABLED, - allow_request_simulate: rng.gen(), request_generator: Either::Left(try_speculative_exec_request), }; - let simulate_evm_call_disabled = TestCase { - allow_request_get_all_values: rng.gen(), - allow_request_get_trie: rng.gen(), - allow_request_speculative_exec: rng.gen(), - allow_request_simulate: DISABLED, - request_generator: Either::Left(simulate_evm_call_request), - }; - for test_case in [ get_all_values_disabled, get_trie_disabled, try_speculative_exec_disabled, - simulate_evm_call_disabled, ] { let (receiver, mut runner) = run_test_case(test_case, &mut rng).await; @@ -175,32 +149,6 @@ async fn should_return_error_for_disabled_functions() { } } -#[tokio::test] -async fn should_return_error_for_unsupported_simulation_transaction() { - let mut rng = TestRng::new(); - let test_case = TestCase { - allow_request_get_all_values: rng.gen(), - allow_request_get_trie: rng.gen(), - allow_request_speculative_exec: rng.gen(), - allow_request_simulate: ENABLED, - request_generator: Either::Left(simulate_transaction_request), - }; - - let (receiver, mut runner) = run_test_case(test_case, &mut rng).await; - - let result = tokio::select! { - result = receiver => result.expect("expected successful response"), - _ = runner.crank_until( - &mut rng, - got_contract_runtime_request, - Duration::from_secs(10), - ) => { - panic!("expected receiver to complete first") - } - }; - assert_eq!(result.error_code(), ErrorCode::UnsupportedRequest as u16) -} - #[tokio::test] async fn should_return_empty_response_when_fetching_empty_key() { let mut rng = TestRng::new(); @@ -211,7 +159,6 @@ async fn should_return_empty_response_when_fetching_empty_key() { allow_request_get_all_values: DISABLED, allow_request_get_trie: DISABLED, allow_request_speculative_exec: DISABLED, - allow_request_simulate: DISABLED, request_generator: Either::Right(request), }) .collect(); @@ -239,7 +186,6 @@ async fn run_test_case( allow_request_get_all_values, allow_request_get_trie, allow_request_speculative_exec, - allow_request_simulate, request_generator, }: TestCase, rng: &mut TestRng, @@ -252,7 +198,6 @@ async fn run_test_case( allow_request_get_all_values, allow_request_get_trie, allow_request_speculative_exec, - allow_request_simulate, max_message_size_bytes: 1024, max_connections: 2, ..Default::default() @@ -506,24 +451,6 @@ fn try_speculative_exec_request(rng: &mut TestRng) -> Command { } } -fn simulate_evm_call_request(rng: &mut TestRng) -> Command { - Command::Simulate { - request: SimulationRequest::EvmCall(EvmCallRequest::new( - evm::Address::new(rng.gen()), - rng.gen::().then(|| evm::Address::new(rng.gen())), - U256::from_big_endian(&rng.gen::<[u8; 32]>()), - Bytes::from(rng.random_vec(0..64)), - rng.gen(), - )), - } -} - -fn simulate_transaction_request(rng: &mut TestRng) -> Command { - Command::Simulate { - request: SimulationRequest::Transaction(Transaction::V1(TransactionV1::random(rng))), - } -} - fn got_contract_runtime_request(event: &Event) -> bool { matches!(event, Event::ContractRuntimeRequest(_)) } diff --git a/node/src/components/contract_runtime.rs b/node/src/components/contract_runtime.rs index 9de7b36c35..ba5a36d364 100644 --- a/node/src/components/contract_runtime.rs +++ b/node/src/components/contract_runtime.rs @@ -76,7 +76,7 @@ use metrics::Metrics; #[cfg(test)] pub(crate) use operations::compute_execution_results_checksum; pub use operations::execute_finalized_block; -use operations::{evm_call, speculatively_execute}; +use operations::speculatively_execute; pub(crate) use types::{ BlockAndExecutionArtifacts, ExecutionArtifact, ExecutionPreState, SpeculativeExecutionResult, StepOutcome, @@ -722,6 +722,7 @@ impl ContractRuntime { } ContractRuntimeRequest::SpeculativelyExecute { block_header, + block_hashes, transaction, responder, } => { @@ -735,30 +736,8 @@ impl ContractRuntime { chainspec.as_ref(), execution_engine_v1.as_ref(), *block_header, - *transaction, - ) - }) - .await; - responder.respond(result).await - } - .ignore() - } - ContractRuntimeRequest::EvmCall { - block_header, - block_hashes, - request, - responder, - } => { - let chainspec = Arc::clone(&self.chainspec); - let data_access_layer = Arc::clone(&self.data_access_layer); - async move { - let result = run_intensive_task(move || { - evm_call( - data_access_layer.as_ref(), - chainspec.as_ref(), - *block_header, block_hashes, - *request, + *transaction, ) }) .await; diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 11850be845..cd2d8410fd 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -6,15 +6,14 @@ use std::{collections::BTreeMap, convert::TryInto, sync::Arc, time::Instant}; use tracing::{debug, error, info, trace, warn}; use wasm_v2_request::{WasmV2Request, WasmV2Result}; -use casper_binary_port::{EvmCallRequest, EvmCallResult}; use casper_execution_engine::engine_state::{ BlockInfo, ExecutionEngineV1, WasmV1Request, WasmV1Result, }; use casper_executor_evm::{ BlockContext as EvmBlockContext, BlockHashProvider as EvmBlockHashProvider, - BlockHashProviderResult as EvmBlockHashProviderResult, EvmExecutor, - ExecuteKind as EvmExecuteKind, ExecuteRequest as EvmExecuteRequest, - ExecutionStatus as EvmExecutionStatus, + BlockHashProviderResult as EvmBlockHashProviderResult, CallRequest as EvmExecutorCallRequest, + CallValidation as EvmCallValidation, EvmExecutor, ExecuteKind as EvmExecuteKind, + ExecuteRequest as EvmExecuteRequest, ExecutionStatus as EvmExecutionStatus, }; use casper_storage::{ block_store::types::ApprovalsHashes, @@ -40,7 +39,7 @@ use casper_storage::{ }; use casper_types::{ account::{Account, AccountHash}, - bytesrepr::{self, ToBytes, U32_SERIALIZED_LENGTH}, + bytesrepr::{self, Bytes, ToBytes, U32_SERIALIZED_LENGTH}, contracts::NamedKeys, evm::{ Address as EvmAddress, HaltReason as EvmHaltReason, Receipt as EvmReceipt, @@ -1830,6 +1829,7 @@ pub(super) fn speculatively_execute( chainspec: &Chainspec, execution_engine_v1: &ExecutionEngineV1, block_header: BlockHeader, + block_hashes: BTreeMap, input_transaction: Transaction, ) -> SpeculativeExecutionResult where @@ -1856,14 +1856,15 @@ where let block_time = block_header .timestamp() .saturating_add(chainspec.core_config.minimum_block_time); - let gas_limit = match input_transaction.gas_limit(chainspec, transaction.transaction_lane()) { - Ok(gas_limit) => gas_limit, - Err(_) => { - return SpeculativeExecutionResult::invalid_gas_limit(input_transaction); - } - }; if transaction.is_deploy_transaction() { + let gas_limit = match input_transaction.gas_limit(chainspec, transaction.transaction_lane()) + { + Ok(gas_limit) => gas_limit, + Err(_) => { + return SpeculativeExecutionResult::invalid_gas_limit(input_transaction); + } + }; if transaction.is_native() { let limit = Gas::from(chainspec.system_costs_config.mint_costs().transfer); let protocol_version = chainspec.protocol_version(); @@ -1917,6 +1918,13 @@ where ))) } } else if transaction.is_wasm() { + let gas_limit = match input_transaction.gas_limit(chainspec, transaction.transaction_lane()) + { + Ok(gas_limit) => gas_limit, + Err(_) => { + return SpeculativeExecutionResult::invalid_gas_limit(input_transaction); + } + }; let block_info = BlockInfo::new( *state_root_hash, block_time.into(), @@ -1937,6 +1945,14 @@ where wasm_v1_result, block_header.block_hash(), ))) + } else if let Some(evm_transaction) = transaction.as_evm() { + speculatively_execute_evm( + state_provider, + chainspec, + block_header, + block_hashes, + evm_transaction, + ) } else { // TODO: placeholder error SpeculativeExecutionResult::InvalidTransaction(InvalidTransaction::V1( @@ -1945,26 +1961,48 @@ where } } -/// Executes a read-only EVM call against a checked-out block state. -pub(super) fn evm_call( +fn speculatively_execute_evm( state_provider: &S, chainspec: &Chainspec, block_header: BlockHeader, block_hashes: BTreeMap, - request: EvmCallRequest, -) -> Result + evm_transaction: &casper_types::evm::Transaction, +) -> SpeculativeExecutionResult where S: StateProvider, { if !chainspec.evm_config.enabled { - return Err("EVM execution is disabled".to_string()); + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Disabled, + )); + } + if evm_transaction.gas_limit() > chainspec.evm_config.block_gas_limit { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit: evm_transaction.gas_limit(), + block_gas_limit: chainspec.evm_config.block_gas_limit, + }, + )); } let state_root_hash = block_header.state_root_hash(); - let mut tracking_copy = state_provider - .tracking_copy(*state_root_hash) - .map_err(|error| format!("failed to check out EVM call state: {error}"))? - .ok_or_else(|| format!("state root {state_root_hash} not found"))?; + let mut tracking_copy = match state_provider.tracking_copy(*state_root_hash) { + Ok(Some(tracking_copy)) => tracking_copy, + Ok(None) => { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Decode(format!( + "state root {state_root_hash} not found" + )), + )) + } + Err(error) => { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Decode(format!( + "failed to check out EVM speculative execution state: {error}" + )), + )) + } + }; let block_time = block_header .timestamp() .saturating_add(chainspec.core_config.minimum_block_time); @@ -1975,29 +2013,55 @@ where gas_limit: Some(chainspec.evm_config.block_gas_limit), base_fee: Some(chainspec.evm_config.base_fee), }; - let call = casper_executor_evm::CallRequest { - from: request.from(), - to: request.to(), - value: request.value(), - input: request.input().to_vec(), - gas_limit: request.gas_limit(), - gas_price: u128::from(chainspec.evm_config.base_fee), - nonce: 0, - validation: casper_executor_evm::CallValidation::UncheckedSimulation, + let kind = if evm_transaction.is_unsigned_call() { + EvmExecuteKind::Call(EvmExecutorCallRequest { + from: evm_transaction.from(), + to: evm_transaction.to(), + value: evm_transaction.value(), + input: evm_transaction.input().to_vec(), + gas_limit: evm_transaction.gas_limit(), + gas_price: u128::from(chainspec.evm_config.base_fee), + nonce: evm_transaction.nonce(), + validation: EvmCallValidation::UncheckedSimulation, + }) + } else { + EvmExecuteKind::Transaction(evm_transaction.clone()) }; let execute_request = EvmExecuteRequest { block: block_context, - kind: EvmExecuteKind::Call(call), + kind, }; let block_hash_provider = StaticEvmBlockHashProvider { block_hashes }; - let outcome = EvmExecutor::new(chainspec.evm_config) - .execute_with_block_hash_provider(&mut tracking_copy, execute_request, &block_hash_provider) - .map_err(|error| error.to_string())?; - let receipt = outcome.to_receipt(0); - Ok(EvmCallResult::new( - receipt.status, - outcome.output.into(), - outcome.gas_used, + let outcome = match EvmExecutor::new(chainspec.evm_config).execute_with_block_hash_provider( + &mut tracking_copy, + execute_request, + &block_hash_provider, + ) { + Ok(outcome) => outcome, + Err(error) => { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Decode(error.to_string()), + )) + } + }; + let effects = tracking_copy.effects(); + let effective_gas_price = if evm_transaction.is_unsigned_call() { + u128::from(chainspec.evm_config.base_fee) + } else { + evm_transaction.effective_gas_price(chainspec.evm_config.base_fee) + }; + let receipt = outcome.to_receipt(effective_gas_price); + let error = receipt.status.message().map(str::to_string); + SpeculativeExecutionResult::Evm(Box::new( + casper_binary_port::EvmSpeculativeExecutionResult::new( + block_header.block_hash(), + Gas::new(evm_transaction.gas_limit()), + Gas::new(outcome.gas_used), + effects, + error, + receipt, + Bytes::from(outcome.output), + ), )) } diff --git a/node/src/components/contract_runtime/types.rs b/node/src/components/contract_runtime/types.rs index 5505e805c0..2a296c8cad 100644 --- a/node/src/components/contract_runtime/types.rs +++ b/node/src/components/contract_runtime/types.rs @@ -601,6 +601,7 @@ pub struct BlockAndExecutionArtifacts { pub enum SpeculativeExecutionResult { InvalidTransaction(InvalidTransaction), WasmV1(Box), + Evm(Box), } impl SpeculativeExecutionResult { diff --git a/node/src/components/network/tasks.rs b/node/src/components/network/tasks.rs index 9700ddbbb6..5e08c9db54 100644 --- a/node/src/components/network/tasks.rs +++ b/node/src/components/network/tasks.rs @@ -685,111 +685,110 @@ where let demands_in_flight = Arc::new(Semaphore::new(context.max_in_flight_demands)); let event_queue = context.event_queue.expect("component not initialized"); - let read_messages = async move { - while let Some(msg_result) = stream.next().await { - match msg_result { - Ok(msg) => { - trace!(%msg, "message received"); - - let effect_builder = EffectBuilder::new(event_queue); - - match msg.try_into_demand(effect_builder, peer_id) { - Ok((event, wait_for_response)) => { - // Note: For now, demands bypass the limiter, as we expect the - // backpressure to handle this instead. - - // Acquire a permit. If we are handling too many demands at this - // time, this will block, halting the processing of new message, - // thus letting the peer they have reached their maximum allowance. - let in_flight = demands_in_flight - .clone() - .acquire_owned() - .await - // Note: Since the semaphore is reference counted, it must - // explicitly be closed for acquisition to fail, which we - // never do. If this happens, there is a bug in the code; - // we exit with an error and close the connection. - .map_err(|_| { - io::Error::new( - io::ErrorKind::Other, - "demand limiter semaphore closed unexpectedly", - ) - })?; - - Metrics::record_trie_request_start(&context.net_metrics); - - let net_metrics = context.net_metrics.clone(); - // Spawn a future that will eventually send the returned message. It - // will essentially buffer the response. - tokio::spawn(async move { - if let Some(payload) = wait_for_response.await { - // Send message and await its return. `send_message` should - // only return when the message has been buffered, if the - // peer is not accepting data, we will block here until the - // send buffer has sufficient room. - effect_builder.send_message(peer_id, payload).await; - - // Note: We could short-circuit the event queue here and - // directly insert into the outgoing message queue, - // which may be potential performance improvement. - } - - // Missing else: The handler of the demand did not deem it - // worthy a response. Just drop it. - - // After we have either successfully buffered the message for - // sending, failed to do so or did not have a message to send - // out, we consider the request handled and free up the permit. - Metrics::record_trie_request_end(&net_metrics); - drop(in_flight); - }); - - // Schedule the created event. - event_queue - .schedule::(event, QueueKind::NetworkDemand) - .await; - } - Err(msg) => { - // We've received a non-demand message. Ensure we have the proper amount - // of resources, then push it to the reactor. - limiter - .request_allowance( - msg.payload_incoming_resource_estimate( + let read_messages = + async move { + while let Some(msg_result) = stream.next().await { + match msg_result { + Ok(msg) => { + trace!(%msg, "message received"); + + let effect_builder = EffectBuilder::new(event_queue); + + match msg.try_into_demand(effect_builder, peer_id) { + Ok((event, wait_for_response)) => { + // Note: For now, demands bypass the limiter, as we expect the + // backpressure to handle this instead. + + // Acquire a permit. If we are handling too many demands at this + // time, this will block, halting the processing of new message, + // thus letting the peer they have reached their maximum allowance. + let in_flight = demands_in_flight + .clone() + .acquire_owned() + .await + // Note: Since the semaphore is reference counted, it must + // explicitly be closed for acquisition to fail, which we + // never do. If this happens, there is a bug in the code; + // we exit with an error and close the connection. + .map_err(|_| { + io::Error::new( + io::ErrorKind::Other, + "demand limiter semaphore closed unexpectedly", + ) + })?; + + Metrics::record_trie_request_start(&context.net_metrics); + + let net_metrics = context.net_metrics.clone(); + // Spawn a future that will eventually send the returned message. It + // will essentially buffer the response. + tokio::spawn(async move { + if let Some(payload) = wait_for_response.await { + // Send message and await its return. `send_message` should + // only return when the message has been buffered, if the + // peer is not accepting data, we will block here until the + // send buffer has sufficient room. + effect_builder.send_message(peer_id, payload).await; + + // Note: We could short-circuit the event queue here and + // directly insert into the outgoing message queue, + // which may be potential performance improvement. + } + + // Missing else: The handler of the demand did not deem it + // worthy a response. Just drop it. + + // After we have either successfully buffered the message for + // sending, failed to do so or did not have a message to send + // out, we consider the request handled and free up the permit. + Metrics::record_trie_request_end(&net_metrics); + drop(in_flight); + }); + + // Schedule the created event. + event_queue + .schedule::(event, QueueKind::NetworkDemand) + .await; + } + Err(msg) => { + // We've received a non-demand message. Ensure we have the proper amount + // of resources, then push it to the reactor. + limiter + .request_allowance(msg.payload_incoming_resource_estimate( &context.payload_weights, - ), - ) - .await; - - let queue_kind = if msg.is_low_priority() { - QueueKind::NetworkLowPriority - } else { - QueueKind::NetworkIncoming - }; - - event_queue - .schedule( - Event::IncomingMessage { - peer_id: Box::new(peer_id), - msg, - span: span.clone(), - }, - queue_kind, - ) - .await; + )) + .await; + + let queue_kind = if msg.is_low_priority() { + QueueKind::NetworkLowPriority + } else { + QueueKind::NetworkIncoming + }; + + event_queue + .schedule( + Event::IncomingMessage { + peer_id: Box::new(peer_id), + msg, + span: span.clone(), + }, + queue_kind, + ) + .await; + } } } - } - Err(err) => { - warn!( - err = display_error(&err), - "receiving message failed, closing connection" - ); - return Err(err); + Err(err) => { + warn!( + err = display_error(&err), + "receiving message failed, closing connection" + ); + return Err(err); + } } } - } - Ok(()) - }; + Ok(()) + }; let shutdown_messages = async move { while close_incoming_receiver.changed().await.is_ok() {} }; diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index 0522feea99..d4e6506fb7 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -236,6 +236,20 @@ impl TransactionAcceptor { return self.reject_transaction(effect_builder, *event_metadata, error); } + if event_metadata + .meta_transaction + .as_evm() + .is_some_and(|evm_transaction| evm_transaction.is_unsigned_call()) + { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::MissingApproval, + )), + ); + } + // We only perform expiry checks on transactions received from the client. let current_node_timestamp = event_metadata.verification_start_timestamp; if event_metadata.source.is_client() diff --git a/node/src/effect.rs b/node/src/effect.rs index 770f7972d5..4263b38162 100644 --- a/node/src/effect.rs +++ b/node/src/effect.rs @@ -2306,6 +2306,7 @@ impl EffectBuilder { pub(crate) async fn speculatively_execute( self, block_header: Box, + block_hashes: BTreeMap, transaction: Box, ) -> SpeculativeExecutionResult where @@ -2313,30 +2314,9 @@ impl EffectBuilder { { self.make_request( |responder| ContractRuntimeRequest::SpeculativelyExecute { - block_header, - transaction, - responder, - }, - QueueKind::ContractRuntime, - ) - .await - } - - /// Requests a read-only EVM call, without committing its effects. - pub(crate) async fn evm_call( - self, - block_header: Box, - block_hashes: BTreeMap, - request: Box, - ) -> Result - where - REv: From, - { - self.make_request( - |responder| ContractRuntimeRequest::EvmCall { block_header, block_hashes, - request, + transaction, responder, }, QueueKind::ContractRuntime, diff --git a/node/src/effect/requests.rs b/node/src/effect/requests.rs index d95181505c..5713fe989c 100644 --- a/node/src/effect/requests.rs +++ b/node/src/effect/requests.rs @@ -16,8 +16,7 @@ use smallvec::SmallVec; use static_assertions::const_assert; use casper_binary_port::{ - ConsensusStatus, ConsensusValidatorChanges, EvmCallRequest, EvmCallResult, LastProgress, - NetworkName, RecordId, Uptime, + ConsensusStatus, ConsensusValidatorChanges, LastProgress, NetworkName, RecordId, Uptime, }; use casper_storage::{ block_store::types::ApprovalsHashes, @@ -879,22 +878,13 @@ pub(crate) enum ContractRuntimeRequest { SpeculativelyExecute { /// Pre-state. block_header: Box, + /// Recent block hashes available to the EVM `BLOCKHASH` opcode. + block_hashes: BTreeMap, /// Transaction to execute. transaction: Box, /// Results responder: Responder, }, - /// Execute a read-only EVM call without committing effects. - EvmCall { - /// Pre-state. - block_header: Box, - /// Recent block hashes available to the EVM `BLOCKHASH` opcode. - block_hashes: BTreeMap, - /// EVM call request. - request: Box, - /// Result. - responder: Responder>, - }, UpdateRuntimePrice(EraId, u8), GetEraGasPrice { era_id: EraId, @@ -984,17 +974,6 @@ impl Display for ContractRuntimeRequest { block_header.state_root_hash() ) } - ContractRuntimeRequest::EvmCall { - request, - block_header, - .. - } => write!( - formatter, - "Execute EVM call from {} to {:?} on {}", - request.from(), - request.to(), - block_header.state_root_hash() - ), ContractRuntimeRequest::UpdateRuntimePrice(_, era_gas_price) => { write!(formatter, "updating price to {}", era_gas_price) } diff --git a/node/src/reactor/main_reactor/tests/fixture.rs b/node/src/reactor/main_reactor/tests/fixture.rs index 82f2b9e377..106aaa5cee 100644 --- a/node/src/reactor/main_reactor/tests/fixture.rs +++ b/node/src/reactor/main_reactor/tests/fixture.rs @@ -402,7 +402,6 @@ impl TestFixture { allow_request_get_all_values: true, allow_request_get_trie: true, allow_request_speculative_exec: true, - allow_request_simulate: true, ..Default::default() }, ..Default::default() diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index 880a6d6162..42a7f6ae8d 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -667,6 +667,46 @@ mod tests { )); } + #[test] + fn evm_config_compliance_accepts_unsigned_call() { + let chainspec = chainspec(); + let meta = evm_meta(&chainspec, unsigned_call(CHAIN_ID, BASE_FEE.into(), 21_000)); + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()) + .expect("unsigned EVM call should be config compliant"); + } + + #[test] + fn evm_config_compliance_rejects_unsigned_call_mismatched_chain_id() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + unsigned_call(CHAIN_ID + 1, BASE_FEE.into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::ChainIdMismatch { + expected: CHAIN_ID, + actual + })) if actual == CHAIN_ID + 1 + )); + } + + #[test] + fn evm_config_compliance_rejects_unsigned_call_gas_price_below_base_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + unsigned_call(CHAIN_ID, u128::from(BASE_FEE - 1), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee + })) if gas_price == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + #[test] fn evm_config_compliance_rejects_max_fee_below_base_fee() { let chainspec = chainspec(); @@ -834,6 +874,20 @@ mod tests { .expect("EVM transaction metadata should be created") } + fn unsigned_call(chain_id: u64, gas_price: u128, gas_limit: u64) -> evm::Transaction { + evm::Transaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(60), + chain_id, + evm::Address::new([1u8; 20]), + Some(evm::Address::new([2u8; 20])), + casper_types::U256::zero(), + Default::default(), + gas_limit, + gas_price, + ) + } + fn legacy_transaction( chain_id: Option, gas_price: u128, diff --git a/node/src/types/transaction/meta_transaction/meta_evm.rs b/node/src/types/transaction/meta_transaction/meta_evm.rs index 58581114b4..5dd4a504d6 100644 --- a/node/src/types/transaction/meta_transaction/meta_evm.rs +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -97,7 +97,9 @@ impl MetaEvmTransaction { return Err(evm::TransactionError::Disabled); } - transaction.verify()?; + if !transaction.is_unsigned_call() { + transaction.verify()?; + } let expected = evm_config.chain_id; let actual = transaction diff --git a/resources/integration-test/config-example.toml b/resources/integration-test/config-example.toml index e4e2a997ae..72a782ef34 100644 --- a/resources/integration-test/config-example.toml +++ b/resources/integration-test/config-example.toml @@ -334,9 +334,6 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false -# Flag that enables the `Simulate` request. Disabled by default. -allow_request_simulate = false - # Maximum size of a message in bytes. max_message_size_bytes = 134_217_728 @@ -375,9 +372,6 @@ accept_transaction_request_termination_delay = '24 seconds' speculative_exec_request_termination_delay = '0 seconds' #The amount of time which is given to a connection to extend it's lifetime when a valid -#[`Command::Simulate`] is sent to the node -simulate_request_termination_delay = '0 seconds' - # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/local/config.toml b/resources/local/config.toml index 959ba00345..d7692ec4ed 100644 --- a/resources/local/config.toml +++ b/resources/local/config.toml @@ -335,9 +335,6 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false -# Flag that enables the `Simulate` request. Disabled by default. -allow_request_simulate = false - # Maximum size of a message in bytes. max_message_size_bytes = 4_194_304 @@ -376,9 +373,6 @@ accept_transaction_request_termination_delay = '24 seconds' speculative_exec_request_termination_delay = '0 seconds' #The amount of time which is given to a connection to extend it's lifetime when a valid -#[`Command::Simulate`] is sent to the node -simulate_request_termination_delay = '0 seconds' - # ============================================== # Configuration options for the REST HTTP server # ============================================== diff --git a/resources/mainnet/config-example.toml b/resources/mainnet/config-example.toml index 958658989a..dba2fa6f17 100644 --- a/resources/mainnet/config-example.toml +++ b/resources/mainnet/config-example.toml @@ -334,9 +334,6 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false -# Flag that enables the `Simulate` request. Disabled by default. -allow_request_simulate = false - # Maximum size of a message in bytes. max_message_size_bytes = 134_217_728 @@ -375,9 +372,6 @@ accept_transaction_request_termination_delay = '24 seconds' speculative_exec_request_termination_delay = '0 seconds' #The amount of time which is given to a connection to extend it's lifetime when a valid -#[`Command::Simulate`] is sent to the node -simulate_request_termination_delay = '0 seconds' - # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/production/config-example.toml b/resources/production/config-example.toml index 4efe3e3117..861697c3f7 100644 --- a/resources/production/config-example.toml +++ b/resources/production/config-example.toml @@ -334,9 +334,6 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false -# Flag that enables the `Simulate` request. Disabled by default. -allow_request_simulate = false - # Maximum size of a message in bytes. max_message_size_bytes = 134_217_728 @@ -375,9 +372,6 @@ accept_transaction_request_termination_delay = '24 seconds' speculative_exec_request_termination_delay = '0 seconds' #The amount of time which is given to a connection to extend it's lifetime when a valid -#[`Command::Simulate`] is sent to the node -simulate_request_termination_delay = '0 seconds' - # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/testnet/config-example.toml b/resources/testnet/config-example.toml index bd957db868..b69e0900f8 100644 --- a/resources/testnet/config-example.toml +++ b/resources/testnet/config-example.toml @@ -334,9 +334,6 @@ allow_request_get_trie = false # Flag that enables the `TrySpeculativeExec` request. Disabled by default. allow_request_speculative_exec = false -# Flag that enables the `Simulate` request. Disabled by default. -allow_request_simulate = false - # Maximum size of a message in bytes. max_message_size_bytes = 134_217_728 @@ -375,9 +372,6 @@ accept_transaction_request_termination_delay = '24 seconds' speculative_exec_request_termination_delay = '0 seconds' #The amount of time which is given to a connection to extend it's lifetime when a valid -#[`Command::Simulate`] is sent to the node -simulate_request_termination_delay = '0 seconds' - # ============================================== # Configuration options for the REST HTTP server diff --git a/storage/src/system/transfer.rs b/storage/src/system/transfer.rs index 10e39c2510..fd1be494d7 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -485,28 +485,27 @@ impl TransferRuntimeArgsBuilder { where R: StateReader, { - let (to, target) = match self - .resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))? - { - TransferTargetMode::ExistingAccount { - main_purse: purse_uref, - target_account_hash: target_account, - } => (Some(target_account), purse_uref), - TransferTargetMode::ExistingEvmAccount { - main_purse: purse_uref, - .. - } => (None, purse_uref), - TransferTargetMode::PurseExists { - target_account_hash, - purse_uref, - } => (target_account_hash, purse_uref), - TransferTargetMode::CreateAccount(_) | TransferTargetMode::CreateEvmAccount(_) => { - // Method "build()" is called after `resolve_transfer_target_mode` is first called - // and handled by creating a new account. Calling `resolve_transfer_target_mode` - // for the second time should never return `CreateAccount` variant. - return Err(TransferError::InvalidOperation); - } - }; + let (to, target) = + match self.resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))? { + TransferTargetMode::ExistingAccount { + main_purse: purse_uref, + target_account_hash: target_account, + } => (Some(target_account), purse_uref), + TransferTargetMode::ExistingEvmAccount { + main_purse: purse_uref, + .. + } => (None, purse_uref), + TransferTargetMode::PurseExists { + target_account_hash, + purse_uref, + } => (target_account_hash, purse_uref), + TransferTargetMode::CreateAccount(_) | TransferTargetMode::CreateEvmAccount(_) => { + // Method "build()" is called after `resolve_transfer_target_mode` is first called + // and handled by creating a new account. Calling `resolve_transfer_target_mode` + // for the second time should never return `CreateAccount` variant. + return Err(TransferError::InvalidOperation); + } + }; let source = self.resolve_source_uref(from, Rc::clone(&tracking_copy))?; diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index afef12ef12..c6148fb741 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -625,6 +625,93 @@ impl<'de> Deserialize<'de> for Transaction { } impl Transaction { + /// Constructs an unsigned EVM call transaction for speculative execution. + /// + /// This is intended for read-only `eth_call` style execution through the + /// node's speculative execution path, so it intentionally carries no + /// approvals and should not be accepted as a network transaction. + /// The chain ID and gas price are still part of the marker payload so the + /// node can enforce EVM configuration compliance before execution. + #[allow(clippy::too_many_arguments)] + pub fn new_unsigned_call( + timestamp: Timestamp, + ttl: TimeDiff, + chain_id: u64, + from: Address, + to: Option
, + value: U256, + input: Vec, + gas_limit: u64, + gas_price: u128, + ) -> Self { + let mut transaction = Transaction { + timestamp, + ttl, + hash: TransactionHash::default(), + from, + kind: TransactionKind::Legacy, + to, + nonce: 0, + gas_limit, + gas_price: Some(gas_price), + max_fee_per_gas: 0, + max_priority_fee_per_gas: None, + value, + input, + chain_id: Some(chain_id), + authorization_list: Vec::new(), + approvals: BTreeSet::new(), + }; + transaction.hash = transaction.unsigned_call_hash(); + transaction + } + + fn unsigned_call_hash(&self) -> TransactionHash { + let mut bytes = Vec::new(); + bytes.extend_from_slice(b"casper-evm-call"); + self.timestamp + .write_bytes(&mut bytes) + .expect("timestamp should serialize"); + self.ttl + .write_bytes(&mut bytes) + .expect("ttl should serialize"); + self.from + .write_bytes(&mut bytes) + .expect("from address should serialize"); + self.to + .write_bytes(&mut bytes) + .expect("to address should serialize"); + self.value + .write_bytes(&mut bytes) + .expect("value should serialize"); + Bytes::from(self.input.clone()) + .write_bytes(&mut bytes) + .expect("input should serialize"); + self.gas_limit + .write_bytes(&mut bytes) + .expect("gas limit should serialize"); + self.gas_price + .write_bytes(&mut bytes) + .expect("gas price should serialize"); + self.chain_id + .write_bytes(&mut bytes) + .expect("chain ID should serialize"); + TransactionHash::new(Digest::hash(bytes)) + } + + /// Returns `true` if this is an unsigned read-only call transaction. + pub fn is_unsigned_call(&self) -> bool { + self.approvals.is_empty() + && self.hash == self.unsigned_call_hash() + && self.kind == TransactionKind::Legacy + && self.nonce == 0 + && self.gas_price.is_some() + && self.max_fee_per_gas == 0 + && self.max_priority_fee_per_gas.is_none() + && self.chain_id.is_some() + && self.authorization_list.is_empty() + } + /// Decodes a signed Ethereum RLP transaction into an unsigned payload plus approval. pub fn from_signed_rlp( raw_signed_rlp: Vec, @@ -1170,9 +1257,11 @@ impl FromBytes for Transaction { authorization_list, approvals, }; - transaction - .verify() - .map_err(|_| bytesrepr::Error::Formatting)?; + if !transaction.is_unsigned_call() { + transaction + .verify() + .map_err(|_| bytesrepr::Error::Formatting)?; + } Ok((transaction, remainder)) } } @@ -1298,6 +1387,54 @@ mod tests { .contains("unexpected EVM set-code authorization list")); } + #[test] + fn unsigned_call_transaction_bytesrepr_roundtrips_without_approvals() { + let transaction = Transaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(300), + 7, + Address::new([1; crate::evm::ADDRESS_LENGTH]), + Some(Address::new([2; crate::evm::ADDRESS_LENGTH])), + U256::from(3), + vec![0xde, 0xad], + 1_000, + 1, + ); + + assert!(transaction.approvals().is_empty()); + assert!(transaction.is_unsigned_call()); + assert!(matches!( + transaction.verify(), + Err(TransactionError::MissingApproval) + )); + bytesrepr::test_serialization_roundtrip(&transaction); + } + + #[test] + fn non_marker_unsigned_transaction_bytesrepr_is_rejected() { + let mut transaction = Transaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(300), + 7, + Address::new([1; crate::evm::ADDRESS_LENGTH]), + Some(Address::new([2; crate::evm::ADDRESS_LENGTH])), + U256::from(3), + vec![0xde, 0xad], + 1_000, + 1, + ); + transaction.gas_price = Some(2); + + assert!(transaction.approvals().is_empty()); + assert!(!transaction.is_unsigned_call()); + assert!(Transaction::from_bytes( + &transaction + .to_bytes() + .expect("transaction should serialize") + ) + .is_err()); + } + fn signed_legacy_transaction() -> Transaction { let tx = TxLegacy { chain_id: Some(7), From b7c3495b45a367d2349238988d12150b6b15c1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Tue, 26 May 2026 14:16:54 +0200 Subject: [PATCH 16/17] Reject native transfers to EVM contract addresses --- node/src/components/network/tasks.rs | 199 +++++++++--------- .../main_reactor/tests/transactions.rs | 115 ++++++++++ storage/src/system/transfer.rs | 78 +++++-- 3 files changed, 272 insertions(+), 120 deletions(-) diff --git a/node/src/components/network/tasks.rs b/node/src/components/network/tasks.rs index 5e08c9db54..9700ddbbb6 100644 --- a/node/src/components/network/tasks.rs +++ b/node/src/components/network/tasks.rs @@ -685,110 +685,111 @@ where let demands_in_flight = Arc::new(Semaphore::new(context.max_in_flight_demands)); let event_queue = context.event_queue.expect("component not initialized"); - let read_messages = - async move { - while let Some(msg_result) = stream.next().await { - match msg_result { - Ok(msg) => { - trace!(%msg, "message received"); - - let effect_builder = EffectBuilder::new(event_queue); - - match msg.try_into_demand(effect_builder, peer_id) { - Ok((event, wait_for_response)) => { - // Note: For now, demands bypass the limiter, as we expect the - // backpressure to handle this instead. - - // Acquire a permit. If we are handling too many demands at this - // time, this will block, halting the processing of new message, - // thus letting the peer they have reached their maximum allowance. - let in_flight = demands_in_flight - .clone() - .acquire_owned() - .await - // Note: Since the semaphore is reference counted, it must - // explicitly be closed for acquisition to fail, which we - // never do. If this happens, there is a bug in the code; - // we exit with an error and close the connection. - .map_err(|_| { - io::Error::new( - io::ErrorKind::Other, - "demand limiter semaphore closed unexpectedly", - ) - })?; - - Metrics::record_trie_request_start(&context.net_metrics); - - let net_metrics = context.net_metrics.clone(); - // Spawn a future that will eventually send the returned message. It - // will essentially buffer the response. - tokio::spawn(async move { - if let Some(payload) = wait_for_response.await { - // Send message and await its return. `send_message` should - // only return when the message has been buffered, if the - // peer is not accepting data, we will block here until the - // send buffer has sufficient room. - effect_builder.send_message(peer_id, payload).await; - - // Note: We could short-circuit the event queue here and - // directly insert into the outgoing message queue, - // which may be potential performance improvement. - } - - // Missing else: The handler of the demand did not deem it - // worthy a response. Just drop it. - - // After we have either successfully buffered the message for - // sending, failed to do so or did not have a message to send - // out, we consider the request handled and free up the permit. - Metrics::record_trie_request_end(&net_metrics); - drop(in_flight); - }); - - // Schedule the created event. - event_queue - .schedule::(event, QueueKind::NetworkDemand) - .await; - } - Err(msg) => { - // We've received a non-demand message. Ensure we have the proper amount - // of resources, then push it to the reactor. - limiter - .request_allowance(msg.payload_incoming_resource_estimate( - &context.payload_weights, - )) - .await; - - let queue_kind = if msg.is_low_priority() { - QueueKind::NetworkLowPriority - } else { - QueueKind::NetworkIncoming - }; - - event_queue - .schedule( - Event::IncomingMessage { - peer_id: Box::new(peer_id), - msg, - span: span.clone(), - }, - queue_kind, + let read_messages = async move { + while let Some(msg_result) = stream.next().await { + match msg_result { + Ok(msg) => { + trace!(%msg, "message received"); + + let effect_builder = EffectBuilder::new(event_queue); + + match msg.try_into_demand(effect_builder, peer_id) { + Ok((event, wait_for_response)) => { + // Note: For now, demands bypass the limiter, as we expect the + // backpressure to handle this instead. + + // Acquire a permit. If we are handling too many demands at this + // time, this will block, halting the processing of new message, + // thus letting the peer they have reached their maximum allowance. + let in_flight = demands_in_flight + .clone() + .acquire_owned() + .await + // Note: Since the semaphore is reference counted, it must + // explicitly be closed for acquisition to fail, which we + // never do. If this happens, there is a bug in the code; + // we exit with an error and close the connection. + .map_err(|_| { + io::Error::new( + io::ErrorKind::Other, + "demand limiter semaphore closed unexpectedly", ) - .await; - } + })?; + + Metrics::record_trie_request_start(&context.net_metrics); + + let net_metrics = context.net_metrics.clone(); + // Spawn a future that will eventually send the returned message. It + // will essentially buffer the response. + tokio::spawn(async move { + if let Some(payload) = wait_for_response.await { + // Send message and await its return. `send_message` should + // only return when the message has been buffered, if the + // peer is not accepting data, we will block here until the + // send buffer has sufficient room. + effect_builder.send_message(peer_id, payload).await; + + // Note: We could short-circuit the event queue here and + // directly insert into the outgoing message queue, + // which may be potential performance improvement. + } + + // Missing else: The handler of the demand did not deem it + // worthy a response. Just drop it. + + // After we have either successfully buffered the message for + // sending, failed to do so or did not have a message to send + // out, we consider the request handled and free up the permit. + Metrics::record_trie_request_end(&net_metrics); + drop(in_flight); + }); + + // Schedule the created event. + event_queue + .schedule::(event, QueueKind::NetworkDemand) + .await; + } + Err(msg) => { + // We've received a non-demand message. Ensure we have the proper amount + // of resources, then push it to the reactor. + limiter + .request_allowance( + msg.payload_incoming_resource_estimate( + &context.payload_weights, + ), + ) + .await; + + let queue_kind = if msg.is_low_priority() { + QueueKind::NetworkLowPriority + } else { + QueueKind::NetworkIncoming + }; + + event_queue + .schedule( + Event::IncomingMessage { + peer_id: Box::new(peer_id), + msg, + span: span.clone(), + }, + queue_kind, + ) + .await; } - } - Err(err) => { - warn!( - err = display_error(&err), - "receiving message failed, closing connection" - ); - return Err(err); } } + Err(err) => { + warn!( + err = display_error(&err), + "receiving message failed, closing connection" + ); + return Err(err); + } } - Ok(()) - }; + } + Ok(()) + }; let shutdown_messages = async move { while close_incoming_receiver.changed().await.is_ok() {} }; diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index f03576f4cb..dada9399fd 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -1015,6 +1015,34 @@ fn evm_identity_at(fixture: &mut TestFixture, block_height: u64, address: evm::A } } +fn evm_code_hash_at( + fixture: &mut TestFixture, + block_height: u64, + address: evm::Address, +) -> evm::Hash { + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let block_header = runner + .main_reactor() + .storage() + .read_block_header_by_height(block_height, true) + .expect("failure to read block header") + .expect("should have header"); + let state_root_hash = *block_header.state_root_hash(); + match query_global_state( + fixture, + state_root_hash, + Key::Evm(evm::EvmAddr::CodeHash(address)), + ) { + Some(value) => match *value { + StoredValue::CLValue(cl_value) => cl_value + .into_t::() + .expect("EVM code hash should decode"), + value => panic!("expected EVM code hash, got {value:?}"), + }, + None => EMPTY_CODE_HASH, + } +} + fn evm_balance(fixture: &mut TestFixture, address: evm::Address, block_height: u64) -> U512 { let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); let protocol_version = fixture.chainspec.protocol_version(); @@ -1412,6 +1440,93 @@ async fn should_transfer_to_evm_address_with_native_transfer() { assert_eq!(transfer.amount, U512::from(transfer_amount)); } +#[tokio::test] +async fn should_reject_native_transfer_to_evm_contract_address() { + let evm_config = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_pricing_handling(PricingHandling::Fixed) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let evm_transaction = signed_evm_deploy_transaction(evm_config.chain_id); + let evm_sender = evm_transaction.from(); + seed_evm_account( + &mut test.fixture, + evm_sender, + U512::from(EVM_INITIAL_BALANCE), + ); + + let (_txn_hash, deploy_block_height, deploy_execution_result) = test + .send_transaction(Transaction::from(evm_transaction)) + .await; + let ExecutionResult::Evm(deploy_execution_result) = deploy_execution_result else { + panic!("expected EVM execution result"); + }; + assert_eq!( + deploy_execution_result.receipt.status, + evm::ReceiptStatus::Success + ); + + let contract_address = deploy_execution_result + .receipt + .contract_address + .expect("EVM deployment should create a contract"); + assert_ne!( + evm_code_hash_at(&mut test.fixture, deploy_block_height, contract_address), + EMPTY_CODE_HASH + ); + + let contract_balance_before = + evm_balance(&mut test.fixture, contract_address, deploy_block_height); + assert_eq!(contract_balance_before, U512::zero()); + + let transfer_amount = test + .fixture + .chainspec + .transaction_config + .native_transfer_minimum_motes + + 100; + let alice_secret_key = Arc::clone(&test.fixture.node_contexts[0].secret_key); + let (_txn_hash, transfer_block_height, transfer_execution_result) = transfer_to_evm_address( + &mut test.fixture, + transfer_amount, + &alice_secret_key, + contract_address, + PricingMode::Fixed { + gas_price_tolerance: 1, + additional_computation_factor: 0, + }, + Some(0xE1), + ) + .await; + + assert!( + !exec_result_is_success(&transfer_execution_result), + "native transfer to EVM contract address should fail: {transfer_execution_result:?}" + ); + assert_eq!( + evm_balance(&mut test.fixture, contract_address, transfer_block_height), + contract_balance_before + ); +} + #[tokio::test] async fn should_accept_transfer_without_id() { let initial_stakes = InitialStakes::FromVec(vec![u128::MAX, 1]); diff --git a/storage/src/system/transfer.rs b/storage/src/system/transfer.rs index fd1be494d7..fcaa515b56 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -51,6 +51,9 @@ pub enum TransferError { /// Invalid operation. #[error("Invalid operation")] InvalidOperation, + /// Native transfer to an EVM contract address. + #[error("Native transfer to EVM contract address {0} is not allowed")] + EvmContractAddress(evm::Address), /// Disallowed transfer attempt (private chain). #[error("Either the source or the target must be an admin (private chain).")] RestrictedTransferAttempted, @@ -364,6 +367,7 @@ impl TransferRuntimeArgsBuilder { if *cl_value.cl_type() == CLType::ByteArray(evm::ADDRESS_LENGTH as u32) => { let address: evm::Address = self.map_cl_value(cl_value)?; + self.reject_evm_contract_target(address, Rc::clone(&tracking_copy))?; let key = Key::Evm(evm::EvmAddr::Account(address)); let maybe_stored_value = tracking_copy.borrow_mut().read(&key)?; return match maybe_stored_value { @@ -441,6 +445,37 @@ impl TransferRuntimeArgsBuilder { } } + fn reject_evm_contract_target( + &self, + address: evm::Address, + tracking_copy: Rc>>, + ) -> Result<(), TransferError> + where + R: StateReader, + { + let key = Key::Evm(evm::EvmAddr::CodeHash(address)); + match tracking_copy.borrow_mut().read(&key)? { + Some(StoredValue::CLValue(cl_value)) => { + let code_hash = cl_value + .into_t::() + .map_err(TransferError::CLValue)?; + // Crediting a contract purse directly would bypass Ethereum value-transfer + // semantics. Preserving those semantics would require executing recipient + // EVM code, which is intentionally outside native transfer behavior. + if code_hash == evm::EMPTY_CODE_HASH { + Ok(()) + } else { + Err(TransferError::EvmContractAddress(address)) + } + } + Some(stored_value) => Err(TransferError::TypeMismatch(StoredValueTypeMismatch::new( + "StoredValue::CLValue(evm::Hash)".to_string(), + stored_value.type_name(), + ))), + None => Ok(()), + } + } + /// Resolves amount. /// /// User has to specify "amount" argument that could be either a [`U512`] or a u64. @@ -485,27 +520,28 @@ impl TransferRuntimeArgsBuilder { where R: StateReader, { - let (to, target) = - match self.resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))? { - TransferTargetMode::ExistingAccount { - main_purse: purse_uref, - target_account_hash: target_account, - } => (Some(target_account), purse_uref), - TransferTargetMode::ExistingEvmAccount { - main_purse: purse_uref, - .. - } => (None, purse_uref), - TransferTargetMode::PurseExists { - target_account_hash, - purse_uref, - } => (target_account_hash, purse_uref), - TransferTargetMode::CreateAccount(_) | TransferTargetMode::CreateEvmAccount(_) => { - // Method "build()" is called after `resolve_transfer_target_mode` is first called - // and handled by creating a new account. Calling `resolve_transfer_target_mode` - // for the second time should never return `CreateAccount` variant. - return Err(TransferError::InvalidOperation); - } - }; + let (to, target) = match self + .resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))? + { + TransferTargetMode::ExistingAccount { + main_purse: purse_uref, + target_account_hash: target_account, + } => (Some(target_account), purse_uref), + TransferTargetMode::ExistingEvmAccount { + main_purse: purse_uref, + .. + } => (None, purse_uref), + TransferTargetMode::PurseExists { + target_account_hash, + purse_uref, + } => (target_account_hash, purse_uref), + TransferTargetMode::CreateAccount(_) | TransferTargetMode::CreateEvmAccount(_) => { + // Method "build()" is called after `resolve_transfer_target_mode` is first called + // and handled by creating a new account. Calling `resolve_transfer_target_mode` + // for the second time should never return `CreateAccount` variant. + return Err(TransferError::InvalidOperation); + } + }; let source = self.resolve_source_uref(from, Rc::clone(&tracking_copy))?; From 0b52027cd56665eb871ba2a024822f63b412ebb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Wed, 27 May 2026 17:38:05 +0200 Subject: [PATCH 17/17] Unify transaction origin handling for EVM and Wasm --- EVM.md | 21 +- execution_engine/src/engine_state/mod.rs | 8 +- .../src/transfer_request_builder.rs | 8 +- .../test_support/src/wasm_test_builder.rs | 6 +- node/src/components/contract_runtime/error.rs | 21 +- .../components/contract_runtime/operations.rs | 205 +++++++++++------- .../operations/wasm_v2_request.rs | 12 +- node/src/components/contract_runtime/types.rs | 15 +- node/src/components/event_stream_server.rs | 38 ++-- .../event_stream_server/sse_server.rs | 14 +- node/src/components/transaction_acceptor.rs | 22 +- .../main_reactor/tests/transactions.rs | 7 +- .../src/types/transaction/meta_transaction.rs | 18 +- .../transaction/meta_transaction/meta_evm.rs | 10 +- .../meta_transaction/transaction_header.rs | 37 +++- storage/src/data_access_layer.rs | 4 +- storage/src/data_access_layer/balance.rs | 37 +--- storage/src/data_access_layer/handle_fee.rs | 4 +- storage/src/global_state/state/mod.rs | 22 +- types/src/execution/evm_execution_result.rs | 8 +- types/src/gens.rs | 2 - types/src/transaction.rs | 10 +- types/src/transaction/deploy.rs | 2 +- types/src/transaction/initiator_addr.rs | 66 +----- 24 files changed, 295 insertions(+), 302 deletions(-) diff --git a/EVM.md b/EVM.md index 976639b0e7..f5bc7152a8 100644 --- a/EVM.md +++ b/EVM.md @@ -210,14 +210,13 @@ For client-submitted EVM transactions, the acceptor currently validates: 12. That balance must meet the chain baseline motes requirement. The acceptor does not require a Casper `AddressableEntity` for every EVM -address. The sender identity is still -`InitiatorAddr::EvmAddress(transaction.from())`. If the EVM address is linked to -`Key::Account(account_hash)`, the Casper account's main purse is used. If it is -an EVM-native identity, the stored purse is used. If no EVM identity exists yet, -the acceptor uses the recovered signer public key to check the corresponding -Casper account balance, falling back to the address's deterministic EVM purse -when no Casper account exists. The runtime later checks the full EVM maximum -fee amount. +address. The EVM sender identity is `transaction.from()`. If the EVM address is +linked to `Key::Account(account_hash)`, the Casper account's main purse is used. +If it is an EVM-native identity, the stored purse is used. If no EVM identity +exists yet, the acceptor uses the recovered signer public key to check the +corresponding Casper account balance, falling back to the address's +deterministic EVM purse when no Casper account exists. The runtime later checks +the full EVM maximum fee amount. The nonce check is also applied to peer-sourced EVM transactions before storage, so a gossiped transaction with a nonce that cannot execute at the current state @@ -238,10 +237,10 @@ evm_transaction = stored_transaction.as_evm() meta_transaction = MetaTransaction::from_transaction(stored_transaction, ...) ``` -Common metadata such as hash, initiator, authorization keys, size estimate, -gas limit, and cost is derived directly from `Transaction`. For EVM: +Common metadata such as hash, authorization keys, size estimate, gas limit, and +cost is derived directly from `Transaction`. For EVM: -- the initiator is `InitiatorAddr::EvmAddress(transaction.from())`, +- the initiator is the EVM sender address, `transaction.from()`, - the transaction lane is currently the last configured Wasm lane, - the gas limit is the Ethereum transaction gas limit, - the maximum cost is `gas_limit * effective_gas_price`, diff --git a/execution_engine/src/engine_state/mod.rs b/execution_engine/src/engine_state/mod.rs index d279f2ac82..b0b201afcb 100644 --- a/execution_engine/src/engine_state/mod.rs +++ b/execution_engine/src/engine_state/mod.rs @@ -76,9 +76,7 @@ impl ExecutionEngineV1 { // A good deal of effort has been put into removing all such behaviors; please do not // come along and start adding it back. - let account_hash = initiator_addr - .account_hash() - .expect("Wasm v1 initiator must be a Casper account"); + let account_hash = initiator_addr.account_hash(); let protocol_version = self.config.protocol_version(); let state_hash = block_info.state_hash; let tc = match state_provider.tracking_copy(state_hash) { @@ -157,9 +155,7 @@ impl ExecutionEngineV1 { // A good deal of effort has been put into removing all such behaviors; please do not // come along and start adding it back. - let account_hash = initiator_addr - .account_hash() - .expect("Wasm v1 initiator must be a Casper account"); + let account_hash = initiator_addr.account_hash(); let protocol_version = self.config.protocol_version(); let tc = Rc::new(RefCell::new(tracking_copy)); let (runtime_footprint, entity_addr) = { diff --git a/execution_engine_testing/test_support/src/transfer_request_builder.rs b/execution_engine_testing/test_support/src/transfer_request_builder.rs index 1f077172ea..46da00f8f0 100644 --- a/execution_engine_testing/test_support/src/transfer_request_builder.rs +++ b/execution_engine_testing/test_support/src/transfer_request_builder.rs @@ -128,11 +128,9 @@ impl TransferRequestBuilder { /// authorization keys. pub fn with_initiator>(mut self, initiator: T) -> Self { self.initiator = initiator.into(); - let _ = self.authorization_keys.insert( - self.initiator - .account_hash() - .expect("test transfer initiator must be a Casper account"), - ); + let _ = self + .authorization_keys + .insert(self.initiator.account_hash()); self } diff --git a/execution_engine_testing/test_support/src/wasm_test_builder.rs b/execution_engine_testing/test_support/src/wasm_test_builder.rs index 97c8dda244..70b8db9115 100644 --- a/execution_engine_testing/test_support/src/wasm_test_builder.rs +++ b/execution_engine_testing/test_support/src/wasm_test_builder.rs @@ -813,11 +813,7 @@ where .expect("builder must have a post-state hash"); let transaction_hash = TransactionHash::V1(TransactionV1Hash::default()); - let authorization_keys = BTreeSet::from_iter(iter::once( - initiator - .account_hash() - .expect("test bidding initiator must be a Casper account"), - )); + let authorization_keys = BTreeSet::from_iter(iter::once(initiator.account_hash())); let config = &self.chainspec; let fee_handling = config.core_config.fee_handling; diff --git a/node/src/components/contract_runtime/error.rs b/node/src/components/contract_runtime/error.rs index 5fb5b6618b..09af7a8736 100644 --- a/node/src/components/contract_runtime/error.rs +++ b/node/src/components/contract_runtime/error.rs @@ -8,13 +8,12 @@ use thiserror::Error; use casper_execution_engine::engine_state::Error as EngineStateError; use casper_storage::{ data_access_layer::{ - forced_undelegate::ForcedUndelegateError, BalanceIdentifierFromInitiatorError, - BlockRewardsError, FeeError, StepError, + forced_undelegate::ForcedUndelegateError, BlockRewardsError, FeeError, StepError, }, global_state::error::Error as GlobalStateError, tracking_copy::TrackingCopyError, }; -use casper_types::{bytesrepr, evm, CLValueError, Digest, EraId, PublicKey, U512}; +use casper_types::{bytesrepr, CLValueError, Digest, EraId, PublicKey, U512}; use crate::{ components::contract_runtime::ExecutionPreState, @@ -177,25 +176,9 @@ pub enum BlockExecutionError { /// Invalid transaction variant. #[error("Invalid transaction variant")] InvalidTransactionVariant, - /// EVM initiators are only valid for EVM transaction variants. - #[error("EVM initiator address {address:?} is only valid for EVM transactions")] - EvmInitiatorForNonEvmTransaction { - /// The EVM initiator address found on a non-EVM transaction. - address: evm::Address, - }, /// Invalid transaction arguments. #[error("Invalid transaction arguments")] InvalidTransactionArgs, #[error("Data Access Layer conflicts with chainspec setting: {0}")] InvalidAESetting(bool), } - -impl From for BlockExecutionError { - fn from(error: BalanceIdentifierFromInitiatorError) -> Self { - match error { - BalanceIdentifierFromInitiatorError::EvmAddress(address) => { - BlockExecutionError::EvmInitiatorForNonEvmTransaction { address } - } - } - } -} diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index cd2d8410fd..ec3daa6204 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -48,9 +48,9 @@ use casper_types::{ execution::{Effects, ExecutionResult, TransformKindV2, TransformV2}, system::handle_payment::ARG_AMOUNT, BlockHash, BlockHeader, BlockTime, BlockV2, CLValue, Chainspec, ChecksumRegistry, Digest, - EntityAddr, EraEndV2, EraId, FeeHandling, Gas, InvalidTransaction, InvalidTransactionV1, Key, - ProtocolVersion, PublicKey, RefundHandling, StoredValue, TimeDiff, Transaction, - TransactionEntryPoint, AUCTION_LANE_ID, MINT_LANE_ID, U512, + EntityAddr, EraEndV2, EraId, FeeHandling, Gas, InitiatorAddr, InvalidTransaction, + InvalidTransactionV1, Key, ProtocolVersion, PublicKey, RefundHandling, StoredValue, TimeDiff, + Transaction, TransactionEntryPoint, AUCTION_LANE_ID, MINT_LANE_ID, U512, }; use super::{ @@ -87,15 +87,82 @@ fn evm_precondition_receipt(effective_gas_price: u128) -> EvmReceipt { } #[derive(Clone, Debug)] -struct EvmOriginResolution { - // Concrete payer selected before payment checks. This is deliberately a - // Casper balance identifier, not an EVM-specific balance mode, so the rest - // of block execution can use the normal hold/refund/fee machinery. - balance_identifier: BalanceIdentifier, - // State mutation to perform later, inside the same tracking copy as EVM - // execution. Origin resolution itself is read-only so a rejected - // transaction does not create accounts or links as a side effect. - identity_plan: EvmIdentityPlan, +enum RuntimeOrigin { + Initiator { + initiator_addr: InitiatorAddr, + payer: BalanceIdentifier, + }, + Evm { + // Concrete payer selected before payment checks. This is deliberately a + // data-access balance identifier, not an EVM-specific balance mode, so + // the rest of block execution can use the normal hold/refund/fee + // machinery. + balance_identifier: BalanceIdentifier, + // State mutation to perform later, inside the same tracking copy as EVM + // execution. Origin resolution itself is read-only so a rejected + // transaction does not create accounts or links as a side effect. + identity_plan: EvmIdentityPlan, + }, +} + +impl RuntimeOrigin { + fn from_initiator_addr(initiator_addr: InitiatorAddr) -> Self { + RuntimeOrigin::Initiator { + payer: BalanceIdentifier::from(initiator_addr.clone()), + initiator_addr, + } + } + + fn from_evm_parts( + balance_identifier: BalanceIdentifier, + identity_plan: EvmIdentityPlan, + ) -> Self { + RuntimeOrigin::Evm { + balance_identifier, + identity_plan, + } + } + + fn payer_balance_identifier(&self) -> BalanceIdentifier { + match self { + RuntimeOrigin::Initiator { payer, .. } => payer.clone(), + RuntimeOrigin::Evm { + balance_identifier, .. + } => balance_identifier.clone(), + } + } + + fn initiator_addr(&self) -> Result { + match self { + RuntimeOrigin::Initiator { initiator_addr, .. } => Ok(initiator_addr.clone()), + RuntimeOrigin::Evm { .. } => Err(BlockExecutionError::InvalidTransactionVariant), + } + } + + fn account_hash(&self) -> Result { + self.initiator_addr() + .map(|initiator_addr| initiator_addr.account_hash()) + } + + fn fee_initiator(&self) -> Option> { + match self { + RuntimeOrigin::Initiator { initiator_addr, .. } => { + Some(Box::new(initiator_addr.clone())) + } + RuntimeOrigin::Evm { .. } => None, + } + } + + fn evm_identity_plan(&self) -> Option { + match self { + RuntimeOrigin::Initiator { .. } => None, + RuntimeOrigin::Evm { identity_plan, .. } => Some(*identity_plan), + } + } + + fn is_evm(&self) -> bool { + matches!(self, RuntimeOrigin::Evm { .. }) + } } /// Deferred write needed to make an EVM sender's identity explicit in global state. @@ -129,12 +196,12 @@ enum EvmIdentityPlan { /// This function only reads state. That matters because it runs before payment /// preconditions are known to pass. If execution is later allowed, the returned /// [`EvmIdentityPlan`] is applied in the tracking copy used for EVM execution. -fn resolve_evm_origin( +fn resolve_evm_runtime_origin( scratch_state: &ScratchGlobalState, state_root_hash: Digest, protocol_version: ProtocolVersion, transaction: &casper_types::evm::Transaction, -) -> Result { +) -> Result { let address = transaction.from(); // The signer gives us a Casper `AccountHash` preimage from the secp256k1 // public key. That account hash is not derivable from the 20-byte EVM @@ -169,10 +236,10 @@ fn resolve_evm_origin( // Existing bridge records are authoritative. Once an EVM // address is linked, the payer is the linked Casper account's // main purse. - Key::Account(account_hash) => Ok(EvmOriginResolution { - balance_identifier: BalanceIdentifier::Account(account_hash), - identity_plan: EvmIdentityPlan::None, - }), + Key::Account(account_hash) => Ok(RuntimeOrigin::from_evm_parts( + BalanceIdentifier::Account(account_hash), + EvmIdentityPlan::None, + )), // Existing EVM-native identities keep paying from their stored // purse. We may still plan an upgrade to a Casper link, but // only when doing so cannot steal a contract identity or move @@ -186,10 +253,10 @@ fn resolve_evm_origin( purse, deterministic_purse, )?; - Ok(EvmOriginResolution { - balance_identifier: BalanceIdentifier::Purse(purse), + Ok(RuntimeOrigin::from_evm_parts( + BalanceIdentifier::Purse(purse), identity_plan, - }) + )) } other => Err(BlockExecutionError::PaymentError(format!( "invalid EVM account identity key: {other}" @@ -205,33 +272,33 @@ fn resolve_evm_origin( // already a contract/runtime-created EVM account. Contracts do not // have a signing key, so they must remain EVM-native. if evm_account_has_code(&mut tracking_copy, address)? { - return Ok(EvmOriginResolution { - balance_identifier: BalanceIdentifier::Purse(deterministic_purse), - identity_plan: EvmIdentityPlan::None, - }); + return Ok(RuntimeOrigin::from_evm_parts( + BalanceIdentifier::Purse(deterministic_purse), + EvmIdentityPlan::None, + )); } match account_main_purse(&mut tracking_copy, protocol_version, account_hash)? { // A Casper account exists for the recovered signer, but the EVM // address has not been seen before. Use the account for payment // immediately and write the bridge only if execution proceeds. - Some(_) => Ok(EvmOriginResolution { - balance_identifier: BalanceIdentifier::Account(account_hash), - identity_plan: EvmIdentityPlan::LinkExisting { + Some(_) => Ok(RuntimeOrigin::from_evm_parts( + BalanceIdentifier::Account(account_hash), + EvmIdentityPlan::LinkExisting { address, account_hash, }, - }), + )), // First use of this signing pair on both sides. Runtime will // create a Casper account whose main purse is the deterministic // EVM purse, then write the bridge record. - None => Ok(EvmOriginResolution { - balance_identifier: BalanceIdentifier::Purse(deterministic_purse), - identity_plan: EvmIdentityPlan::CreateAccount { + None => Ok(RuntimeOrigin::from_evm_parts( + BalanceIdentifier::Purse(deterministic_purse), + EvmIdentityPlan::CreateAccount { address, account_hash, main_purse: deterministic_purse, }, - }), + )), } } } @@ -552,16 +619,9 @@ pub fn execute_finalized_block( ) .map_err(|err| BlockExecutionError::TransactionConversion(err.to_string()))?; - let initiator_addr = stored_transaction.initiator_addr(); let transaction_hash = stored_transaction.hash(); let authorization_keys = stored_transaction.authorization_keys(); - if !is_evm { - if let Some(address) = initiator_addr.evm_address() { - return Err(BlockExecutionError::EvmInitiatorForNonEvmTransaction { address }); - } - } - /* we solve for halting state using a `gas limit` which is the maximum amount of computation we will allow a given transaction to consume. the transaction itself @@ -655,16 +715,20 @@ pub fn execute_finalized_block( let is_custom_payment = !is_standard_payment && transaction.is_custom_payment(); let is_v1_wasm = transaction.is_v1_wasm(); let is_v2_wasm = transaction.is_v2_wasm(); - let evm_origin = if let Some(evm_transaction) = evm_transaction { - Some(resolve_evm_origin( + let runtime_origin = if let Some(evm_transaction) = evm_transaction { + resolve_evm_runtime_origin( &scratch_state, state_root_hash, protocol_version, evm_transaction, - )?) + )? } else { - None + let initiator_addr = stored_transaction + .initiator_addr() + .ok_or(BlockExecutionError::InvalidTransactionVariant)?; + RuntimeOrigin::from_initiator_addr(initiator_addr) }; + let payer_balance_identifier = runtime_origin.payer_balance_identifier(); let refund_purse_active = is_custom_payment; if refund_purse_active { @@ -678,7 +742,7 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleRefundMode::SetRefundPurse { - target: Box::new(initiator_addr.clone().try_into()?), + target: Box::new(payer_balance_identifier.clone()), }, ); let handle_refund_result = scratch_state.handle_refund(handle_refund_request); @@ -700,10 +764,7 @@ pub fn execute_finalized_block( let initial_balance_result = scratch_state.balance(BalanceRequest::new( state_root_hash, protocol_version, - evm_origin.as_ref().map_or_else( - || initiator_addr.clone().try_into(), - |origin| Ok(origin.balance_identifier.clone()), - )?, + payer_balance_identifier.clone(), balance_handling, ProofHandling::NoProofs, )); @@ -733,7 +794,7 @@ pub fn execute_finalized_block( } let mut balance_identifier = { - if let Some(origin) = evm_origin.as_ref() { + if runtime_origin.is_evm() { // EVM transactions intentionally do not participate in Casper custom payment // or refund-purse setup. Ethereum payloads carry a gas limit and gas price fields, // but this chain still owns the fee/refund policy through the same chainspec @@ -742,7 +803,7 @@ pub fn execute_finalized_block( // calculation, and final fee handling, while revm runs with gas fee // charging disabled and only mutates EVM nonce, code, storage, // logs, creates, and value transfers. - origin.balance_identifier.clone() + payer_balance_identifier.clone() } else if is_standard_payment { let contract_might_pay = addressable_entity_enabled && transaction.is_contract_by_hash_invocation(); @@ -753,29 +814,26 @@ pub fn execute_finalized_block( Ok(None) => { // the initiating account pays using its main purse trace!(%transaction_hash, "direct invocation with account payment"); - initiator_addr.clone().try_into()? + payer_balance_identifier.clone() } Err(err) => { trace!(%transaction_hash, "failed to resolve contract self payment"); artifact_builder .with_state_result_error(err) .map_err(|_| BlockExecutionError::RootNotFound(state_root_hash))?; - let account_hash = initiator_addr - .account_hash() - .expect("contract runtime initiator must be a Casper account"); - BalanceIdentifier::PenalizedAccount(account_hash) + BalanceIdentifier::PenalizedAccount(runtime_origin.account_hash()?) } } } else { // the initiating account pays using its main purse trace!(%transaction_hash, "account session with standard payment"); - initiator_addr.clone().try_into()? + payer_balance_identifier.clone() } } else if is_v2_wasm { // vm2 does not support custom payment, so it MUST be standard payment // if transaction runtime is v2 then the initiating account will pay using // the refund purse - initiator_addr.clone().try_into()? + payer_balance_identifier.clone() } else if is_custom_payment { // this is the custom payment flow // the initiating account will pay, but wants to do so with a different purse or @@ -819,11 +877,11 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys.clone(), BalanceIdentifierTransferArgs::new( None, - initiator_addr.clone().try_into()?, + payer_balance_identifier.clone(), BalanceIdentifier::Payment, baseline_motes_amount, None, @@ -864,10 +922,7 @@ pub fn execute_finalized_block( BalanceIdentifier::Payment } } else { - let account_hash = initiator_addr - .account_hash() - .expect("contract runtime initiator must be a Casper account"); - BalanceIdentifier::PenalizedAccount(account_hash) + BalanceIdentifier::PenalizedAccount(runtime_origin.account_hash()?) } }; @@ -959,7 +1014,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys, runtime_args.clone(), )); @@ -975,7 +1030,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys, runtime_args.clone(), )); @@ -1005,7 +1060,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys, auction_method, )); @@ -1045,7 +1100,7 @@ pub fn execute_finalized_block( let mut tracking_copy = scratch_state .tracking_copy(state_root_hash)? .ok_or(BlockExecutionError::RootNotFound(state_root_hash))?; - if let Some(origin) = evm_origin.as_ref() { + if let Some(identity_plan) = runtime_origin.evm_identity_plan() { // Apply the deferred bridge/account creation only now, // after balance preconditions have allowed execution. // This keeps rejected EVM transactions from mutating @@ -1054,7 +1109,7 @@ pub fn execute_finalized_block( apply_evm_identity_plan( &mut tracking_copy, protocol_version, - origin.identity_plan, + identity_plan, )?; } let outcome = EvmExecutor::new(chainspec.evm_config) @@ -1230,7 +1285,7 @@ pub fn execute_finalized_block( // placing a hold on the correct purse. balance_identifier = BalanceIdentifier::Refund; Some(HandleRefundMode::RefundNoFeeCustomPayment { - initiator_addr: Box::new(initiator_addr.clone()), + initiator_addr: Box::new(runtime_origin.initiator_addr()?), limit: artifact_builder.limit(), gas_price: current_gas_price, cost: artifact_builder.cost_to_use(), @@ -1271,7 +1326,7 @@ pub fn execute_finalized_block( // logic, which is interpreted by inner logic to use the currently set // refund purse. Some(HandleRefundMode::Refund { - initiator_addr: Box::new(initiator_addr.clone()), + initiator_addr: Box::new(runtime_origin.initiator_addr()?), limit: artifact_builder.limit(), gas_price: current_gas_price, consumed, @@ -1382,7 +1437,7 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleFeeMode::pay( - Box::new(initiator_addr.clone()), + runtime_origin.fee_initiator(), balance_identifier, BalanceIdentifier::Public(*(proposer.clone())), fee_amount, @@ -1399,7 +1454,7 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleFeeMode::pay( - Box::new(initiator_addr.clone()), + runtime_origin.fee_initiator(), balance_identifier, BalanceIdentifier::Accumulate, fee_amount, @@ -1886,7 +1941,9 @@ where *state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + initiator_addr + .cloned() + .expect("native speculative execution requires a Casper initiator"), authorization_keys, runtime_args, )); diff --git a/node/src/components/contract_runtime/operations/wasm_v2_request.rs b/node/src/components/contract_runtime/operations/wasm_v2_request.rs index 8314b14c91..f50c5cb7f7 100644 --- a/node/src/components/contract_runtime/operations/wasm_v2_request.rs +++ b/node/src/components/contract_runtime/operations/wasm_v2_request.rs @@ -101,7 +101,9 @@ impl WasmV2Request { transaction: &MetaTransaction, ) -> Result { let transaction_hash = transaction.hash(); - let initiator_addr = transaction.initiator_addr(); + let initiator_addr = transaction + .initiator_addr() + .expect("Wasm v2 transaction requires a Casper initiator"); let gas_limit: u64 = gas_limit .value() @@ -213,9 +215,7 @@ impl WasmV2Request { // different API. debug_assert_eq!(transferred_value, value); - let initiator_account_hash = initiator_addr - .account_hash() - .expect("Wasm v2 initiator must be a Casper account"); + let initiator_account_hash = initiator_addr.account_hash(); let install_request = builder .with_initiator(initiator_account_hash) .with_gas_limit(gas_limit) @@ -236,9 +236,7 @@ impl WasmV2Request { Target::Session { .. } | Target::Stored { .. } => { let mut builder = ExecuteRequestBuilder::default(); - let initiator_account_hash = initiator_addr - .account_hash() - .expect("Wasm v2 initiator must be a Casper account"); + let initiator_account_hash = initiator_addr.account_hash(); let initiator_key = Key::Account(initiator_account_hash); diff --git a/node/src/components/contract_runtime/types.rs b/node/src/components/contract_runtime/types.rs index 2a296c8cad..17e93256e7 100644 --- a/node/src/components/contract_runtime/types.rs +++ b/node/src/components/contract_runtime/types.rs @@ -74,7 +74,8 @@ pub(crate) struct ExecutionArtifactBuilder { error_message: Option, messages: Messages, transfers: Vec, - initiator: InitiatorAddr, + initiator: Option, + evm_initiator: Option, current_price: u8, cost: U512, limit: Gas, @@ -102,6 +103,7 @@ impl ExecutionArtifactBuilder { transfers: vec![], messages: Default::default(), initiator: transaction.initiator_addr(), + evm_initiator: transaction.evm_initiator_addr(), current_price, cost: initial_cost, limit, @@ -127,6 +129,7 @@ impl ExecutionArtifactBuilder { transfers: vec![], messages: Default::default(), initiator: transaction.initiator_addr(), + evm_initiator: transaction.evm_initiator_addr(), current_price, cost: U512::zero(), limit: Gas::zero(), @@ -477,7 +480,7 @@ impl ExecutionArtifactBuilder { #[allow(unused)] pub fn with_initiator_addr(&mut self, initiator_addr: InitiatorAddr) -> &mut Self { - self.initiator = initiator_addr; + self.initiator = Some(initiator_addr); self } @@ -490,7 +493,9 @@ impl ExecutionArtifactBuilder { let actual_cost = self.cost_to_use(); let execution_result = if let Some(receipt) = self.evm_receipt { let result = EvmExecutionResult { - initiator: self.initiator, + initiator: self + .evm_initiator + .expect("EVM execution result requires an EVM initiator"), current_price: self.current_price, limit: self.limit, cost: actual_cost, @@ -504,7 +509,9 @@ impl ExecutionArtifactBuilder { let result = ExecutionResultV2 { effects: self.effects, transfers: self.transfers, - initiator: self.initiator, + initiator: self + .initiator + .expect("Wasm execution result requires a Casper initiator"), refund: self.refund, limit: self.limit, consumed: self.consumed, diff --git a/node/src/components/event_stream_server.rs b/node/src/components/event_stream_server.rs index 2d76519c14..d93ffc47ff 100644 --- a/node/src/components/event_stream_server.rs +++ b/node/src/components/event_stream_server.rs @@ -299,21 +299,33 @@ where execution_result, messages, } => { - let (initiator_addr, timestamp, ttl) = match *transaction_header { - TransactionHeader::Deploy(deploy_header) => ( - InitiatorAddr::PublicKey(deploy_header.account().clone()), - deploy_header.timestamp(), - deploy_header.ttl(), - ), - TransactionHeader::V1(metadata) | TransactionHeader::Evm(metadata) => ( - metadata.initiator_addr().clone(), - metadata.timestamp(), - metadata.ttl(), - ), - }; + let (initiator_addr, evm_initiator_addr, timestamp, ttl) = + match *transaction_header { + TransactionHeader::Deploy(deploy_header) => ( + Some(Box::new(InitiatorAddr::PublicKey( + deploy_header.account().clone(), + ))), + None, + deploy_header.timestamp(), + deploy_header.ttl(), + ), + TransactionHeader::V1(metadata) => ( + Some(Box::new(metadata.initiator_addr().clone())), + None, + metadata.timestamp(), + metadata.ttl(), + ), + TransactionHeader::Evm(metadata) => ( + None, + Some(Box::new(metadata.initiator_addr())), + metadata.timestamp(), + metadata.ttl(), + ), + }; self.broadcast(SseData::TransactionProcessed { transaction_hash: Box::new(transaction_hash), - initiator_addr: Box::new(initiator_addr), + initiator_addr, + evm_initiator_addr, timestamp, ttl, block_hash: Box::new(block_hash), diff --git a/node/src/components/event_stream_server/sse_server.rs b/node/src/components/event_stream_server/sse_server.rs index 03b9b0cd84..caa6ca186f 100644 --- a/node/src/components/event_stream_server/sse_server.rs +++ b/node/src/components/event_stream_server/sse_server.rs @@ -34,6 +34,7 @@ use warp::{ use casper_types::{ contract_messages::Messages, + evm, execution::{Effects, ExecutionResult}, Block, BlockHash, EraId, FinalitySignature, InitiatorAddr, ProtocolVersion, PublicKey, TimeDiff, Timestamp, Transaction, TransactionHash, @@ -71,7 +72,8 @@ pub enum SseData { /// The given transaction has been executed, committed and forms part of the given block. TransactionProcessed { transaction_hash: Box, - initiator_addr: Box, + initiator_addr: Option>, + evm_initiator_addr: Option>, timestamp: Timestamp, ttl: TimeDiff, block_hash: Box, @@ -131,9 +133,17 @@ impl SseData { .take(message_count) .collect(); + let (initiator_addr, evm_initiator_addr) = match &txn { + Transaction::Deploy(_) | Transaction::V1(_) => { + (txn.initiator_addr().map(Box::new), None) + } + Transaction::Evm(evm_transaction) => (None, Some(Box::new(evm_transaction.from()))), + }; + SseData::TransactionProcessed { transaction_hash: Box::new(txn.hash()), - initiator_addr: Box::new(txn.initiator_addr()), + initiator_addr, + evm_initiator_addr, timestamp, ttl, block_hash: Box::new(BlockHash::random(rng)), diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index d4e6506fb7..41e496ab9d 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -313,10 +313,11 @@ impl TransactionAcceptor { } if event_metadata.source.is_client() { - let initiator_addr = event_metadata.transaction.initiator_addr(); - let account_hash = initiator_addr - .account_hash() + let initiator_addr = event_metadata + .transaction + .initiator_addr() .expect("non-EVM transaction initiator must be a Casper account"); + let account_hash = initiator_addr.account_hash(); let entity_addr = EntityAddr::Account(account_hash.value()); effect_builder .get_addressable_entity(*block_header.state_root_hash(), entity_addr) @@ -641,7 +642,10 @@ impl TransactionAcceptor { ) -> Effects { match maybe_entity { None => { - let initiator_addr = event_metadata.transaction.initiator_addr(); + let initiator_addr = event_metadata + .transaction + .initiator_addr() + .expect("missing entity check requires a Casper initiator"); let error = Error::parameter_failure( &block_header, ParameterFailure::NoSuchAddressableEntity { initiator_addr }, @@ -694,7 +698,10 @@ impl TransactionAcceptor { } match maybe_balance { None => { - let initiator_addr = event_metadata.transaction.initiator_addr(); + let initiator_addr = event_metadata + .transaction + .initiator_addr() + .expect("balance check requires a Casper initiator"); let error = Error::parameter_failure( &block_header, ParameterFailure::UnknownBalance { initiator_addr }, @@ -705,7 +712,10 @@ impl TransactionAcceptor { let has_minimum_balance = balance >= self.chainspec.core_config.baseline_motes_amount_u512(); if !has_minimum_balance { - let initiator_addr = event_metadata.transaction.initiator_addr(); + let initiator_addr = event_metadata + .transaction + .initiator_addr() + .expect("balance check requires a Casper initiator"); let error = Error::parameter_failure( &block_header, ParameterFailure::InsufficientBalance { initiator_addr }, diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index dada9399fd..e215eef22c 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -23,7 +23,7 @@ use casper_types::{ runtime_args, system::mint::{ARG_AMOUNT, ARG_TARGET}, AccessRights, AddressableEntity, CLValue, Digest, EntityAddr, ExecutableDeployItem, - ExecutionInfo, InitiatorAddr, TransactionRuntimeParams, URef, URefAddr, DEFAULT_TRANSFER_COST, + ExecutionInfo, TransactionRuntimeParams, URef, URefAddr, DEFAULT_TRANSFER_COST, }; use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey}; use once_cell::sync::Lazy; @@ -1162,10 +1162,7 @@ async fn should_execute_evm_transaction_and_store_receipt() { panic!("expected EVM execution result"); }; - assert_eq!( - execution_result.initiator, - InitiatorAddr::EvmAddress(sender) - ); + assert_eq!(execution_result.initiator, sender); assert_eq!(execution_result.receipt.status, evm::ReceiptStatus::Success); assert_eq!( execution_result.receipt.effective_gas_price, diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index 42a7f6ae8d..444637c262 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -69,12 +69,12 @@ impl MetaTransaction { } } - /// Returns the address of the initiator of the transaction. - pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { + /// Returns the Casper initiator address, if this transaction has one. + pub(crate) fn initiator_addr(&self) -> Option<&InitiatorAddr> { match self { - MetaTransaction::Deploy(meta_deploy) => meta_deploy.initiator_addr(), - MetaTransaction::Evm(evm) => evm.initiator_addr(), - MetaTransaction::V1(txn) => txn.initiator_addr(), + MetaTransaction::Deploy(meta_deploy) => Some(meta_deploy.initiator_addr()), + MetaTransaction::Evm(_) => None, + MetaTransaction::V1(txn) => Some(txn.initiator_addr()), } } @@ -324,11 +324,11 @@ impl MetaTransaction { } pub(crate) fn to_session_input_data(&self) -> SessionInputData<'_> { - let initiator_addr = self.initiator_addr(); let is_standard_payment = self.is_standard_payment(); match self { MetaTransaction::Deploy(meta_deploy) => { let deploy = meta_deploy.deploy(); + let initiator_addr = meta_deploy.initiator_addr(); let data = SessionDataDeploy::new( deploy.hash(), deploy.session(), @@ -342,6 +342,7 @@ impl MetaTransaction { unreachable!("EVM transactions do not have Casper session input data") } MetaTransaction::V1(v1) => { + let initiator_addr = v1.initiator_addr(); let data = SessionDataV1::new( v1.args().as_named().expect("V1 wasm args should be named and validated at the transaction acceptor level"), v1.target(), @@ -568,10 +569,7 @@ mod tests { assert_eq!(meta.timestamp(), evm_transaction.timestamp()); assert_eq!(meta.ttl(), evm_transaction.ttl()); assert_eq!(meta.approvals(), evm_transaction.approvals().clone()); - assert_eq!( - meta.initiator_addr(), - &InitiatorAddr::EvmAddress(evm_transaction.from()) - ); + assert_eq!(meta.initiator_addr(), None); assert_eq!(meta.transaction_lane(), EVM_LANE); assert_eq!(meta.gas_limit(&chainspec).unwrap(), Gas::new(21_000)); assert_eq!(meta.gas_price_tolerance().unwrap(), u8::MAX); diff --git a/node/src/types/transaction/meta_transaction/meta_evm.rs b/node/src/types/transaction/meta_transaction/meta_evm.rs index 5dd4a504d6..6ae905a8f8 100644 --- a/node/src/types/transaction/meta_transaction/meta_evm.rs +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -4,8 +4,8 @@ use std::{ }; use casper_types::{ - bytesrepr::ToBytes, evm, Approval, Chainspec, Digest, Gas, InitiatorAddr, InvalidTransaction, - TimeDiff, Timestamp, TransactionConfig, TransactionHash, + bytesrepr::ToBytes, evm, Approval, Chainspec, Digest, Gas, InvalidTransaction, TimeDiff, + Timestamp, TransactionConfig, TransactionHash, }; use serde::Serialize; @@ -13,7 +13,6 @@ use serde::Serialize; #[derive(Clone, Debug, Serialize)] pub(crate) struct MetaEvmTransaction { transaction: evm::Transaction, - initiator_addr: InitiatorAddr, lane_id: u8, payload_hash: Digest, } @@ -33,7 +32,6 @@ impl MetaEvmTransaction { let payload_hash = Digest::hash(transaction.signing_payload()?); Ok(MetaEvmTransaction { transaction: transaction.clone(), - initiator_addr: InitiatorAddr::EvmAddress(transaction.from()), lane_id, payload_hash, }) @@ -59,10 +57,6 @@ impl MetaEvmTransaction { self.transaction.approvals() } - pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { - &self.initiator_addr - } - pub(crate) fn lane_id(&self) -> u8 { self.lane_id } diff --git a/node/src/types/transaction/meta_transaction/transaction_header.rs b/node/src/types/transaction/meta_transaction/transaction_header.rs index 71be97b7a7..0f8c694006 100644 --- a/node/src/types/transaction/meta_transaction/transaction_header.rs +++ b/node/src/types/transaction/meta_transaction/transaction_header.rs @@ -36,12 +36,43 @@ impl Display for TransactionMetadata { } } +impl Display for EvmTransactionMetadata { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + write!( + formatter, + "transaction-metadata[initiator_addr: EVM address {}]", + self.initiator_addr, + ) + } +} + +#[derive(Debug, Clone, DataSize, PartialEq, Eq, Serialize)] +pub(crate) struct EvmTransactionMetadata { + initiator_addr: evm::Address, + timestamp: Timestamp, + ttl: TimeDiff, +} + +impl EvmTransactionMetadata { + pub(crate) fn initiator_addr(&self) -> evm::Address { + self.initiator_addr + } + + pub(crate) fn timestamp(&self) -> Timestamp { + self.timestamp + } + + pub(crate) fn ttl(&self) -> TimeDiff { + self.ttl + } +} + #[derive(Debug, Clone, DataSize, Serialize, PartialEq, Eq)] /// A versioned wrapper for a transaction header or deploy header. pub(crate) enum TransactionHeader { Deploy(DeployHeader), V1(TransactionMetadata), - Evm(TransactionMetadata), + Evm(EvmTransactionMetadata), } impl From for TransactionHeader { @@ -63,8 +94,8 @@ impl From<&TransactionV1> for TransactionHeader { impl From<&evm::Transaction> for TransactionHeader { fn from(transaction: &evm::Transaction) -> Self { - let meta = TransactionMetadata { - initiator_addr: InitiatorAddr::EvmAddress(transaction.from()), + let meta = EvmTransactionMetadata { + initiator_addr: transaction.from(), timestamp: transaction.timestamp(), ttl: transaction.ttl(), }; diff --git a/storage/src/data_access_layer.rs b/storage/src/data_access_layer.rs index 7af13031ca..a34c16442e 100644 --- a/storage/src/data_access_layer.rs +++ b/storage/src/data_access_layer.rs @@ -56,8 +56,8 @@ mod trie; pub use addressable_entity::{AddressableEntityRequest, AddressableEntityResult}; pub use auction::{AuctionMethod, BiddingRequest, BiddingResult}; pub use balance::{ - BalanceHolds, BalanceHoldsWithProof, BalanceIdentifier, BalanceIdentifierFromInitiatorError, - BalanceRequest, BalanceResult, GasHoldBalanceHandling, ProofHandling, ProofsResult, + BalanceHolds, BalanceHoldsWithProof, BalanceIdentifier, BalanceRequest, BalanceResult, + GasHoldBalanceHandling, ProofHandling, ProofsResult, }; pub use balance_hold::{ BalanceHoldError, BalanceHoldKind, BalanceHoldMode, BalanceHoldRequest, BalanceHoldResult, diff --git a/storage/src/data_access_layer/balance.rs b/storage/src/data_access_layer/balance.rs index 6d7d5ba33c..67185f2d22 100644 --- a/storage/src/data_access_layer/balance.rs +++ b/storage/src/data_access_layer/balance.rs @@ -1,7 +1,6 @@ //! Types for balance queries. use casper_types::{ account::AccountHash, - evm, global_state::TrieMerkleProof, system::{ handle_payment::{ACCUMULATION_PURSE_KEY, PAYMENT_PURSE_KEY, REFUND_PURSE_KEY}, @@ -16,7 +15,6 @@ use num_rational::Ratio; use num_traits::CheckedMul; use std::{ collections::{btree_map::Entry, BTreeMap}, - convert::TryFrom, fmt::{Display, Formatter}, }; use tracing::error; @@ -72,38 +70,11 @@ pub enum BalanceIdentifier { PenalizedPayment, } -/// Error converting a transaction initiator into a balance identifier. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum BalanceIdentifierFromInitiatorError { - /// EVM initiators require EVM origin resolution before they can identify a purse. - EvmAddress(evm::Address), -} - -impl Display for BalanceIdentifierFromInitiatorError { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - match self { - BalanceIdentifierFromInitiatorError::EvmAddress(address) => { - write!( - formatter, - "EVM initiator address {address:?} cannot directly identify a balance" - ) - } - } - } -} - -impl TryFrom for BalanceIdentifier { - type Error = BalanceIdentifierFromInitiatorError; - - fn try_from(value: InitiatorAddr) -> Result { +impl From for BalanceIdentifier { + fn from(value: InitiatorAddr) -> Self { match value { - InitiatorAddr::PublicKey(public_key) => Ok(BalanceIdentifier::Public(public_key)), - InitiatorAddr::AccountHash(account_hash) => { - Ok(BalanceIdentifier::Account(account_hash)) - } - InitiatorAddr::EvmAddress(address) => { - Err(BalanceIdentifierFromInitiatorError::EvmAddress(address)) - } + InitiatorAddr::PublicKey(public_key) => BalanceIdentifier::Public(public_key), + InitiatorAddr::AccountHash(account_hash) => BalanceIdentifier::Account(account_hash), } } } diff --git a/storage/src/data_access_layer/handle_fee.rs b/storage/src/data_access_layer/handle_fee.rs index 8ad4d0e5d5..35967acf2d 100644 --- a/storage/src/data_access_layer/handle_fee.rs +++ b/storage/src/data_access_layer/handle_fee.rs @@ -13,7 +13,7 @@ pub enum HandleFeeMode { /// Pay the fee. Pay { /// Initiator. - initiator_addr: Box, + initiator_addr: Option>, /// Source. source: Box, /// Target. @@ -42,7 +42,7 @@ pub enum HandleFeeMode { impl HandleFeeMode { /// Ctor for Pay mode. pub fn pay( - initiator_addr: Box, + initiator_addr: Option>, source: BalanceIdentifier, target: BalanceIdentifier, amount: U512, diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index 8af12d9b99..20b8e015b2 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -1171,9 +1171,7 @@ pub trait StateProvider: Send + Sync + Sized { Err(err) => return BiddingResult::Failure(TrackingCopyError::Storage(err)), }; - let source_account_hash = initiator - .account_hash() - .expect("bidding initiator must be a Casper account"); + let source_account_hash = initiator.account_hash(); let (entity_addr, mut footprint, mut entity_access_rights) = match tc .borrow_mut() .authorized_runtime_footprint_with_access_rights( @@ -1492,7 +1490,7 @@ pub trait StateProvider: Send + Sync + Sized { // pay amount from source to target match runtime .transfer( - initiator_addr.account_hash(), + Some(initiator_addr.account_hash()), source_purse, target_purse, refund_amount, @@ -1562,7 +1560,7 @@ pub trait StateProvider: Send + Sync + Sized { }; match runtime .transfer( - initiator_addr.account_hash(), + Some(initiator_addr.account_hash()), source_purse, target_purse, refund_amount, @@ -1704,7 +1702,9 @@ pub trait StateProvider: Send + Sync + Sized { }; runtime .transfer( - initiator_addr.account_hash(), + initiator_addr + .as_ref() + .map(|initiator_addr| initiator_addr.account_hash()), source_purse, target_purse, amount, @@ -2081,10 +2081,7 @@ pub trait StateProvider: Send + Sync + Sized { } }; - let source_account_hash = request - .initiator() - .account_hash() - .expect("transfer initiator must be a Casper account"); + let source_account_hash = request.initiator().account_hash(); let protocol_version = request.protocol_version(); if let Err(tce) = tc .borrow_mut() @@ -2325,10 +2322,7 @@ pub trait StateProvider: Send + Sync + Sized { } }; - let source_account_hash = request - .initiator() - .account_hash() - .expect("burn initiator must be a Casper account"); + let source_account_hash = request.initiator().account_hash(); let protocol_version = request.protocol_version(); if let Err(tce) = tc .borrow_mut() diff --git a/types/src/execution/evm_execution_result.rs b/types/src/execution/evm_execution_result.rs index d0b0f58b09..9b980de710 100644 --- a/types/src/execution/evm_execution_result.rs +++ b/types/src/execution/evm_execution_result.rs @@ -15,7 +15,7 @@ use super::Effects; use crate::testing::TestRng; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, - evm, Gas, InitiatorAddr, U512, + evm, Gas, U512, }; /// The result of executing a single EVM transaction. @@ -25,7 +25,7 @@ use crate::{ #[serde(deny_unknown_fields)] pub struct EvmExecutionResult { /// Who initiated this EVM transaction. - pub initiator: InitiatorAddr, + pub initiator: evm::Address, /// The current Casper gas price used for fee accounting. pub current_price: u8, /// The maximum allowed gas limit for this transaction. @@ -50,7 +50,7 @@ impl EvmExecutionResult { let gas_price = rng.gen_range(1..6); let cost = limit.value() * U512::from(gas_price); EvmExecutionResult { - initiator: InitiatorAddr::random(rng), + initiator: evm::Address::new(rng.gen()), current_price: gas_price, limit, cost, @@ -94,7 +94,7 @@ impl ToBytes for EvmExecutionResult { impl FromBytes for EvmExecutionResult { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (initiator, remainder) = InitiatorAddr::from_bytes(bytes)?; + let (initiator, remainder) = evm::Address::from_bytes(bytes)?; let (current_price, remainder) = u8::from_bytes(remainder)?; let (limit, remainder) = Gas::from_bytes(remainder)?; let (cost, remainder) = U512::from_bytes(remainder)?; diff --git a/types/src/gens.rs b/types/src/gens.rs index 6fef440972..f7a9d77157 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -1320,8 +1320,6 @@ pub fn initiator_addr_arb() -> impl Strategy { prop_oneof![ public_key_arb_no_system().prop_map(InitiatorAddr::PublicKey), u2_slice_32().prop_map(|hash| InitiatorAddr::AccountHash(AccountHash::new(hash))), - array::uniform20(any::()) - .prop_map(|address| InitiatorAddr::EvmAddress(evm::Address::new(address))), ] } diff --git a/types/src/transaction.rs b/types/src/transaction.rs index 9cbd802b45..c9db53cbfa 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -304,12 +304,12 @@ impl Transaction { } } - /// Returns the address of the initiator of the transaction. - pub fn initiator_addr(&self) -> InitiatorAddr { + /// Returns the Casper initiator address, if this transaction has one. + pub fn initiator_addr(&self) -> Option { match self { - Transaction::Deploy(deploy) => InitiatorAddr::PublicKey(deploy.account().clone()), - Transaction::V1(txn) => txn.initiator_addr().clone(), - Transaction::Evm(txn) => InitiatorAddr::EvmAddress(txn.from()), + Transaction::Deploy(deploy) => Some(InitiatorAddr::PublicKey(deploy.account().clone())), + Transaction::V1(txn) => Some(txn.initiator_addr().clone()), + Transaction::Evm(_) => None, } } diff --git a/types/src/transaction/deploy.rs b/types/src/transaction/deploy.rs index b817aa4bda..15208ea1f8 100644 --- a/types/src/transaction/deploy.rs +++ b/types/src/transaction/deploy.rs @@ -216,7 +216,7 @@ impl Deploy { let account = match initiator_addr_and_secret_key.initiator_addr() { InitiatorAddr::PublicKey(public_key) => public_key, - InitiatorAddr::AccountHash(_) | InitiatorAddr::EvmAddress(_) => unreachable!(), + InitiatorAddr::AccountHash(_) => unreachable!(), }; let dependencies = dependencies.into_iter().unique().collect(); diff --git a/types/src/transaction/initiator_addr.rs b/types/src/transaction/initiator_addr.rs index 0d1b5ebe22..215b035125 100644 --- a/types/src/transaction/initiator_addr.rs +++ b/types/src/transaction/initiator_addr.rs @@ -7,7 +7,6 @@ use crate::{ Error::{self, Formatting}, FromBytes, ToBytes, }, - evm, transaction::serialization::CalltableSerializationEnvelopeBuilder, AsymmetricType, PublicKey, }; @@ -29,9 +28,6 @@ const PUBLIC_KEY_FIELD_INDEX: u16 = 1; const ACCOUNT_HASH_VARIANT_TAG: u8 = 1; const ACCOUNT_HASH_FIELD_INDEX: u16 = 1; -const EVM_ADDRESS_VARIANT_TAG: u8 = 2; -const EVM_ADDRESS_FIELD_INDEX: u16 = 1; - /// The address of the initiator of a [`crate::Transaction`]. #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] @@ -46,42 +42,23 @@ pub enum InitiatorAddr { PublicKey(PublicKey), /// The account hash derived from the public key of the initiator. AccountHash(AccountHash), - /// The EVM-native address recovered from the signed Ethereum transaction. - EvmAddress(evm::Address), } impl InitiatorAddr { - /// Returns the Casper account hash carried by this initiator, if it has one. - /// - /// EVM transaction initiators carry only a 20-byte EVM address. That address - /// may later resolve to a linked Casper account hash through global state and - /// signature context, but the mapping is not intrinsic to the initiator value. - /// EVM-aware code should use [`InitiatorAddr::evm_address`] or - /// [`crate::Transaction::evm_initiator_addr`] and perform explicit EVM origin - /// resolution where the transaction signer and state root are available. - pub fn account_hash(&self) -> Option { - match self { - InitiatorAddr::PublicKey(public_key) => Some(public_key.to_account_hash()), - InitiatorAddr::AccountHash(hash) => Some(*hash), - InitiatorAddr::EvmAddress(_) => None, - } - } - - /// Returns the native EVM address if this is an EVM initiator. - pub fn evm_address(&self) -> Option { + /// Returns the Casper account hash carried by this initiator. + pub fn account_hash(&self) -> AccountHash { match self { - InitiatorAddr::EvmAddress(address) => Some(*address), - InitiatorAddr::PublicKey(_) | InitiatorAddr::AccountHash(_) => None, + InitiatorAddr::PublicKey(public_key) => public_key.to_account_hash(), + InitiatorAddr::AccountHash(hash) => *hash, } } /// Returns a random `InitiatorAddr`. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..=2) { + match rng.gen_range(0..=1) { 0 => InitiatorAddr::PublicKey(PublicKey::random(rng)), 1 => InitiatorAddr::AccountHash(rng.gen()), - 2 => InitiatorAddr::EvmAddress(evm::Address::new(rng.gen())), _ => unreachable!(), } } @@ -100,12 +77,6 @@ impl InitiatorAddr { hash.serialized_length(), ] } - InitiatorAddr::EvmAddress(address) => { - vec![ - crate::bytesrepr::U8_SERIALIZED_LENGTH, - address.serialized_length(), - ] - } } } } @@ -125,12 +96,6 @@ impl ToBytes for InitiatorAddr { .add_field(ACCOUNT_HASH_FIELD_INDEX, &hash)? .binary_payload_bytes() } - InitiatorAddr::EvmAddress(address) => { - CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? - .add_field(TAG_FIELD_INDEX, &EVM_ADDRESS_VARIANT_TAG)? - .add_field(EVM_ADDRESS_FIELD_INDEX, &address)? - .binary_payload_bytes() - } } } fn serialized_length(&self) -> usize { @@ -163,15 +128,6 @@ impl FromBytes for InitiatorAddr { } Ok(InitiatorAddr::AccountHash(hash)) } - EVM_ADDRESS_VARIANT_TAG => { - let window = window.ok_or(Formatting)?; - window.verify_index(EVM_ADDRESS_FIELD_INDEX)?; - let (address, window) = window.deserialize_and_maybe_next::()?; - if window.is_some() { - return Err(Formatting); - } - Ok(InitiatorAddr::EvmAddress(address)) - } _ => Err(Formatting), }; to_ret.map(|endpoint| (endpoint, remainder)) @@ -190,12 +146,6 @@ impl From for InitiatorAddr { } } -impl From for InitiatorAddr { - fn from(address: evm::Address) -> Self { - InitiatorAddr::EvmAddress(address) - } -} - impl Display for InitiatorAddr { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { match self { @@ -205,9 +155,6 @@ impl Display for InitiatorAddr { InitiatorAddr::AccountHash(account_hash) => { write!(formatter, "account hash {}", account_hash) } - InitiatorAddr::EvmAddress(address) => { - write!(formatter, "EVM address {}", address) - } } } } @@ -223,9 +170,6 @@ impl Debug for InitiatorAddr { .debug_tuple("AccountHash") .field(account_hash) .finish(), - InitiatorAddr::EvmAddress(address) => { - formatter.debug_tuple("EvmAddress").field(address).finish() - } } } }