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/Cargo.lock b/Cargo.lock index 4fdc26be77..469b335003 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,203 @@ 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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b151e38e42f1586a01369ec52a6934702731d07e8509a7307331b09f6c46dc" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f076d25ddfcd2f1cbcc234e072baf97567d1df0e3fccdc1f8af8cc8b18dc6299" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "borsh", + "c-kzg", + "derive_more 2.0.1", + "either", + "serde", + "serde_with", + "sha2", + "thiserror 2.0.12", +] + +[[package]] +name = "alloy-primitives" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if 1.0.0", + "const-hex", + "derive_more 2.0.1", + "foldhash", + "hashbrown 0.16.1", + "indexmap 2.9.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.4", + "rapidhash", + "ruint", + "rustc-hash 2.1.2", + "serde", + "sha3", +] + +[[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.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ece63b89294b8614ab3f483560c08d016930f842bf36da56bf0b764a15c11e" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-trie" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more 2.0.1", + "nybbles", + "smallvec", + "thiserror 2.0.12", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8e52276fdb553d3c11563afad2898f4085165e4093604afe3d78b69afbf408f" +dependencies = [ + "alloy-primitives", + "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" @@ -195,6 +392,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" @@ -284,6 +771,27 @@ dependencies = [ "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]] name = "autocfg" version = "1.4.0" @@ -356,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", @@ -365,7 +873,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.101", ] @@ -385,6 +893,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" @@ -393,9 +917,25 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] [[package]] name = "blake2" @@ -457,6 +997,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 +1078,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 +1146,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 +1236,7 @@ checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.26", "serde", "serde_json", "thiserror 1.0.69", @@ -674,7 +1250,7 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.26", "serde", "serde_json", "thiserror 2.0.12", @@ -690,7 +1266,7 @@ dependencies = [ "num-derive", "num-traits", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "serde_test", @@ -718,7 +1294,7 @@ dependencies = [ "blake2-rfc", "casper-contract-sdk-sys", "casper-executor-wasm-common", - "darling", + "darling 0.20.11", "paste", "proc-macro2", "quote", @@ -731,7 +1307,7 @@ name = "casper-contract-sdk" version = "0.1.3" dependencies = [ "base16", - "bitflags 2.9.1", + "bitflags 2.11.1", "bnum", "borsh", "bytes", @@ -744,7 +1320,7 @@ dependencies = [ "impl-trait-for-tuples", "linkme", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "thiserror 2.0.12", @@ -785,7 +1361,7 @@ dependencies = [ "num-rational", "num-traits", "once_cell", - "rand", + "rand 0.8.5", "serde", "tempfile", "toml 0.5.11", @@ -818,7 +1394,7 @@ dependencies = [ "num-rational", "num-traits", "once_cell", - "rand", + "rand 0.8.5", "regex", "serde", "serde_json", @@ -861,9 +1437,9 @@ dependencies = [ "num_cpus", "once_cell", "proptest", - "rand", - "rand_chacha", - "schemars", + "rand 0.8.5", + "rand_chacha 0.3.1", + "schemars 0.8.22", "serde", "serde_bytes", "serde_json", @@ -878,6 +1454,19 @@ dependencies = [ "wat", ] +[[package]] +name = "casper-executor-evm" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "casper-storage", + "casper-types", + "revm", + "thiserror 2.0.12", +] + [[package]] name = "casper-executor-wasm" version = "0.1.3" @@ -908,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", @@ -977,6 +1566,9 @@ dependencies = [ name = "casper-node" version = "2.2.0" dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", "ansi_term", "anyhow", "aquamarine", @@ -990,6 +1582,7 @@ dependencies = [ "bytes", "casper-binary-port", "casper-execution-engine", + "casper-executor-evm", "casper-executor-wasm", "casper-executor-wasm-interface", "casper-storage", @@ -1011,6 +1604,7 @@ dependencies = [ "humantime", "hyper", "itertools 0.10.5", + "k256", "libc", "linked-hash-map", "lmdb-rkv", @@ -1030,14 +1624,15 @@ 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", + "revm", "rmp", "rmp-serde", - "schemars", + "schemars 0.8.22", "serde", "serde-big-array", "serde-map-to-array", @@ -1064,7 +1659,7 @@ dependencies = [ "tower", "tracing", "tracing-futures", - "tracing-subscriber", + "tracing-subscriber 0.3.20", "uint", "uuid 0.8.2", "warp", @@ -1094,8 +1689,8 @@ dependencies = [ "parking_lot", "pprof", "proptest", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_json", "tempfile", @@ -1108,6 +1703,9 @@ dependencies = [ name = "casper-types" version = "7.0.0" dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", "base16", "base64 0.13.1", "bincode", @@ -1120,6 +1718,7 @@ dependencies = [ "ed25519-dalek", "getrandom 0.2.16", "hex", + "hex-literal", "hex_fmt", "humantime", "itertools 0.10.5", @@ -1136,9 +1735,9 @@ dependencies = [ "proptest", "proptest-attr-macro", "proptest-derive", - "rand", + "rand 0.8.5", "rand_pcg", - "schemars", + "schemars 0.8.22", "serde", "serde-map-to-array", "serde_bytes", @@ -1161,7 +1760,7 @@ dependencies = [ "clap 4.5.38", "once_cell", "regex", - "semver", + "semver 1.0.26", ] [[package]] @@ -1279,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" @@ -1453,12 +2064,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 +2241,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" @@ -1736,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", @@ -1770,7 +2429,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 +2475,7 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "rustc_version", + "rustc_version 0.4.1", "subtle", "zeroize", ] @@ -1838,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]] @@ -1856,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", ] @@ -1938,6 +2632,38 @@ 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" +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 +2673,7 @@ dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "syn 2.0.101", ] @@ -1970,6 +2696,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.101", + "unicode-xid", ] [[package]] @@ -2166,6 +2893,7 @@ dependencies = [ "elliptic-curve", "rfc6979", "signature 2.2.0", + "spki", ] [[package]] @@ -2198,12 +2926,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 +3144,8 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core", + "pkcs8", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -2496,6 +3237,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" @@ -2511,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", @@ -2607,6 +3368,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 +3413,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 +3464,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 +3492,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -2737,6 +3538,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,7 +3892,7 @@ dependencies = [ "clap 2.34.0", "itertools 0.10.5", "lmdb-rkv", - "rand", + "rand 0.8.5", "serde", "toml 0.5.11", ] @@ -3097,7 +3904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3158,12 +3965,20 @@ name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", +] [[package]] 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" @@ -3268,6 +4083,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 +4127,7 @@ version = "0.1.0" dependencies = [ "casper-contract", "casper-types", - "rand", + "rand 0.8.5", ] [[package]] @@ -3399,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" @@ -3518,6 +4372,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 +4444,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.3", + "serde", ] [[package]] @@ -3686,15 +4550,35 @@ dependencies = [ ] [[package]] -name = "k256" -version = "0.13.4" +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", +] + +[[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 = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +checksum = "fa468878266ad91431012b3e5ef1bf9b170eab22883503a318d46857afa4579a" dependencies = [ - "cfg-if 1.0.0", - "ecdsa", - "elliptic-curve", - "sha2", + "digest 0.10.7", + "sha3-asm", ] [[package]] @@ -3713,6 +4597,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" @@ -3759,7 +4658,7 @@ 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", ] @@ -4216,7 +5115,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]] @@ -4252,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" @@ -4312,6 +5217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4324,6 +5230,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" @@ -4371,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", @@ -4450,6 +5389,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", +] + +[[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 +5483,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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -4677,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" @@ -4731,6 +5769,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" @@ -4817,11 +5875,11 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.1", + "bitflags 2.11.1", "lazy_static", "num-traits", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -4897,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", ] @@ -4980,6 +6038,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 +6060,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 +6082,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 +6104,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 +6129,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]] @@ -5053,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" @@ -5112,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]] @@ -5360,19 +6484,211 @@ dependencies = [ ] [[package]] -name = "ret-uref" -version = "0.1.0" +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 = "38.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91202d39dbe8e8d10e9e8f2b76c30da68ecd1d25be69ba6d853ad0d03a3a398a" +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 = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbb3a3d735efa94c91f2ef6bf20a35f99a77bc78f3e25bd758336901bdf9661" +dependencies = [ + "bitvec", + "phf", + "revm-primitives", + "serde", +] + +[[package]] +name = "revm-context" +version = "16.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f68d928d8b228e0faeb1c6ed75c4fde7d124f1ddf9119b67e7a0ad4041237d" +dependencies = [ + "bitvec", + "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 = "17.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3758e6167c4ba7a59a689c519a047edaefcd4c37d74f279b93ed87bc8aece4" +dependencies = [ + "alloy-eip2930", + "alloy-eip7702", + "auto_impl", + "either", + "revm-database-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-database" +version = "13.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c281a1f11d3bcb8c0bba1199ed6bcb001d1aeb3d4fb366819e14f88723989a4e" +dependencies = [ + "alloy-eips", + "revm-bytecode", + "revm-database-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-database-interface" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89efb9832a4e3742bb4ded5f7fe5bf905e8860e69427d4dfec153484fc6d304" +dependencies = [ + "auto_impl", + "either", + "revm-primitives", + "revm-state", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "revm-handler" +version = "18.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "783e903d6922b7f5f9a940d1bb229530502d2924b1aed9d5ca5a94ebf065d460" +dependencies = [ + "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 = "revm-inspector" +version = "19.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8216ad58422090d0daa9eb430e0a081f7ad07e7fd30681dee71f8420c99624e0" +dependencies = [ + "auto_impl", + "either", + "revm-context", + "revm-database-interface", + "revm-handler", + "revm-interpreter", + "revm-primitives", + "revm-state", + "serde", + "serde_json", +] + +[[package]] +name = "revm-interpreter" +version = "35.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ece9f41b69658c15d748288a4dbdfc06a63f3ce93d983af440de3f1631dce6a" +dependencies = [ + "revm-bytecode", + "revm-context-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-precompile" +version = "34.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a346a8cc6c8c39bd65306641c692191299c0a7b63d38810e39e8fe9b92378660" +dependencies = [ + "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", + "p256", + "revm-context-interface", + "revm-primitives", + "ripemd", + "secp256k1", + "sha2", +] + +[[package]] +name = "revm-primitives" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c99bda77d9661521ba0b4bc04558c6692074f01e65dd420fa3b893033d9b8a2" dependencies = [ - "casper-contract", - "casper-types", + "alloy-primitives", + "num_enum", + "once_cell", + "serde", ] [[package]] -name = "revert" -version = "0.1.0" +name = "revm-state" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c32490ed687dba31c3c882beb8c20408bdd30ef96690d8f145b0ee9a87040bfe" dependencies = [ - "casper-contract", - "casper-types", + "alloy-eip7928", + "bitflags 2.11.1", + "revm-bytecode", + "revm-primitives", + "serde", ] [[package]] @@ -5408,6 +6724,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 +6763,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 +6795,39 @@ dependencies = [ "serde", ] +[[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 +6840,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]] @@ -5487,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", @@ -5610,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" @@ -5643,17 +7056,38 @@ 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" 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", @@ -5676,6 +7110,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 +7128,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", ] @@ -5709,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", ] @@ -5733,11 +7186,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", @@ -5809,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" @@ -5839,6 +7333,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 +7421,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 +7430,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 +7730,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 +7864,46 @@ 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 = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "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]] name = "tinystr" version = "0.8.1" @@ -6429,7 +7995,7 @@ checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" dependencies = [ "bincode", "bytes", - "educe", + "educe 0.4.23", "futures-core", "futures-sink", "pin-project", @@ -6665,6 +8231,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 +8424,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", "thiserror 1.0.69", "url", @@ -6872,6 +8447,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 +8509,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 +8653,7 @@ dependencies = [ "proc-macro2", "pulldown-cmark", "regex", - "semver", + "semver 1.0.26", "syn 2.0.101", "toml 0.7.8", "url", @@ -7553,10 +9140,10 @@ 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", + "semver 1.0.26", ] [[package]] @@ -7565,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", ] @@ -7575,9 +9162,9 @@ 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", + "semver 1.0.26", ] [[package]] @@ -7690,6 +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" @@ -7717,6 +9363,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" @@ -7936,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]] @@ -7953,6 +9608,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 +9703,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..389280e7ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "executor/wasm_host", "executor/wasmer_backend", "executor/wasm", + "executor/evm", ] default-members = [ @@ -53,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..a15151d959 --- /dev/null +++ b/EVM.md @@ -0,0 +1,980 @@ +# 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 binary-port speculative execution 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. Type `0x04` transactions are accepted with non-empty authorization lists; Casper still rejects non-empty access lists and non-zero priority fees. | + +## 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 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. +- Contract runtime execution for finalized `Transaction::Evm` values. +- Casper fee and refund handling for EVM transactions. +- 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 + lists passed through to `revm` for Prague execution. + +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`. +- `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. + +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. +- 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 + +`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`. +- [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. + +`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: + +```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. + - Non-empty access lists. + - Unknown typed transactions. +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 `EvmApproval`: + - `Approval.signer` is the recovered secp256k1 public key. + - `Approval.signature` is the 64-byte secp256k1 `(r, s)` signature. + - `EvmApproval.y_parity` is the Ethereum recovery parity carried by the + signed payload as legacy `v` or typed-transaction `yParity`. +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 +receives a typed `Transaction::Evm` whose EVM approval contains the Ethereum +signature and recovery parity. + +## Approval Handling + +`Transaction::Evm` stores one `EvmApproval`, a wrapper around the normal Casper +`Approval` plus Ethereum `y_parity`. The wrapper is needed because +`Signature::Secp256k1` is only the canonical 64-byte ECDSA signature, `r || s`; +it does not contain Ethereum's recovery parity. Without storing that parity, +the node has to infer it by trying recovery IDs when reconstructing the signed +Ethereum envelope. + +For transaction identity and block approval accounting, `Transaction::Evm` +still exposes the wrapped Casper approval through the same approval 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 EVM approval. +- The approval uses secp256k1. +- The stored `y_parity` and `(r, s)` signature recover 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 inside the `Transaction::Evm` approval wrapper. + +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 native Transaction::V1 payloads: 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. 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 every EVM +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 +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 native Transaction::V1 payloads. +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, authorization keys, size estimate, gas limit, and +cost is derived directly from `Transaction`. For EVM: + +- 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`, +- 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 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: + - block height, + - block timestamp, + - deterministic proposer-derived beneficiary, + - `[evm].block_gas_limit`, + - `[evm].base_fee`. +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. +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`. + +The executor always disables `revm` gas fee balance mutation. 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 the resolved EVM payer. 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 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 +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::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::ByteCode(ByteCode)`. +- `Key::Evm(EvmAddr::Storage(StorageAddr))` stores + `StoredValue::CLValue(U256)`. + +`StoredValue::Evm` is not part of the current layout. + +Balances are Casper purse balances. EVM balance reads and writes reconcile +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 +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 + +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 the node binary-port `TrySpeculativeExec` command, not +`TryAcceptTransaction` submission. + +Sidecar constructs a `Transaction::Evm` with +`evm::Transaction::new_unsigned_call`, which carries: + +- chain ID, +- `from`, +- `to`, +- `value`, +- input bytes, +- 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, receipt status, and gas used in +`EvmSpeculativeExecutionResult`, carried by the contract-runtime +`SpeculativeExecutionResult::Evm` variant. +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. Use a node config where +`[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. + +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_speculative_exec = true. +``` + +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 "$EVM_DEVNET_NODE_CONFIG" \ + --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 "$EVM_DEVNET_NODE_CONFIG" \ + --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 +``` + +### No EVM Prefund Required + +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 + +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: +``` + +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 +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 +export COUNTER_ADDRESS=0x6c0704679CA22b83778Ef815607359cf6F5352B6 + +cast send "$COUNTER_ADDRESS" \ + '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 +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 +cast call 0x6c0704679CA22b83778Ef815607359cf6F5352B6 \ + 'get()(uint256)' \ + --rpc-url http://127.0.0.1:11101/rpc +``` + +Expected output: + +```text +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 +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 +``` + +With `--gas-price 1000000`, every 1,000 gas consumed is 1 CSPR before refund +policy is applied. + +## Useful Checks + +Node workspace: + +```bash +cargo check -p casper-node --bin casper-node +cargo test -p casper-binary-port --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. +- `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. +- 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/Makefile b/Makefile index 79b26f463d..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 @@ -16,10 +35,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 +76,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 @./$< @@ -108,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..4f9ddb35a3 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::{EvmTransactionError, InvalidDeploy, InvalidTransaction, InvalidTransactionV1}; use num_derive::FromPrimitive; use num_traits::FromPrimitive; @@ -370,6 +370,12 @@ 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, + /// 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 { @@ -397,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(EvmTransactionError::InvalidNonce { .. }) => { + ErrorCode::InvalidTransactionEvmInvalidNonce + } _ => ErrorCode::InvalidTransactionOrDeployUnspecified, } } @@ -552,7 +561,7 @@ impl From for ErrorCode { InvalidTransactionV1::UnexpectedEntryPoint { .. } => { ErrorCode::InvalidTransactionUnexpectedEntryPoint } - InvalidTransactionV1::CouldNotSerializeTransaction { .. } => { + InvalidTransactionV1::CouldNotSerializeTransaction => { ErrorCode::TransactionHasMalformedBinaryRepresentation } InvalidTransactionV1::InsufficientAmount { .. } => { @@ -581,7 +590,9 @@ mod tests { use std::convert::TryFrom; use crate::ErrorCode; - use casper_types::{InvalidDeploy, InvalidTransactionV1}; + use casper_types::{ + EvmTransactionError, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, + }; use strum::IntoEnumIterator; #[test] @@ -618,6 +629,19 @@ mod tests { } } + #[test] + fn evm_invalid_nonce_has_specific_error_code() { + let error = InvalidTransaction::Evm(EvmTransactionError::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/binary_port/src/key_prefix.rs b/binary_port/src/key_prefix.rs index 774e3a36b0..70e4c82750 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, EvmAddr}, 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,11 @@ impl ToBytes for KeyPrefix { writer.push(1); entity.write_bytes(writer)?; } + KeyPrefix::EvmStorageByAddress(address) => { + writer.push(KeyTag::Evm as u8); + writer.push(EvmAddr::STORAGE_TAG); + address.write_bytes(writer)?; + } } Ok(()) } @@ -123,6 +132,9 @@ impl ToBytes for KeyPrefix { KeyPrefix::EntryPointsV2ByEntity(entity) => { U8_SERIALIZED_LENGTH + entity.serialized_length() } + KeyPrefix::EvmStorageByAddress(address) => { + U8_SERIALIZED_LENGTH + address.serialized_length() + } } } } @@ -182,6 +194,16 @@ impl FromBytes for KeyPrefix { _ => return Err(bytesrepr::Error::Formatting), } } + 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), }; Ok(result) diff --git a/binary_port/src/lib.rs b/binary_port/src/lib.rs index dacd9ab7cb..b341c6a2ad 100644 --- a/binary_port/src/lib.rs +++ b/binary_port/src/lib.rs @@ -46,7 +46,7 @@ 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, diff --git a/binary_port/src/response_type.rs b/binary_port/src/response_type.rs index b9cff4caad..e3c8e06154 100644 --- a/binary_port/src/response_type.rs +++ b/binary_port/src/response_type.rs @@ -18,7 +18,7 @@ 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, @@ -119,6 +119,8 @@ pub enum ResponseType { PackageWithProof, /// Addressable entity information. AddressableEntityInformation, + /// Result of the EVM speculative execution. + EvmSpeculativeExecutionResult, } 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,9 @@ impl TryFrom for ResponseType { x if x == ResponseType::AddressableEntityInformation as u8 => { Ok(ResponseType::AddressableEntityInformation) } + x if x == ResponseType::EvmSpeculativeExecutionResult as u8 => { + Ok(ResponseType::EvmSpeculativeExecutionResult) + } _ => Err(()), } } @@ -290,6 +295,9 @@ impl fmt::Display for ResponseType { ResponseType::AddressableEntityInformation => { write!(f, "AddressableEntityInformation") } + ResponseType::EvmSpeculativeExecutionResult => { + write!(f, "EvmSpeculativeExecutionResult") + } } } } @@ -388,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; } 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/execution_engine_testing/test_support/src/transfer_request_builder.rs b/execution_engine_testing/test_support/src/transfer_request_builder.rs index 1e480509e0..46da00f8f0 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/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/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/executor/evm/Cargo.toml b/executor/evm/Cargo.toml new file mode 100644 index 0000000000..4b3fd5dcde --- /dev/null +++ b/executor/evm/Cargo.toml @@ -0,0 +1,20 @@ +[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] +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"] } +thiserror = "2" + +[dev-dependencies] +alloy-consensus = { version = "=1.0.41", default-features = false, features = ["k256"] } +alloy-primitives = { version = "=1.5.7", default-features = false, features = ["rlp", "sha3-keccak"] } diff --git a/executor/evm/src/account_state.rs b/executor/evm/src/account_state.rs new file mode 100644 index 0000000000..1175908e20 --- /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, EvmAddr, 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(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(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(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(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(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(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(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/block_hash.rs b/executor/evm/src/block_hash.rs new file mode 100644 index 0000000000..e565e190e0 --- /dev/null +++ b/executor/evm/src/block_hash.rs @@ -0,0 +1,62 @@ +//! 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::{BlockHash, 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| header.block_hash())) + } +} diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs new file mode 100644 index 0000000000..2b1b8d9549 --- /dev/null +++ b/executor/evm/src/db.rs @@ -0,0 +1,152 @@ +//! 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, EvmAddr, Key, StoredValue, U512}; +use revm::{ + database_interface::Database, + primitives::{Address, Bytes, StorageKey, StorageValue, B256, U256}, + state::{AccountInfo, Bytecode}, +}; + +use crate::{account_state, tx, BlockHashProvider, DbError}; + +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, B> CasperDb<'a, R, B> +where + R: StateReader, + B: BlockHashProvider + ?Sized, +{ + 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 { + 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, B> +where + R: StateReader, + B: BlockHashProvider + ?Sized, +{ + type Error = DbError; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + let address = tx::from_revm_address(address); + 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(EvmAddr::ByteCode(code_hash)); + match self.tracking_copy.read(&key)? { + Some(StoredValue::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::ByteCode", + 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_storage_word(index); + let key = Key::Evm(EvmAddr::Storage(evm::StorageAddr::new(address, slot))); + match self.tracking_copy.read(&key)? { + 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::CLValue(U256)", + found: stored_value.type_name(), + }), + None => Ok(U256::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_block_hash) + .unwrap_or(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..9e92fd06f2 --- /dev/null +++ b/executor/evm/src/error.rs @@ -0,0 +1,132 @@ +//! Error types returned by the Casper EVM executor. + +use casper_storage::tracking_copy::TrackingCopyError; +use casper_types::Key; + +use crate::{account_state::AccountStorageError, BlockHashProviderError}; + +/// 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, + /// 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 { + /// 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 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 { + /// 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, + }, + /// 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 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/executor.rs b/executor/evm/src/executor.rs new file mode 100644 index 0000000000..4b2b9755d8 --- /dev/null +++ b/executor/evm/src/executor.rs @@ -0,0 +1,178 @@ +//! Public executor entry point. + +use casper_storage::{ + global_state::{error::Error as GlobalStateError, state::StateReader}, + TrackingCopy, +}; +use casper_types::{EvmConfig, EvmSpec, Key, StoredValue}; +use revm::{ + 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, +}; + +/// 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: EvmConfig, +} + +impl EvmExecutor { + /// Creates a new executor from chainspec EVM configuration. + pub fn new(config: EvmConfig) -> Self { + Self { config } + } + + /// Returns the immutable EVM configuration used by this executor. + pub fn config(&self) -> &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, + { + 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 { + 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 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, 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 = !skip_validation; + cfg.disable_block_gas_limit = false; + cfg.disable_base_fee = skip_validation; + cfg.disable_balance_check = skip_validation; + cfg.disable_nonce_check = skip_validation; + cfg.disable_fee_charge = true; + }) + .build_mainnet(); + + evm.transact(tx_env).map_err(map_revm_error)? + }; + + let outcome = ExecutionOutcome::from_revm_result(&result_and_state.result); + let mut state = result_and_state.state; + // 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, &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: &EvmConfig, + 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); + let coinbase_gas_price = effective_gas_price.saturating_sub(base_fee); + 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: EvmSpec) -> SpecId { + match spec { + EvmSpec::Prague => SpecId::PRAGUE, + } +} + +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..f978ce71e1 --- /dev/null +++ b/executor/evm/src/lib.rs @@ -0,0 +1,33 @@ +//! Casper EVM executor. +//! +//! 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; +mod executor; +mod outcome; +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}; +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::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 new file mode 100644 index 0000000000..b76f946759 --- /dev/null +++ b/executor/evm/src/outcome.rs @@ -0,0 +1,143 @@ +//! Public execution outcome types. + +use casper_types::evm; +use revm::context_interface::result::{ + ExecutionResult, HaltReason as RevmHaltReason, OutOfGasError as RevmOutOfGasError, 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, 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.tx_gas_used(), + output: output_bytes, + logs: logs.iter().map(from_revm_log).collect(), + created_contract_address, + } + } + ExecutionResult::Revert { gas, output, .. } => Self { + status: ExecutionStatus::Revert, + gas_used: gas.tx_gas_used(), + output: output.to_vec(), + logs: Vec::new(), + created_contract_address: None, + }, + ExecutionResult::Halt { gas, reason, .. } => Self { + status: ExecutionStatus::Halt(from_revm_halt_reason(reason)), + gas_used: gas.tx_gas_used(), + output: Vec::new(), + logs: Vec::new(), + created_contract_address: None, + }, + } + } + + /// 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. +#[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::HaltReason), +} + +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 | RevmHaltReason::PrecompileErrorWithContext(_) => { + 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, + } +} + +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_topic) + .collect(), + data: log.data.data.to_vec().into(), + } +} diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs new file mode 100644 index 0000000000..b6c71fd070 --- /dev/null +++ b/executor/evm/src/request.rs @@ -0,0 +1,91 @@ +//! Public execution request types. + +use casper_types::{evm, EvmConfig, EvmTransaction, U256}; + +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(EvmTransaction), + /// Unsigned local call request. + Call(CallRequest), +} + +/// Unsigned EVM call request. +/// +/// 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`. + 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: U256, + /// 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. + 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. +#[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: &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..96e958d3ec --- /dev/null +++ b/executor/evm/src/state.rs @@ -0,0 +1,192 @@ +//! 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, ByteCode, ByteCodeKind, CLValue, EvmAddr, Key, StoredValue, U512}; +use revm::{ + primitives::{Address, U256}, + state::{Account, EvmState}, +}; + +use crate::{account_state, 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(()) +} + +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, + account: Account, +) -> Result<(), Error> +where + R: StateReader, +{ + // Check how to deal with Key::Balance after selfdestruct + let address = tx::from_revm_address(address); + let account_key = Key::Evm(EvmAddr::Account(address)); + + if account.is_selfdestructed() { + // 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(()); + } + + if let Some(code) = account.info.code.as_ref() { + let bytes = code.original_byte_slice(); + if !bytes.is_empty() { + tracking_copy.write( + Key::Evm(EvmAddr::ByteCode(tx::from_revm_hash( + account.info.code_hash, + ))), + StoredValue::ByteCode(ByteCode::new(ByteCodeKind::EvmPrague, bytes.to_vec())), + ); + } + } + + 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); + + // 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() { + let key = Key::Evm(EvmAddr::Storage(evm::StorageAddr::new( + 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 { + 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)); + } + } + + Ok(()) +} + +fn prune_account( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + account_key: Key, + identity: Option, + main_purse: casper_types::URef, +) -> 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); + } + 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(EvmAddr::Nonce(address))); + tracking_copy.prune(Key::Evm(EvmAddr::CodeHash(address))); + Ok(()) +} + +fn existing_main_purse( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + identity: Option, +) -> Result +where + R: StateReader, +{ + 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)), + } +} + +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..33fbb354cf --- /dev/null +++ b/executor/evm/src/tx.rs @@ -0,0 +1,140 @@ +//! 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, EvmConfig, EvmTransactionKind, U256 as CasperU256}; +use revm::{ + context::TxEnv, + primitives::{Address, Bytes, TxKind, B256, U256}, +}; + +use crate::{Error, ExecuteKind}; + +pub(crate) fn build_tx_env(config: &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() { + EvmTransactionKind::Legacy | EvmTransactionKind::Eip2930 => builder.gas_price( + transaction + .gas_price() + .unwrap_or_else(|| transaction.max_fee_per_gas()), + ), + EvmTransactionKind::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 + // 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(max_priority_fee_per_gas) + } + EvmTransactionKind::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(), + ) + } + }; + + 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()) +} + +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()) +} + +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 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()); + 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_storage_word(value: CasperU256) -> U256 { + to_revm_u256(value) +} + +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 new file mode 100644 index 0000000000..693d15ede0 --- /dev/null +++ b/executor/evm/tests/executor.rs @@ -0,0 +1,1227 @@ +use std::path::PathBuf; + +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, +}; +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::{ + bytesrepr::{FromBytes, ToBytes}, + contracts::NamedKeys, + evm, AccessRights, Account, BlockHash, CLValue, ChainspecRegistry, Digest, EvmAddr, EvmConfig, + EvmSpec, EvmTransaction, 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| { + 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: EvmSpec) -> EvmExecutor { + EvmExecutor::new(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, + } +} + +#[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) -> BlockHash { + let mut bytes = [0u8; BlockHash::LENGTH]; + bytes[24..].copy_from_slice(&block_height.to_be_bytes()); + BlockHash::new(Digest::from_raw(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 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, + input: Vec, + value: CasperU256, +) -> ExecuteRequest { + ExecuteRequest { + block: block(), + kind: ExecuteKind::Call(CallRequest { + from, + to, + value, + input, + 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: CasperU256, +) -> 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, + }), + } +} + +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, CasperU256::zero()), + ) + .expect("EVM execution should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + outcome +} + +fn execute_transaction>( + executor: &EvmExecutor, + tracking_copy: &mut TrackingCopy, + transaction: EvmTransaction, +) -> 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, + 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() +} + +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); + } + bytes +} + +fn word(value: u64) -> AbiWord { + let mut bytes = [0u8; 32]; + bytes[24..].copy_from_slice(&value.to_be_bytes()); + bytes +} + +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()); + 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 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) -> EvmTransaction { + 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(); + EvmTransaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + casper_types::TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") +} + +fn eip7702_transaction( + to: evm::Address, + delegate: evm::Address, + authorization_nonce: u64, + transaction_nonce: u64, + input: Vec, +) -> (EvmTransaction, 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 = EvmTransaction::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: EvmTransaction) -> EvmTransaction { + 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) = + EvmTransaction::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() -> EvmTransaction { + legacy_transaction(None) +} + +fn read_storage>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + slot: CasperU256, +) -> Option { + match tracking_copy + .read(&Key::Evm(EvmAddr::Storage(evm::StorageAddr::new( + address, slot, + )))) + .expect("storage read should not fail") + { + Some(StoredValue::CLValue(value)) => value.into_t::().ok(), + 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::Evm(EvmAddr::Account(address))) + .expect("account read should not fail") + { + 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(), + }; + 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::Evm(EvmAddr::Account(address)), + StoredValue::CLValue(CLValue::from_t(Key::URef(main_purse)).unwrap()), + ); + tracking_copy.write( + Key::Evm(EvmAddr::Nonce(address)), + StoredValue::CLValue(CLValue::from_t(0u64).unwrap()), + ); + tracking_copy.write( + Key::Evm(EvmAddr::CodeHash(address)), + StoredValue::CLValue(CLValue::from_t(EMPTY_CODE_HASH).unwrap()), + ); + tracking_copy.write( + Key::Balance(main_purse.addr()), + StoredValue::CLValue(CLValue::from_t(balance).unwrap()), + ); +} + +fn read_evm_nonce>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> u64 { + match tracking_copy + .read(&Key::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, + } +} + +fn read_code_hash>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> evm::Hash { + match tracking_copy + .read(&Key::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(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(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(), CasperU256::zero()), + &block_hash_provider, + ) + .expect("EVM execution should succeed"); + assert_eq!(outcome.status, ExecutionStatus::Success); + 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; + 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(), &[0u8; evm::HASH_LENGTH]); + + 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( + &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_ref()); +} + +#[test] +fn eip7702_authorization_installs_delegation_and_executes_delegate_code() { + let executor = executor(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(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(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(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(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(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(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 = CasperU256::from(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 nonzero_gas_price_does_not_charge_evm_balances() { + let executor = executor(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(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(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, CasperU256::zero()), + Some(storage_word(123)) + ); + + execute_call( + &executor, + &mut tracking_copy, + from, + Some(contract), + selector("clear()"), + ); + assert_eq!( + read_storage(&mut tracking_copy, contract, CasperU256::zero()), + None + ); +} + +#[test] +fn selfdestruct_preserves_account_on_prague() { + let from = evm::Address::new([1; 20]); + let beneficiary = evm::Address::new([2; 20]); + + let prague_executor = executor(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::Evm(EvmAddr::Account(prague_contract))) + .unwrap() + .is_some()); +} + +#[test] +fn signed_transactions_require_configured_chain_id() { + let executor = executor(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(EvmConfig { + enabled: true, + chain_id: 8, + spec: 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 signed_transaction_sender_uses_linked_casper_account_identity() { + let executor = executor(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(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(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(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(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(EvmSpec::Prague); + let from = evm::Address::new([1; 20]); + let recipient = evm::Address::new([2; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + 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/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 45f1eca4fa..4fb9d365b9 100644 --- a/node/src/components/binary_port.rs +++ b/node/src/components/binary_port.rs @@ -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, @@ -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 { @@ -1367,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 { @@ -1379,6 +1382,9 @@ where SpeculativeExecutionResult::WasmV1(spec_exec_result) => { BinaryResponse::from_value(spec_exec_result) } + SpeculativeExecutionResult::Evm(spec_exec_result) => { + BinaryResponse::from_value(spec_exec_result) + } } } 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/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/contract_runtime.rs b/node/src/components/contract_runtime.rs index 3c0771780b..ba5a36d364 100644 --- a/node/src/components/contract_runtime.rs +++ b/node/src/components/contract_runtime.rs @@ -81,6 +81,7 @@ 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"; @@ -721,6 +722,7 @@ impl ContractRuntime { } ContractRuntimeRequest::SpeculativelyExecute { block_header, + block_hashes, transaction, responder, } => { @@ -734,6 +736,7 @@ impl ContractRuntime { chainspec.as_ref(), execution_engine_v1.as_ref(), *block_header, + block_hashes, *transaction, ) }) diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 4dfb6a2d2e..40541499d6 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -9,6 +9,12 @@ use wasm_v2_request::{WasmV2Request, WasmV2Result}; use casper_execution_engine::engine_state::{ BlockInfo, ExecutionEngineV1, WasmV1Request, WasmV1Result, }; +use casper_executor_evm::{ + BlockContext as EvmBlockContext, BlockHashProvider as EvmBlockHashProvider, + 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, data_access_layer::{ @@ -28,15 +34,23 @@ use casper_storage::{ StateProvider, StateReader, }, system::runtime_native::Config as NativeRuntimeConfig, + tracking_copy::{TrackingCopyEntityExt, TrackingCopyError}, + TrackingCopy, }; use casper_types::{ - bytesrepr::{self, ToBytes, U32_SERIALIZED_LENGTH}, + account::{Account, AccountHash}, + bytesrepr::{self, Bytes, ToBytes, U32_SERIALIZED_LENGTH}, + contracts::NamedKeys, + 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, - EntityAddr, EraEndV2, EraId, FeeHandling, Gas, InvalidTransaction, InvalidTransactionV1, Key, - ProtocolVersion, PublicKey, RefundHandling, 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::{ @@ -51,6 +65,392 @@ 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(), + } +} + +#[derive(Clone, Debug)] +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. +/// +/// 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_runtime_origin( + scratch_state: &ScratchGlobalState, + state_root_hash: Digest, + protocol_version: ProtocolVersion, + transaction: &casper_types::EvmTransaction, +) -> 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::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(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 + // 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(RuntimeOrigin::from_evm_parts( + 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(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(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(RuntimeOrigin::from_evm_parts( + BalanceIdentifier::Purse(deterministic_purse), + 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::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::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( @@ -60,6 +460,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 +610,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 +619,8 @@ 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 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 +644,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; + } } }; @@ -298,6 +715,19 @@ 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 runtime_origin = if let Some(evm_transaction) = evm_transaction { + resolve_evm_runtime_origin( + &scratch_state, + state_root_hash, + protocol_version, + evm_transaction, + )? + } else { + let initiator_addr = stored_transaction.initiator_addr(); + 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 { // if custom payment before doing any processing, initialize the initiator's main purse @@ -310,7 +740,7 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleRefundMode::SetRefundPurse { - target: Box::new(initiator_addr.clone().into()), + target: Box::new(payer_balance_identifier.clone()), }, ); let handle_refund_result = scratch_state.handle_refund(handle_refund_request); @@ -332,7 +762,7 @@ pub fn execute_finalized_block( let initial_balance_result = scratch_state.balance(BalanceRequest::new( state_root_hash, protocol_version, - initiator_addr.clone().into(), + payer_balance_identifier.clone(), balance_handling, ProofHandling::NoProofs, )); @@ -345,6 +775,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 +792,17 @@ pub fn execute_finalized_block( } let mut balance_identifier = { - if is_standard_payment { + 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 + // 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. + payer_balance_identifier.clone() + } else if is_standard_payment { let contract_might_pay = addressable_entity_enabled && transaction.is_contract_by_hash_invocation(); @@ -363,28 +812,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().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))?; - BalanceIdentifier::PenalizedAccount( - initiator_addr.clone().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().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().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 @@ -428,11 +875,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().into(), + payer_balance_identifier.clone(), BalanceIdentifier::Payment, baseline_motes_amount, None, @@ -473,7 +920,7 @@ pub fn execute_finalized_block( BalanceIdentifier::Payment } } else { - BalanceIdentifier::PenalizedAccount(initiator_addr.clone().account_hash()) + BalanceIdentifier::PenalizedAccount(runtime_origin.account_hash()?) } }; @@ -486,8 +933,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 @@ -496,8 +941,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 { @@ -544,6 +1000,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)?; @@ -555,7 +1012,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(), )); @@ -571,7 +1028,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(), )); @@ -589,9 +1046,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( @@ -599,7 +1058,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys, auction_method, )); @@ -622,6 +1081,66 @@ 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()), + }; + let mut tracking_copy = scratch_state + .tracking_copy(state_root_hash)? + .ok_or(BlockExecutionError::RootNotFound(state_root_hash))?; + 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 + // identity state and makes the identity write atomic + // with the revm state transition below. + apply_evm_identity_plan( + &mut tracking_copy, + protocol_version, + identity_plan, + )?; + } + 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(); @@ -715,6 +1234,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( @@ -751,7 +1283,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(), @@ -760,15 +1292,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 { @@ -785,7 +1324,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, @@ -805,9 +1344,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, @@ -891,7 +1435,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, @@ -908,7 +1452,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, @@ -1338,6 +1882,7 @@ pub(super) fn speculatively_execute( chainspec: &Chainspec, execution_engine_v1: &ExecutionEngineV1, block_header: BlockHeader, + block_hashes: BTreeMap, input_transaction: Transaction, ) -> SpeculativeExecutionResult where @@ -1364,14 +1909,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(); @@ -1425,6 +1971,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(), @@ -1445,6 +1998,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( @@ -1453,6 +2014,110 @@ where } } +fn speculatively_execute_evm( + state_provider: &S, + chainspec: &Chainspec, + block_header: BlockHeader, + block_hashes: BTreeMap, + evm_transaction: &casper_types::EvmTransaction, +) -> SpeculativeExecutionResult +where + S: StateProvider, +{ + if !chainspec.evm_config.enabled { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::EvmTransactionError::Disabled, + )); + } + if evm_transaction.gas_limit() > chainspec.evm_config.block_gas_limit { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::EvmTransactionError::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 = match state_provider.tracking_copy(*state_root_hash) { + Ok(Some(tracking_copy)) => tracking_copy, + Ok(None) => { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::EvmTransactionError::Decode(format!( + "state root {state_root_hash} not found" + )), + )) + } + Err(error) => { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::EvmTransactionError::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); + 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 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, + }; + let block_hash_provider = StaticEvmBlockHashProvider { block_hashes }; + 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::EvmTransactionError::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), + ), + )) +} + fn invoked_contract_will_pay( state_provider: &ScratchGlobalState, state_root_hash: Digest, 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..68f7278ac9 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,9 @@ impl WasmV2Request { // different API. debug_assert_eq!(transferred_value, value); + let initiator_account_hash = initiator_addr.account_hash(); 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 +234,15 @@ 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(); - 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..cb82a5e81c 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, EvmTransactionError, 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, }; @@ -75,6 +75,7 @@ pub(crate) struct ExecutionArtifactBuilder { messages: Messages, transfers: Vec, initiator: InitiatorAddr, + evm_initiator: Option, current_price: u8, cost: U512, limit: Gas, @@ -83,6 +84,7 @@ pub(crate) struct ExecutionArtifactBuilder { size_estimate: u64, min_cost: U512, available: Option, + evm_receipt: Option, } impl ExecutionArtifactBuilder { @@ -101,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, @@ -109,6 +112,7 @@ impl ExecutionArtifactBuilder { size_estimate: transaction.size_estimate() as u64, min_cost, available: None, + evm_receipt: None, } } @@ -125,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(), @@ -133,6 +138,7 @@ impl ExecutionArtifactBuilder { size_estimate: transaction.size_estimate() as u64, min_cost: U512::zero(), available: None, + evm_receipt: None, } } @@ -382,6 +388,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 +491,35 @@ 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 + .evm_initiator + .expect("EVM execution result requires an EVM 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) } @@ -566,6 +606,7 @@ pub struct BlockAndExecutionArtifacts { pub enum SpeculativeExecutionResult { InvalidTransaction(InvalidTransaction), WasmV1(Box), + Evm(Box), } impl SpeculativeExecutionResult { @@ -577,6 +618,11 @@ impl SpeculativeExecutionResult { Transaction::V1(_) => SpeculativeExecutionResult::InvalidTransaction( InvalidTransaction::V1(InvalidTransactionV1::UnableToCalculateGasLimit), ), + Transaction::Evm(_) => SpeculativeExecutionResult::InvalidTransaction( + InvalidTransaction::Evm(EvmTransactionError::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/event_stream_server.rs b/node/src/components/event_stream_server.rs index e8d9820e3c..ef985f8b9c 100644 --- a/node/src/components/event_stream_server.rs +++ b/node/src/components/event_stream_server.rs @@ -301,19 +301,24 @@ where } => { let (initiator_addr, timestamp, ttl) = match *transaction_header { TransactionHeader::Deploy(deploy_header) => ( - InitiatorAddr::PublicKey(deploy_header.account().clone()), + Box::new(InitiatorAddr::PublicKey(deploy_header.account().clone())), deploy_header.timestamp(), deploy_header.ttl(), ), TransactionHeader::V1(metadata) => ( - metadata.initiator_addr().clone(), + Box::new(metadata.initiator_addr().clone()), + metadata.timestamp(), + metadata.ttl(), + ), + TransactionHeader::Evm(metadata) => ( + Box::new(metadata.initiator_addr().clone()), metadata.timestamp(), metadata.ttl(), ), }; self.broadcast(SseData::TransactionProcessed { transaction_hash: Box::new(transaction_hash), - initiator_addr: Box::new(initiator_addr), + 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 051d0fd762..8fa065558d 100644 --- a/node/src/components/event_stream_server/sse_server.rs +++ b/node/src/components/event_stream_server/sse_server.rs @@ -124,15 +124,18 @@ 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()) .take(message_count) .collect(); + let initiator_addr = Box::new(txn.initiator_addr()); + SseData::TransactionProcessed { transaction_hash: Box::new(txn.hash()), - initiator_addr: Box::new(txn.initiator_addr()), + initiator_addr, timestamp, ttl, block_hash: Box::new(BlockHash::random(rng)), 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.rs b/node/src/components/storage.rs index 4e073b6008..fadfb1351c 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,21 @@ impl Storage { } } } + (approvals_hash, finalized_approvals, transaction @ Transaction::Evm(_)) => { + match ApprovalsHash::compute(&finalized_approvals) { + Ok(computed_approvals_hash) + if computed_approvals_hash == approvals_hash + && finalized_approvals == transaction.approvals() => + { + 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 +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).into(), execution_result)) + } }; } Ok(Some(ret)) @@ -2124,6 +2142,7 @@ impl Storage { ExecutionResultV1::Success { cost, .. } => *cost, }, ExecutionResult::V2(v2_result) => v2_result.limit.value(), + ExecutionResult::Evm(evm_result) => evm_result.limit.value(), }) .sum(); @@ -2140,6 +2159,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 } @@ -2304,6 +2325,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/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/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index 1fc6ad6010..e84faea298 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -13,14 +13,16 @@ 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, BalanceRequest, ProofHandling, QueryRequest, QueryResult, +}; 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, + account::AccountHash, addressable_entity::AddressableEntity, evm, system::auction::ARG_AMOUNT, + AddressableEntityHash, AddressableEntityIdentifier, BlockHeader, CLType, Chainspec, EntityAddr, + EntityKind, EntityVersion, EntityVersionKey, EvmAddr, EvmTransactionError, + ExecutableDeployItem, ExecutableDeployItemIdentifier, Key, Package, PackageAddr, PackageHash, + PackageIdentifier, StoredValue, Timestamp, Transaction, TransactionEntryPoint, + TransactionInvocationTarget, TransactionTarget, DEFAULT_ENTRY_POINT_NAME, U512, }; use crate::{ @@ -38,12 +40,78 @@ 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_lookup_from_query_result(query_result: QueryResult) -> EvmAccountLookup { + match query_result { + QueryResult::Success { value, .. } => match *value { + 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(_) => { + 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 + } + } +} + /// A helper trait constraining `TransactionAcceptor` compatible reactor events. pub(crate) trait ReactorEventT: From @@ -168,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( + EvmTransactionError::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() @@ -211,11 +293,28 @@ 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(EvmAddr::Account(evm_transaction.from())), + vec![], + ); + return effect_builder + .query_global_state(query_request) + .event(move |query_result| Event::GetEvmAccountResult { + event_metadata, + block_header, + account: evm_account_lookup_from_query_result(query_result), + }); + } + 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 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) @@ -229,6 +328,308 @@ impl TransactionAcceptor { } } + fn handle_get_evm_account_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + account: EvmAccountLookup, + ) -> Effects { + 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( + EvmTransactionError::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(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( + EvmTransactionError::Decode(error_message), + )), + ); + } + }; + let query_request = QueryRequest::new( + *block_header.state_root_hash(), + Key::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( + EvmTransactionError::Decode(error_message), + )), + ); + } + }; + + 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 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(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( + EvmTransactionError::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, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + EvmTransactionError::InvalidNonce { expected, actual }, + )), + ); + } + + if event_metadata.source.is_client() { + 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 { + 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, @@ -405,6 +806,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) } @@ -419,6 +823,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( @@ -433,12 +845,21 @@ 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(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, .. } => { @@ -546,6 +967,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) => { @@ -637,6 +1066,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 @@ -848,6 +1281,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())), @@ -1036,6 +1472,64 @@ impl Component for TransactionAcceptor { block_header, maybe_balance, ), + Event::GetEvmAccountResult { + event_metadata, + block_header, + account, + } => self.handle_get_evm_account_result( + effect_builder, + event_metadata, + block_header, + 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, block_header, 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/components/transaction_acceptor/event.rs b/node/src/components/transaction_acceptor/event.rs index d03c949e3a..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, 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 { @@ -78,6 +123,40 @@ 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, + 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 { event_metadata: Box, @@ -176,6 +255,41 @@ impl Display for Event { event_metadata.transaction.hash() ) } + 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 ee77a15fd6..f60f797cd1 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; @@ -34,13 +41,15 @@ use casper_types::{ contracts::{ ContractHash, ContractPackage, ContractPackageStatus, ContractVersionKey, NamedKeys, }, + evm, global_state::TrieMerkleProof, testing::TestRng, - Block, BlockV2, CLValue, Chainspec, ChainspecRawBytes, Contract, Deploy, EraId, Groups, - HashAddr, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, Key, PackageAddr, - PricingHandling, PricingMode, ProtocolVersion, PublicKey, SecretKey, StoredValue, - TestBlockBuilder, TimeDiff, Timestamp, Transaction, TransactionArgs, TransactionConfig, - TransactionRuntimeParams, TransactionV1, URef, DEFAULT_BASELINE_MOTES_AMOUNT, + Block, BlockV2, CLValue, Chainspec, ChainspecRawBytes, Contract, Deploy, EraId, EvmTransaction, + EvmTransactionError, Groups, HashAddr, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, + Key, PackageAddr, PricingHandling, PricingMode, ProtocolVersion, PublicKey, SecretKey, + StoredValue, TestBlockBuilder, TimeDiff, Timestamp, Transaction, TransactionArgs, + TransactionConfig, TransactionRuntimeParams, TransactionV1, URef, + DEFAULT_BASELINE_MOTES_AMOUNT, }; use super::*; @@ -69,6 +78,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)] @@ -194,6 +205,7 @@ enum TxnType { #[derive(Clone, PartialEq, Eq, Debug)] enum TestScenario { + FromPeerEvmInvalidNonce, FromPeerInvalidTransaction(TxnType), FromPeerInvalidTransactionZeroPayment(TxnType), FromPeerExpired(TxnType), @@ -207,6 +219,8 @@ enum TestScenario { FromPeerSessionContract(TxnType, ContractScenario), FromPeerSessionContractPackage(TxnType, ContractPackageScenario), FromClientInvalidTransaction(TxnType), + FromClientEvmInvalidNonce, + FromClientEvmMissingIdentityWithCodeHash, FromClientInvalidTransactionZeroPayment(TxnType), FromClientSlightlyFutureDatedTransaction(TxnType), FromClientFutureDatedTransaction(TxnType), @@ -256,6 +270,7 @@ impl TestScenario { fn source(&self, rng: &mut NodeRng) -> Source { match self { TestScenario::FromPeerInvalidTransaction(_) + | TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromPeerInvalidTransactionZeroPayment(_) | TestScenario::FromPeerExpired(_) | TestScenario::FromPeerValidTransaction(_) @@ -270,6 +285,8 @@ impl TestScenario { | TestScenario::FromPeerSessionContractPackage(..) | TestScenario::InvalidFieldsFromPeer => Source::Peer(NodeId::random(rng)), TestScenario::FromClientInvalidTransaction(_) + | TestScenario::FromClientEvmInvalidNonce + | TestScenario::FromClientEvmMissingIdentityWithCodeHash | TestScenario::FromClientInvalidTransactionZeroPayment(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) | TestScenario::FromClientFutureDatedTransaction(_) @@ -324,6 +341,11 @@ impl TestScenario { txn.invalidate(); Transaction::from(txn) } + TestScenario::FromPeerEvmInvalidNonce + | TestScenario::FromClientEvmInvalidNonce + | TestScenario::FromClientEvmMissingIdentityWithCodeHash => { + Transaction::from(signed_evm_legacy_transaction(1)) + } TestScenario::FromClientInvalidTransactionZeroPayment(TxnType::V1) => { let txn = TransactionV1Builder::new_session( false, @@ -874,12 +896,15 @@ impl TestScenario { | TestScenario::FromClientRepeatedValidTransaction(_) | TestScenario::FromClientValidTransaction(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) + | TestScenario::FromClientEvmMissingIdentityWithCodeHash | TestScenario::FromClientSignedByAdmin(..) => true, TestScenario::FromPeerInvalidTransaction(_) + | TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromPeerInvalidTransactionZeroPayment(_) | TestScenario::FromClientInsufficientBalance(_) | TestScenario::FromClientMissingAccount(_) | TestScenario::FromClientInvalidTransaction(_) + | TestScenario::FromClientEvmInvalidNonce | TestScenario::FromClientInvalidTransactionZeroPayment(_) | TestScenario::FromClientFutureDatedTransaction(_) | TestScenario::FromClientAccountWithInsufficientWeight(_) @@ -963,6 +988,41 @@ impl TestScenario { fn is_v2_casper_vm(&self) -> bool { matches!(self, TestScenario::VmCasperV2ByPackageHash) } + + fn is_evm(&self) -> bool { + matches!( + self, + TestScenario::FromPeerEvmInvalidNonce + | TestScenario::FromClientEvmInvalidNonce + | TestScenario::FromClientEvmMissingIdentityWithCodeHash + ) + } +} + +fn signed_evm_legacy_transaction(nonce: u64) -> EvmTransaction { + 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); + EvmTransaction::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 { @@ -1036,9 +1096,24 @@ 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(EvmAddr::Account(address)) = query_request.key() { + 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 { TestScenario::FromPeerCustomPaymentContractPackage( ContractPackageScenario::MissingPackageAtHash, @@ -1107,8 +1182,36 @@ impl reactor::Reactor for Reactor { self.test_scenario ), } + } else if let Key::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(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() } @@ -1454,6 +1557,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(); @@ -1538,6 +1645,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(_) @@ -1626,6 +1734,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 => { @@ -1668,6 +1777,7 @@ async fn run_transaction_acceptor_without_timeout( // `AcceptedNewTransaction` announcement with the appropriate source. TestScenario::FromClientValidTransaction(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) + | TestScenario::FromClientEvmMissingIdentityWithCodeHash | TestScenario::FromClientSignedByAdmin(_) => { matches!( event, @@ -1840,6 +1950,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( + EvmTransactionError::InvalidNonce { + expected: 0, + actual: 1 + } + ))) + )) +} + #[tokio::test] async fn should_reject_zero_payment_transaction_v1_from_peer() { let result = run_transaction_acceptor(TestScenario::FromPeerInvalidTransactionZeroPayment( @@ -1940,6 +2064,27 @@ 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( + EvmTransactionError::InvalidNonce { + expected: 0, + actual: 1 + } + ))) + )) +} + +#[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/effect.rs b/node/src/effect.rs index 6004d0efaa..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 @@ -2314,6 +2315,7 @@ impl EffectBuilder { self.make_request( |responder| ContractRuntimeRequest::SpeculativelyExecute { block_header, + block_hashes, transaction, responder, }, diff --git a/node/src/effect/requests.rs b/node/src/effect/requests.rs index cf497e00f6..5713fe989c 100644 --- a/node/src/effect/requests.rs +++ b/node/src/effect/requests.rs @@ -878,6 +878,8 @@ 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 diff --git a/node/src/reactor/main_reactor/tests/configs_override.rs b/node/src/reactor/main_reactor/tests/configs_override.rs index 5734f1e776..3379fa3d74 100644 --- a/node/src/reactor/main_reactor/tests/configs_override.rs +++ b/node/src/reactor/main_reactor/tests/configs_override.rs @@ -3,7 +3,7 @@ use std::collections::BTreeSet; use num_rational::Ratio; use casper_types::{ - ConsensusProtocolName, FeeHandling, HoldBalanceHandling, PricingHandling, PublicKey, + ConsensusProtocolName, EvmConfig, FeeHandling, HoldBalanceHandling, PricingHandling, PublicKey, RefundHandling, TimeDiff, TransactionV1Config, }; @@ -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 4bbda7c74e..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; @@ -902,6 +906,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..d5e4d2640b 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, 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,7 +35,9 @@ use crate::reactor::main_reactor::tests::{ }; use casper_types::{ bytesrepr::{Bytes, ToBytes}, + evm, execution::ExecutionResultV1, + EvmAddr, EvmConfig, EvmSpec, EvmTransaction, }; pub(crate) static ALICE_SECRET_KEY: Lazy> = Lazy::new(|| { @@ -342,6 +356,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, @@ -759,11 +817,711 @@ 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, } } +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::Topic = evm::Topic::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) -> EvmTransaction { + 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, +) -> EvmTransaction { + 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) -> EvmTransaction { + 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); + EvmTransaction::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::Evm(EvmAddr::Account(address)), + StoredValue::CLValue(CLValue::from_t(Key::URef(main_purse)).unwrap()), + ), + ( + Key::Evm(EvmAddr::Nonce(address)), + StoredValue::CLValue(CLValue::from_t(0u64).unwrap()), + ), + ( + Key::Evm(EvmAddr::CodeHash(address)), + StoredValue::CLValue(CLValue::from_t(EMPTY_CODE_HASH).unwrap()), + ), + ( + 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(), + )); + } +} + +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(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_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(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(); + 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(); + 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::from_purse( + state_root_hash, + protocol_version, + main_purse, + 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, +) -> EvmAccountView { + 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(); + 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(EvmAddr::Nonce(address))) { + Some(value) => match *value { + StoredValue::CLValue(cl_value) => { + cl_value.into_t::().expect("nonce should decode") + } + value => panic!("expected EVM nonce, got {value:?}"), + }, + None => 0, + }; + EvmAccountView { nonce, main_purse } +} + +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 = EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: 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, 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(&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" + ); +} + +#[tokio::test] +async fn should_apply_casper_refund_handling_to_evm_transaction() { + let evm_config = EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: 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(&mut 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 = EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: 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(&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(); + 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::Evm(EvmAddr::Account(recipient)) + ) + .is_none()); +} + +#[tokio::test] +async fn should_not_seed_evm_accounts_at_genesis() { + let evm_config = EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: 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::Evm(EvmAddr::Account(alice_evm_address)), + ) + .is_none()); +} + +#[tokio::test] +async fn should_transfer_to_evm_address_with_native_transfer() { + let evm_config = EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: 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_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) + ); + + 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_reject_native_transfer_to_evm_contract_address() { + let evm_config = EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: 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/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/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.rs b/node/src/types/transaction/meta_transaction.rs index 0cc81cbaa6..058dce8526 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, Approval, Chainspec, Digest, EvmTransaction, + 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,14 +64,16 @@ impl MetaTransaction { pub(crate) fn approvals(&self) -> BTreeSet { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().approvals().clone(), + MetaTransaction::Evm(evm) => evm.approval().cloned().into_iter().collect(), MetaTransaction::V1(v1) => v1.approvals().clone(), } } - /// Returns the address of the initiator of the transaction. + /// Returns the Casper initiator address. 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 + .approval() + .into_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, } } @@ -98,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, } } @@ -108,6 +124,7 @@ impl MetaTransaction { .deploy() .payment() .is_standard_payment(Phase::Payment), + MetaTransaction::Evm(_) => true, MetaTransaction::V1(v1) => { if let PricingMode::PaymentLimited { standard_payment, .. @@ -128,6 +145,7 @@ impl MetaTransaction { .deploy() .payment() .is_standard_payment(Phase::Payment), + MetaTransaction::Evm(_) => false, MetaTransaction::V1(v1) => { if let PricingMode::PaymentLimited { standard_payment, .. @@ -150,6 +168,11 @@ impl MetaTransaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + MetaTransaction::Evm(evm) => evm + .approval() + .into_iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), MetaTransaction::V1(transaction_v1) => transaction_v1 .approvals() .iter() @@ -159,11 +182,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 +200,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 +211,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 +223,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 +234,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 +243,7 @@ impl MetaTransaction { pub(crate) fn is_deploy_transaction(&self) -> bool { match self { MetaTransaction::Deploy(_) => true, + MetaTransaction::Evm(_) => false, MetaTransaction::V1(_) => false, } } @@ -234,6 +267,7 @@ impl MetaTransaction { MetaTransaction::V1(v1) => { return v1.contract_direct_address(); } + MetaTransaction::Evm(_) => {} } None } @@ -256,6 +290,10 @@ impl MetaTransaction { &transaction_config.transaction_v1_config, ) .map(MetaTransaction::V1), + Transaction::Evm(evm) => { + MetaEvmTransaction::from_evm_transaction(evm, transaction_config) + .map(MetaTransaction::Evm) + } } } @@ -270,6 +308,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), @@ -279,16 +318,17 @@ 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 { - let initiator_addr = self.initiator_addr(); + pub(crate) fn to_session_input_data(&self) -> SessionInputData<'_> { 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(), @@ -298,7 +338,11 @@ impl MetaTransaction { ); SessionInputData::DeploySessionData { data } } + MetaTransaction::Evm(_) => { + 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(), @@ -316,7 +360,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(); @@ -331,6 +375,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(); @@ -366,6 +413,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(), } } @@ -373,6 +421,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(), } } @@ -380,6 +429,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(), } } @@ -387,6 +437,7 @@ impl MetaTransaction { pub(crate) fn seed(&self) -> Option<[u8; 32]> { match self { MetaTransaction::Deploy(_) => None, + MetaTransaction::Evm(_) => None, MetaTransaction::V1(v1) => v1.seed(), } } @@ -394,6 +445,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 } @@ -403,6 +455,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()), } } @@ -410,15 +463,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<&EvmTransaction> { + 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), } } @@ -441,6 +503,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() @@ -471,6 +541,458 @@ pub(crate) fn calculate_transaction_lane_for_transaction( } } +#[cfg(test)] +mod tests { + use super::*; + 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::{evm, EvmTransactionError, InitiatorAddr, 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.approval().cloned().into_iter().collect() + ); + assert_eq!(meta.initiator_addr(), evm_transaction.initiator_addr()); + 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_transaction_header_keeps_initiator_addr() { + let evm_transaction = legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000); + let expected_signer = evm_transaction + .signer() + .expect("signed EVM transaction should have an approval signer") + .clone(); + let expected_initiator_addr = InitiatorAddr::AccountHash(expected_signer.to_account_hash()); + + assert_eq!( + Transaction::Evm(evm_transaction.clone()).initiator_addr(), + expected_initiator_addr + ); + + let header = TransactionHeader::from(&evm_transaction); + let TransactionHeader::Evm(metadata) = header else { + panic!("expected EVM transaction header"); + }; + assert_eq!(metadata.initiator_addr(), &expected_initiator_addr); + } + + #[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(EvmTransactionError::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(EvmTransactionError::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(EvmTransactionError::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(EvmTransactionError::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(EvmTransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee + })) if gas_price == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + + #[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(EvmTransactionError::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(EvmTransactionError::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(EvmTransactionError::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( + EvmTransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas: 1 + } + )) + )); + } + + #[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(EvmTransactionError::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(EvmTransactionError::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( + EvmTransactionError::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(EvmTransactionError::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_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(EvmTransactionError::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 evm_transaction = + legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000).with_evm_approval(None); + let meta = evm_meta(&chainspec, evm_transaction); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + EvmTransactionError::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: EvmTransaction) -> MetaTransaction { + MetaTransaction::from_transaction( + &Transaction::Evm(evm_transaction), + chainspec.core_config.pricing_handling, + &chainspec.transaction_config, + ) + .expect("EVM transaction metadata should be created") + } + + fn unsigned_call(chain_id: u64, gas_price: u128, gas_limit: u64) -> EvmTransaction { + EvmTransaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(60), + test_initiator_addr(), + chain_id, + evm::Address::new([1u8; 20]), + Some(evm::Address::new([2u8; 20])), + casper_types::U256::zero(), + Default::default(), + gas_limit, + gas_price, + ) + } + + fn test_initiator_addr() -> InitiatorAddr { + InitiatorAddr::AccountHash(AccountHash::new([8; 32])) + } + + fn legacy_transaction( + chain_id: Option, + gas_price: u128, + gas_limit: u64, + ) -> EvmTransaction { + // 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, + ) -> EvmTransaction { + // 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 eip7702_transaction( + chain_id: u64, + max_fee_per_gas: u128, + max_priority_fee_per_gas: u128, + gas_limit: u64, + ) -> EvmTransaction { + 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) -> EvmTransaction { + EvmTransaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ) + .expect("EVM transaction should decode") + } +} + #[cfg(test)] mod proptests { use super::*; 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..47b73466a3 --- /dev/null +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -0,0 +1,167 @@ +use std::fmt::{self, Display, Formatter}; + +use casper_types::{ + bytesrepr::ToBytes, Approval, Chainspec, Digest, EvmTransaction, EvmTransactionError, + EvmTransactionKind, 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: EvmTransaction, + lane_id: u8, + payload_hash: Digest, +} + +impl MetaEvmTransaction { + pub(crate) fn from_evm_transaction( + transaction: &EvmTransaction, + transaction_config: &TransactionConfig, + ) -> Result { + let lane_id = transaction_config + .transaction_v1_config + .wasm_lanes() + .iter() + .last() + .map(|lane| lane.id()) + .ok_or(EvmTransactionError::MissingTransactionLane)?; + let payload_hash = Digest::hash(transaction.signing_payload()?); + Ok(MetaEvmTransaction { + transaction: transaction.clone(), + lane_id, + payload_hash, + }) + } + + pub(crate) fn transaction(&self) -> &EvmTransaction { + &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 approval(&self) -> Option<&Approval> { + self.transaction.approval() + } + + pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { + self.transaction.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<(), EvmTransactionError> { + self.transaction.verify() + } + + pub(crate) fn is_config_compliant( + &self, + chainspec: &Chainspec, + ) -> Result<(), EvmTransactionError> { + let transaction = &self.transaction; + let evm_config = &chainspec.evm_config; + if !evm_config.enabled { + return Err(EvmTransactionError::Disabled); + } + + if !transaction.is_unsigned_call() { + transaction.verify()?; + } + + let expected = evm_config.chain_id; + let actual = transaction + .chain_id() + .ok_or(EvmTransactionError::MissingChainId)?; + if actual != expected { + return Err(EvmTransactionError::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(EvmTransactionError::GasLimitExceedsBlockGasLimit { + gas_limit, + block_gas_limit, + }); + } + + let base_fee = u128::from(evm_config.base_fee); + match transaction.kind() { + EvmTransactionKind::Legacy | EvmTransactionKind::Eip2930 => { + let gas_price = transaction + .gas_price() + .ok_or(EvmTransactionError::MissingGasPrice)?; + if gas_price < base_fee { + return Err(EvmTransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee, + }); + } + } + EvmTransactionKind::Eip1559 | EvmTransactionKind::Eip7702 => { + // `max_fee_per_gas` is still meaningful on Casper as the user's + // 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(EvmTransactionError::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(EvmTransactionError::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/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/node/src/types/transaction/meta_transaction/transaction_header.rs b/node/src/types/transaction/meta_transaction/transaction_header.rs index fa0c6b0108..64cfea74c9 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::{ + DeployHeader, EvmTransaction, 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,21 +26,53 @@ 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, ) } } +impl Display for EvmTransactionMetadata { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + write!( + formatter, + "transaction-metadata[initiator_addr: {}]", + self.initiator_addr, + ) + } +} + +#[derive(Debug, Clone, DataSize, PartialEq, Eq, Serialize)] +pub(crate) struct EvmTransactionMetadata { + initiator_addr: InitiatorAddr, + timestamp: Timestamp, + ttl: TimeDiff, +} + +impl EvmTransactionMetadata { + pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { + &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(TransactionV1Metadata), + V1(TransactionMetadata), + Evm(EvmTransactionMetadata), } impl From for TransactionHeader { @@ -49,7 +83,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,11 +92,23 @@ impl From<&TransactionV1> for TransactionHeader { } } +impl From<&EvmTransaction> for TransactionHeader { + fn from(transaction: &EvmTransaction) -> Self { + let meta = EvmTransactionMetadata { + initiator_addr: transaction.initiator_addr().clone(), + 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(evm) => evm.into(), } } } @@ -72,6 +118,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/node/src/utils/chain_specification/parse_toml.rs b/node/src/utils/chain_specification/parse_toml.rs index 1a3ce947a3..0e0c511e90 100644 --- a/node/src/utils/chain_specification/parse_toml.rs +++ b/node/src/utils/chain_specification/parse_toml.rs @@ -33,9 +33,9 @@ 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, + CoreConfig, EvmConfig, 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/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/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..b21d9624c9 100644 --- a/resources/integration-test/chainspec.toml +++ b/resources/integration-test/chainspec.toml @@ -508,3 +508,17 @@ 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 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/integration-test/config-example.toml b/resources/integration-test/config-example.toml index 688dbc77a1..72a782ef34 100644 --- a/resources/integration-test/config-example.toml +++ b/resources/integration-test/config-example.toml @@ -371,6 +371,7 @@ 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 # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index b9b34c1328..8cd8f5f6c4 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -500,3 +500,17 @@ 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=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 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/config.toml b/resources/local/config.toml index 7d8335f199..d7692ec4ed 100644 --- a/resources/local/config.toml +++ b/resources/local/config.toml @@ -372,6 +372,7 @@ 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 # ============================================== # Configuration options for the REST HTTP server # ============================================== diff --git a/resources/mainnet/chainspec.toml b/resources/mainnet/chainspec.toml index 3448dc9935..d9ac82c9c6 100644 --- a/resources/mainnet/chainspec.toml +++ b/resources/mainnet/chainspec.toml @@ -508,3 +508,17 @@ 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 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/config-example.toml b/resources/mainnet/config-example.toml index fcf4fc82d6..dba2fa6f17 100644 --- a/resources/mainnet/config-example.toml +++ b/resources/mainnet/config-example.toml @@ -371,6 +371,7 @@ 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 # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/production/chainspec.toml b/resources/production/chainspec.toml index 58d0008057..ff0bda27d1 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -507,3 +507,17 @@ 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 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/config-example.toml b/resources/production/config-example.toml index 92343bb95d..861697c3f7 100644 --- a/resources/production/config-example.toml +++ b/resources/production/config-example.toml @@ -371,6 +371,7 @@ 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 # ============================================== # Configuration options for the REST HTTP server diff --git a/resources/test/sse_data_schema.json b/resources/test/sse_data_schema.json index e11e4f4032..bf4f629d8b 100644 --- a/resources/test/sse_data_schema.json +++ b/resources/test/sse_data_schema.json @@ -847,6 +847,19 @@ } }, "additionalProperties": false + }, + { + "description": "An EVM transaction hash.", + "type": "object", + "required": [ + "Evm" + ], + "properties": { + "Evm": { + "$ref": "#/definitions/EvmTransactionHash" + } + }, + "additionalProperties": false } ] }, @@ -858,6 +871,14 @@ } ] }, + "EvmTransactionHash": { + "description": "A transaction hash produced by Ethereum transaction RLP hashing rules.", + "allOf": [ + { + "$ref": "#/definitions/Digest" + } + ] + }, "RewardedSignatures": { "description": "Describes finality signatures that will be rewarded in a block. Consists of a vector of `SingleBlockRewardedSignatures`, each of which describes signatures for a single ancestor block. The first entry represents the signatures for the parent block, the second for the parent of the parent, and so on.", "type": "array", @@ -902,6 +923,19 @@ } }, "additionalProperties": false + }, + { + "description": "An EVM transaction.", + "type": "object", + "required": [ + "Evm" + ], + "properties": { + "Evm": { + "$ref": "#/definitions/EvmTransaction" + } + }, + "additionalProperties": false } ] }, @@ -1614,7 +1648,7 @@ "additionalProperties": false }, "InitiatorAddr": { - "description": "The address of the initiator of a TransactionV1.", + "description": "The address of the initiator of a transaction.", "oneOf": [ { "description": "The public key of the initiator.", @@ -1749,6 +1783,235 @@ } ] }, + "EvmTransaction": { + "description": "An unsigned Ethereum transaction payload plus one Ethereum-style approval.", + "type": "object", + "required": [ + "authorization_list", + "from", + "gas_limit", + "hash", + "initiator_addr", + "input", + "kind", + "max_fee_per_gas", + "nonce", + "timestamp", + "ttl", + "value" + ], + "properties": { + "timestamp": { + "$ref": "#/definitions/Timestamp" + }, + "ttl": { + "$ref": "#/definitions/TimeDiff" + }, + "initiator_addr": { + "$ref": "#/definitions/InitiatorAddr" + }, + "hash": { + "$ref": "#/definitions/EvmTransactionHash" + }, + "from": { + "$ref": "#/definitions/Address" + }, + "kind": { + "$ref": "#/definitions/EvmTransactionKind" + }, + "to": { + "anyOf": [ + { + "$ref": "#/definitions/Address" + }, + { + "type": "null" + } + ] + }, + "nonce": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "gas_limit": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "gas_price": { + "type": [ + "integer", + "null" + ], + "format": "uint128", + "minimum": 0.0 + }, + "max_fee_per_gas": { + "type": "integer", + "format": "uint128", + "minimum": 0.0 + }, + "max_priority_fee_per_gas": { + "type": [ + "integer", + "null" + ], + "format": "uint128", + "minimum": 0.0 + }, + "value": { + "$ref": "#/definitions/U256" + }, + "input": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "chain_id": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "authorization_list": { + "type": "array", + "items": { + "$ref": "#/definitions/SetCodeAuthorization" + } + }, + "approval": { + "anyOf": [ + { + "$ref": "#/definitions/EvmApproval" + }, + { + "type": "null" + } + ] + } + } + }, + "Address": { + "description": "A 20-byte Ethereum account or contract address encoded as hexadecimal.", + "type": "string" + }, + "EvmTransactionKind": { + "description": "The supported Ethereum transaction envelope kinds.", + "oneOf": [ + { + "description": "A legacy Ethereum transaction.", + "type": "string", + "enum": [ + "Legacy" + ] + }, + { + "description": "An EIP-2930 access-list transaction.", + "type": "string", + "enum": [ + "Eip2930" + ] + }, + { + "description": "An EIP-1559 dynamic-fee transaction.", + "type": "string", + "enum": [ + "Eip1559" + ] + }, + { + "description": "An EIP-7702 set-code transaction.", + "type": "string", + "enum": [ + "Eip7702" + ] + } + ] + }, + "U256": { + "description": "Decimal representation of a 256-bit integer.", + "type": "string" + }, + "SetCodeAuthorization": { + "description": "A signed EIP-7702 authorization-list item.", + "type": "object", + "required": [ + "address", + "chain_id", + "nonce", + "r", + "s", + "y_parity" + ], + "properties": { + "chain_id": { + "description": "Chain ID that scopes the authorization; zero follows EIP-7702 wildcard semantics.", + "allOf": [ + { + "$ref": "#/definitions/U256" + } + ] + }, + "address": { + "description": "Address whose code the authorized account delegates to.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + }, + "nonce": { + "description": "Nonce expected on the authorizing account.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "y_parity": { + "description": "secp256k1 signature recovery parity.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "r": { + "description": "secp256k1 signature `r` value.", + "allOf": [ + { + "$ref": "#/definitions/U256" + } + ] + }, + "s": { + "description": "secp256k1 signature `s` value.", + "allOf": [ + { + "$ref": "#/definitions/U256" + } + ] + } + } + }, + "EvmApproval": { + "description": "A Casper approval plus the Ethereum recovery parity for the same signature.\n\nCasper [`Signature::Secp256k1`] stores the canonical 64-byte ECDSA signature, `r || s`. Ethereum signed transactions carry one extra bit, historically encoded as `v` and in typed transactions as `yParity`, so the sender can be recovered from the transaction payload. `EvmApproval` keeps that Ethereum recovery parity next to the normal Casper approval, allowing the signed Ethereum envelope and transaction hash to be reconstructed without guessing which recovery ID was present in the original payload.", + "type": "object", + "required": [ + "approval", + "y_parity" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + }, + "y_parity": { + "type": "boolean" + } + } + }, "ExecutionResult": { "description": "The versioned result of executing a single deploy.", "oneOf": [ @@ -1777,6 +2040,19 @@ } }, "additionalProperties": false + }, + { + "description": "EVM transaction execution result type.", + "type": "object", + "required": [ + "Evm" + ], + "properties": { + "Evm": { + "$ref": "#/definitions/EvmExecutionResult" + } + }, + "additionalProperties": false } ] }, @@ -2802,10 +3078,6 @@ "description": "Decimal representation of a 128-bit integer.", "type": "string" }, - "U256": { - "description": "Decimal representation of a 256-bit integer.", - "type": "string" - }, "NamedKey": { "description": "A key with a name.", "type": "object", @@ -4747,6 +5019,13 @@ "enum": [ "V2CasperWasm" ] + }, + { + "description": "Prague-compatible EVM bytecode.\n\nThis variant records bytecode that is valid for the Prague EVM rules. When support for a future bytecode-affecting EVM spec is added, introduce a new `Evm` variant instead of changing the meaning of this one.", + "type": "string", + "enum": [ + "EvmPrague" + ] } ] }, @@ -5019,6 +5298,403 @@ } } }, + "EvmExecutionResult": { + "description": "The result of executing a single EVM transaction.", + "type": "object", + "required": [ + "cost", + "current_price", + "effects", + "initiator", + "limit", + "receipt", + "refund", + "size_estimate" + ], + "properties": { + "initiator": { + "description": "Who initiated this EVM transaction.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + }, + "current_price": { + "description": "The current Casper gas price used for fee accounting.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "limit": { + "description": "The maximum allowed gas limit for this transaction.", + "allOf": [ + { + "$ref": "#/definitions/Gas" + } + ] + }, + "cost": { + "description": "How much was paid for this transaction.", + "allOf": [ + { + "$ref": "#/definitions/U512" + } + ] + }, + "refund": { + "description": "How much unconsumed gas was refunded, if any.", + "allOf": [ + { + "$ref": "#/definitions/U512" + } + ] + }, + "size_estimate": { + "description": "The size estimate of the transaction.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "effects": { + "description": "The effects of executing this transaction.", + "allOf": [ + { + "$ref": "#/definitions/Effects" + } + ] + }, + "receipt": { + "description": "EVM-native receipt data used by Ethereum JSON-RPC projections.", + "allOf": [ + { + "$ref": "#/definitions/Receipt" + } + ] + } + }, + "additionalProperties": false + }, + "Receipt": { + "description": "EVM transaction receipt data persisted with an EVM execution result.", + "type": "object", + "required": [ + "effective_gas_price", + "gas_used", + "logs", + "status" + ], + "properties": { + "status": { + "description": "Transaction execution status.", + "allOf": [ + { + "$ref": "#/definitions/ReceiptStatus" + } + ] + }, + "gas_used": { + "description": "Gas consumed by EVM execution.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "effective_gas_price": { + "description": "Effective gas price used for Ethereum receipt projection and Casper EVM fee accounting.\n\nFor 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.", + "type": "integer", + "format": "uint128", + "minimum": 0.0 + }, + "contract_address": { + "description": "Contract address created by the transaction, if any.", + "anyOf": [ + { + "$ref": "#/definitions/Address" + }, + { + "type": "null" + } + ] + }, + "logs": { + "description": "Logs emitted by successful execution.", + "type": "array", + "items": { + "$ref": "#/definitions/Log" + } + } + }, + "additionalProperties": false + }, + "ReceiptStatus": { + "description": "High-level status recorded in an EVM transaction receipt.", + "oneOf": [ + { + "description": "EVM execution completed successfully.", + "type": "string", + "enum": [ + "Success" + ] + }, + { + "description": "EVM execution reverted and returned revert bytes.", + "type": "string", + "enum": [ + "Revert" + ] + }, + { + "description": "EVM execution halted for an exceptional reason.", + "type": "object", + "required": [ + "Halt" + ], + "properties": { + "Halt": { + "$ref": "#/definitions/HaltReason" + } + }, + "additionalProperties": false + } + ] + }, + "HaltReason": { + "description": "Reason an EVM execution halted exceptionally.", + "oneOf": [ + { + "description": "Execution ran out of gas.", + "type": "object", + "required": [ + "OutOfGas" + ], + "properties": { + "OutOfGas": { + "$ref": "#/definitions/OutOfGasError" + } + }, + "additionalProperties": false + }, + { + "description": "The bytecode contained an unknown opcode.", + "type": "string", + "enum": [ + "OpcodeNotFound" + ] + }, + { + "description": "The bytecode executed the invalid `0xFE` opcode.", + "type": "string", + "enum": [ + "InvalidFEOpcode" + ] + }, + { + "description": "Execution jumped to an invalid destination.", + "type": "string", + "enum": [ + "InvalidJump" + ] + }, + { + "description": "The opcode or feature is not active for the configured hardfork.", + "type": "string", + "enum": [ + "NotActivated" + ] + }, + { + "description": "Execution attempted to pop a value from an empty stack.", + "type": "string", + "enum": [ + "StackUnderflow" + ] + }, + { + "description": "Execution attempted to push a value onto a full stack.", + "type": "string", + "enum": [ + "StackOverflow" + ] + }, + { + "description": "Execution used an invalid memory or storage offset.", + "type": "string", + "enum": [ + "OutOfOffset" + ] + }, + { + "description": "Contract creation collided with an existing account.", + "type": "string", + "enum": [ + "CreateCollision" + ] + }, + { + "description": "A precompile failed.", + "type": "string", + "enum": [ + "PrecompileError" + ] + }, + { + "description": "Account nonce overflowed.", + "type": "string", + "enum": [ + "NonceOverflow" + ] + }, + { + "description": "Created contract runtime bytecode exceeded the configured limit.", + "type": "string", + "enum": [ + "CreateContractSizeLimit" + ] + }, + { + "description": "Created contract runtime bytecode starts with `0xEF`.", + "type": "string", + "enum": [ + "CreateContractStartingWithEF" + ] + }, + { + "description": "Contract init code exceeded the configured limit.", + "type": "string", + "enum": [ + "CreateInitCodeSizeLimit" + ] + }, + { + "description": "Payment accounting overflowed.", + "type": "string", + "enum": [ + "OverflowPayment" + ] + }, + { + "description": "Execution attempted a state change during a static call.", + "type": "string", + "enum": [ + "StateChangeDuringStaticCall" + ] + }, + { + "description": "Execution attempted a call disallowed during a static call.", + "type": "string", + "enum": [ + "CallNotAllowedInsideStatic" + ] + }, + { + "description": "The caller did not have enough funds.", + "type": "string", + "enum": [ + "OutOfFunds" + ] + }, + { + "description": "Call depth exceeded the EVM limit.", + "type": "string", + "enum": [ + "CallTooDeep" + ] + }, + { + "description": "Halt reason was not recognized by this version.", + "type": "string", + "enum": [ + "Unknown" + ] + } + ] + }, + "OutOfGasError": { + "description": "Reason execution ran out of gas.", + "oneOf": [ + { + "description": "Not enough gas to execute an opcode.", + "type": "string", + "enum": [ + "Basic" + ] + }, + { + "description": "Memory limit exceeded.", + "type": "string", + "enum": [ + "MemoryLimit" + ] + }, + { + "description": "Memory expansion ran out of gas.", + "type": "string", + "enum": [ + "Memory" + ] + }, + { + "description": "Precompile ran out of gas.", + "type": "string", + "enum": [ + "Precompile" + ] + }, + { + "description": "Operand was too large to fit into the required native type.", + "type": "string", + "enum": [ + "InvalidOperand" + ] + }, + { + "description": "`SSTORE` was attempted with too little gas remaining.", + "type": "string", + "enum": [ + "ReentrancySentry" + ] + } + ] + }, + "Log": { + "description": "EVM log entry emitted by a transaction.", + "type": "object", + "required": [ + "address", + "data", + "topics" + ], + "properties": { + "address": { + "description": "Contract address that emitted the log.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + }, + "topics": { + "description": "Indexed log topics.\n\nThe 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.", + "type": "array", + "items": { + "$ref": "#/definitions/EvmTopic" + } + }, + "data": { + "description": "ABI-encoded unindexed log data.\n\nThis 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:\n\n```text log_gas = 375 + 375 * topics + 8 * bytes + memory_gas(bytes) memory_gas(bytes) = 3 * words + floor(words * words / 512) words = ceil(bytes / 32) ```\n\nThere 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.", + "allOf": [ + { + "$ref": "#/definitions/Bytes" + } + ] + } + }, + "additionalProperties": false + }, + "EvmTopic": { + "description": "A 32-byte EVM log topic encoded as 0x-prefixed hexadecimal.", + "type": "string" + }, "Message": { "description": "Message that was emitted by an addressable entity during execution.", "type": "object", diff --git a/resources/testnet/chainspec.toml b/resources/testnet/chainspec.toml index 2158da03cb..78daf1613e 100644 --- a/resources/testnet/chainspec.toml +++ b/resources/testnet/chainspec.toml @@ -510,3 +510,17 @@ 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 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/config-example.toml b/resources/testnet/config-example.toml index d7a5c4b50b..b69e0900f8 100644 --- a/resources/testnet/config-example.toml +++ b/resources/testnet/config-example.toml @@ -371,6 +371,7 @@ 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 # ============================================== # Configuration options for the REST HTTP server 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/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/evm_contracts/Counter.sol b/smart_contracts/evm_contracts/Counter.sol new file mode 100644 index 0000000000..08ddf9ac14 --- /dev/null +++ b/smart_contracts/evm_contracts/Counter.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +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; + } + + 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/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/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/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/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..67185f2d22 100644 --- a/storage/src/data_access_layer/balance.rs +++ b/storage/src/data_access_layer/balance.rs @@ -70,6 +70,15 @@ pub enum BalanceIdentifier { PenalizedPayment, } +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), + } + } +} + impl BalanceIdentifier { /// Returns underlying uref addr from balance identifier, if any. pub fn as_purse_addr(&self) -> Option { @@ -187,15 +196,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), - } - } -} - /// Processing hold balance handling. #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub struct ProcessingHoldBalanceHandling {} 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/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/data_access_layer/key_prefix.rs b/storage/src/data_access_layer/key_prefix.rs index 6e3fe12e06..fd621493ce 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, EvmAddr}, 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,9 @@ impl ToBytes for KeyPrefix { KeyPrefix::EntryPointsV2ByEntity(entity) => { U8_SERIALIZED_LENGTH + entity.serialized_length() } + KeyPrefix::EvmStorageByAddress(address) => { + U8_SERIALIZED_LENGTH + address.serialized_length() + } } } @@ -100,6 +106,11 @@ impl ToBytes for KeyPrefix { writer.push(1); entity.write_bytes(writer)?; } + KeyPrefix::EvmStorageByAddress(address) => { + writer.push(KeyTag::Evm as u8); + writer.push(EvmAddr::STORAGE_TAG); + address.write_bytes(writer)?; + } } Ok(()) } @@ -160,6 +171,16 @@ impl FromBytes for KeyPrefix { _ => return Err(bytesrepr::Error::Formatting), } } + 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), }; Ok(result) @@ -176,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::*; @@ -194,6 +215,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)) }), ] } @@ -214,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 [ ( @@ -253,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 a4fb2870fe..4258f98bd3 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::{ @@ -37,8 +38,8 @@ use casper_types::{ AUCTION, HANDLE_PAYMENT, MINT, }, Account, AddressableEntity, BlockGlobalAddr, CLValue, Digest, EntityAddr, EntityEntryPoint, - EntryPointAddr, EntryPointValue, HoldsEpoch, Key, KeyTag, Phase, PublicKey, RuntimeArgs, - StoredValue, SystemHashRegistry, REWARDS_HANDLING_RATIO_TAG, U512, + EntryPointAddr, EntryPointValue, EvmAddr, HoldsEpoch, Key, KeyTag, Phase, PublicKey, + RuntimeArgs, StoredValue, SystemHashRegistry, REWARDS_HANDLING_RATIO_TAG, U512, }; #[cfg(test)] @@ -1701,7 +1702,9 @@ pub trait StateProvider: Send + Sync + Sized { }; runtime .transfer( - Some(initiator_addr.account_hash()), + initiator_addr + .as_ref() + .map(|initiator_addr| initiator_addr.account_hash()), source_purse, target_purse, amount, @@ -2214,7 +2217,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) => { @@ -2233,6 +2238,46 @@ pub trait StateProvider: Send + Sync + Sized { return TransferResult::Failure(tce.into()); } } + 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(EvmAddr::Account(address)), + StoredValue::CLValue(identity), + ); + tc.borrow_mut().write( + Key::Evm(EvmAddr::Nonce(address)), + StoredValue::CLValue(nonce), + ); + tc.borrow_mut().write( + Key::Evm(EvmAddr::CodeHash(address)), + StoredValue::CLValue(code_hash), + ); + 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/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/system/transfer.rs b/storage/src/system/transfer.rs index ce2634e9c6..06d3d2fd28 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -4,9 +4,10 @@ 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, + AccessRights, CLType, CLTyped, CLValue, CLValueError, EvmAddr, Key, ProtocolVersion, + RuntimeArgs, RuntimeFootprint, StoredValue, StoredValueTypeMismatch, URef, U512, }; use crate::{ @@ -50,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, @@ -87,6 +91,15 @@ pub enum TransferTargetMode { /// Main purse of a resolved account. main_purse: URef, }, + /// 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, + }, /// Native transfer arguments resolved into a transfer to a purse. PurseExists { /// Target account hash (if known). @@ -96,6 +109,12 @@ 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-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), } impl TransferTargetMode { @@ -111,6 +130,8 @@ impl TransferTargetMode { .. } => Some(*target_account_hash), TransferTargetMode::CreateAccount(target_account_hash) => Some(*target_account_hash), + TransferTargetMode::ExistingEvmAccount { .. } + | TransferTargetMode::CreateEvmAccount(_) => None, } } } @@ -342,6 +363,55 @@ 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)?; + self.reject_evm_contract_target(address, Rc::clone(&tracking_copy))?; + let key = Key::Evm(EvmAddr::Account(address)); + 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::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)), + }; + } 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 @@ -375,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(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. @@ -426,11 +527,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/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/Cargo.toml b/types/Cargo.toml index 0c72a916c5..3b0cc8f1ba 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -49,6 +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.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"] } @@ -73,6 +76,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/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/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/byte_code.rs b/types/src/byte_code.rs index a34b4bafcc..48f07e9f68 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 is 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/chainspec.rs b/types/src/chainspec.rs index f29bcfcdba..84432d4e68 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -28,14 +28,14 @@ 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))] use crate::testing::TestRng; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, - ChainNameDigest, Digest, EraId, ProtocolVersion, Timestamp, + ChainNameDigest, Digest, EraId, EvmConfig, ProtocolVersion, Timestamp, }; pub use accounts_config::{ AccountConfig, AccountsConfig, AdministratorAccount, DelegatorConfig, GenesisAccount, @@ -43,14 +43,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; @@ -97,7 +97,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 { @@ -121,6 +121,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 +280,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 +291,7 @@ impl Chainspec { core_config, highway_config, transaction_config, + evm_config, wasm_config, system_costs_config, vacancy_config, @@ -337,6 +343,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 +362,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 +377,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 +388,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/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/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/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/evm.rs b/types/src/evm.rs new file mode 100644 index 0000000000..244bbc65c9 --- /dev/null +++ b/types/src/evm.rs @@ -0,0 +1,35 @@ +//! 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 evm_addr; +mod hash; +mod receipt; +mod topic; +mod transaction; + +pub use account::{deterministic_purse, StorageAddr, EMPTY_CODE_HASH}; +pub use address::{Address, ADDRESS_LENGTH}; +pub use hash::{Hash, HASH_LENGTH}; +pub use receipt::{HaltReason, Log, OutOfGasError, Receipt, ReceiptStatus}; +pub use topic::Topic; +pub use transaction::{ + SetCodeAuthorization, EIP1559_TRANSACTION_TYPE_ID, EIP2930_TRANSACTION_TYPE_ID, + EIP4844_TRANSACTION_TYPE_ID, EIP7702_TRANSACTION_TYPE_ID, LEGACY_TRANSACTION_TYPE_ID, +}; + +// Evm-prefixed wrappers should be reached through the crate root +// (`casper_types::EvmFoo`), not through `casper_types::evm::EvmFoo`. +// They are re-exported here so the rest of `casper-types` can import them +// without going through the crate root. +pub use config::{EvmConfig, EvmSpec}; +pub use evm_addr::EvmAddr; +pub use transaction::{ + EvmApproval, EvmTransaction, EvmTransactionError, EvmTransactionHash, EvmTransactionKind, +}; diff --git a/types/src/evm/account.rs b/types/src/evm/account.rs new file mode 100644 index 0000000000..5b1d77139c --- /dev/null +++ b/types/src/evm/account.rs @@ -0,0 +1,87 @@ +#[cfg(feature = "json-schema")] +use alloc::string::String; +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, FromBytes, ToBytes}, + Digest, URef, U256, +}; + +/// 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, +]); + +/// 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, + #[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: U256) -> 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) -> U256 { + 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) = U256::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..a0d93750a4 --- /dev/null +++ b/types/src/evm/address.rs @@ -0,0 +1,155 @@ +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 alloy_primitives::keccak256; + +use crate::{ + bytesrepr::{self, FromBytes, ToBytes}, + CLType, CLTyped, PublicKey, +}; + +/// 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))] +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) + } + + /// 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 { + 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()) + } +} + +#[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()) + } + + 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)) + } +} + +#[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/config.rs b/types/src/evm/config.rs new file mode 100644 index 0000000000..3d1b2b60c8 --- /dev/null +++ b/types/src/evm/config.rs @@ -0,0 +1,130 @@ +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 { + /// Prague. + #[default] + Prague, +} + +impl EvmSpec { + fn tag(self) -> u8 { + match self { + EvmSpec::Prague => 0, + } + } +} + +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::Prague, + _ => 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/evm_addr.rs b/types/src/evm/evm_addr.rs new file mode 100644 index 0000000000..c244c920d1 --- /dev/null +++ b/types/src/evm/evm_addr.rs @@ -0,0 +1,138 @@ +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 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 { + /// 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; + /// 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 { + 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(), + EvmAddr::Nonce(address) => address.serialized_length(), + EvmAddr::CodeHash(address) => address.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) + } + 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) + } + } + } +} + +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)), + 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), + } + } +} + +#[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..=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!(), + } + } +} + +#[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, + ))); + bytesrepr::test_serialization_roundtrip(&EvmAddr::Nonce(address)); + bytesrepr::test_serialization_roundtrip(&EvmAddr::CodeHash(address)); + } +} diff --git a/types/src/evm/hash.rs b/types/src/evm/hash.rs new file mode 100644 index 0000000000..240bacfcb4 --- /dev/null +++ b/types/src/evm/hash.rs @@ -0,0 +1,160 @@ +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 crate::{ + bytesrepr::{self, FromBytes, ToBytes}, + CLType, CLTyped, Digest, +}; + +/// The number of bytes in an EVM 256-bit hash. +pub const HASH_LENGTH: usize = 32; + +/// 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); + +impl Hash { + /// The zero hash. + 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(Digest::from_raw(bytes)) + } + + /// Returns the raw bytes backing this hash. + pub fn value(self) -> [u8; HASH_LENGTH] { + self.0.value() + } + + /// Returns the raw bytes backing this hash 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 Hash { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Display for Hash { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "0x{}", self.to_hex_string()) + } +} + +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 { + 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 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() + } + + 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 Hash { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + 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/receipt.rs b/types/src/evm/receipt.rs new file mode 100644 index 0000000000..634ba21834 --- /dev/null +++ b/types/src/evm/receipt.rs @@ -0,0 +1,563 @@ +//! 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, Topic}; +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. + /// + /// 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, + /// 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, +} + +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 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
, + /// 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(|_| Topic::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/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/evm/transaction.rs b/types/src/evm/transaction.rs new file mode 100644 index 0000000000..759cfbe965 --- /dev/null +++ b/types/src/evm/transaction.rs @@ -0,0 +1,1882 @@ +use alloc::{ + format, + string::{String, ToString}, + vec::Vec, +}; +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, 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, + TxKind as AlloyTxKind, B256, U256 as AlloyU256, +}; +#[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")] +use schemars::JsonSchema; +#[cfg(any(feature = "std", test))] +use serde::{de, Deserializer, Serializer}; +use serde::{Deserialize, Serialize}; + +use super::{Address, EvmConfig, Hash, HASH_LENGTH}; +#[cfg(any(feature = "testing", test))] +use crate::testing::TestRng; +use crate::{ + bytesrepr::{self, Bytes, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, + transaction::serialization::{ + CalltableSerializationEnvelope, CalltableSerializationEnvelopeBuilder, + }, + Approval, ApprovalsHash, AsymmetricType, Digest, InitiatorAddr, PublicKey, SecretKey, + Signature, TimeDiff, Timestamp, U256, U512, +}; + +const TRANSACTION_KIND_SERIALIZED_LENGTH: usize = U8_SERIALIZED_LENGTH; +const EVM_TRANSACTION_MAX_CURRENT_FIELDS: u32 = 16; + +const TIMESTAMP_FIELD_INDEX: u16 = 0; +const TTL_FIELD_INDEX: u16 = 1; +const KIND_FIELD_INDEX: u16 = 2; + +// Field indices after KIND_FIELD_INDEX are interpreted within the selected +// EVM transaction kind. Keeping kind-specific payloads separate lets future +// transaction types add fields without changing the layout of older kinds. +const HASH_FIELD_INDEX: u16 = 3; +const FROM_FIELD_INDEX: u16 = 4; +const TO_FIELD_INDEX: u16 = 5; +const NONCE_FIELD_INDEX: u16 = 6; +const GAS_LIMIT_FIELD_INDEX: u16 = 7; + +const LEGACY_GAS_PRICE_FIELD_INDEX: u16 = 8; +const LEGACY_VALUE_FIELD_INDEX: u16 = 9; +const LEGACY_INPUT_FIELD_INDEX: u16 = 10; +const LEGACY_CHAIN_ID_FIELD_INDEX: u16 = 11; +const LEGACY_APPROVAL_FIELD_INDEX: u16 = 12; + +const DYNAMIC_MAX_FEE_PER_GAS_FIELD_INDEX: u16 = 8; +const DYNAMIC_MAX_PRIORITY_FEE_PER_GAS_FIELD_INDEX: u16 = 9; +const DYNAMIC_VALUE_FIELD_INDEX: u16 = 10; +const DYNAMIC_INPUT_FIELD_INDEX: u16 = 11; +const DYNAMIC_CHAIN_ID_FIELD_INDEX: u16 = 12; +const DYNAMIC_APPROVAL_FIELD_INDEX: u16 = 13; + +const EIP7702_AUTHORIZATION_LIST_FIELD_INDEX: u16 = 13; +const EIP7702_APPROVAL_FIELD_INDEX: u16 = 14; +const INITIATOR_ADDR_FIELD_INDEX: u16 = 15; + +/// 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; + +/// 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 EvmTransactionHash(Digest); + +impl EvmTransactionHash { + /// Creates a transaction hash from a raw digest. + pub const fn new(hash: Digest) -> Self { + EvmTransactionHash(hash) + } + + /// Returns a new `EvmTransactionHash` directly initialized with the provided bytes. + pub const fn from_raw(raw_digest: [u8; HASH_LENGTH]) -> Self { + EvmTransactionHash(Digest::from_raw(raw_digest)) + } + + /// Returns the wrapped inner digest. + pub fn inner(&self) -> &Digest { + &self.0 + } + + /// Returns the wrapped hash. + pub fn hash(self) -> Hash { + Hash::new(self.0.value()) + } + + /// Returns the raw bytes backing this hash. + 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 { + base16::encode_lower(&self.0) + } + + /// Returns a random EVM transaction hash. + #[cfg(any(feature = "testing", test))] + pub fn random(rng: &mut TestRng) -> Self { + EvmTransactionHash(Digest::from(rng.gen::<[u8; HASH_LENGTH]>())) + } +} + +impl AsRef<[u8]> for EvmTransactionHash { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Display for EvmTransactionHash { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "0x{}", base16::encode_lower(&self.0)) + } +} + +impl ToBytes for EvmTransactionHash { + 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 EvmTransactionHash { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + Digest::from_bytes(bytes).map(|(hash, remainder)| (EvmTransactionHash(hash), remainder)) + } +} + +impl From for EvmTransactionHash { + fn from(digest: Digest) -> Self { + EvmTransactionHash(digest) + } +} + +impl From for Digest { + fn from(transaction_hash: EvmTransactionHash) -> Self { + transaction_hash.0 + } +} + +/// 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))] +pub enum EvmTransactionKind { + /// A legacy Ethereum transaction. + Legacy, + /// An EIP-2930 access-list transaction. + Eip2930, + /// An EIP-1559 dynamic-fee transaction. + Eip1559, + /// An EIP-7702 set-code transaction. + Eip7702, +} + +impl EvmTransactionKind { + /// Returns the Ethereum transaction type ID for this transaction kind. + pub const fn type_id(self) -> u8 { + match self { + EvmTransactionKind::Legacy => LEGACY_TRANSACTION_TYPE_ID, + EvmTransactionKind::Eip2930 => EIP2930_TRANSACTION_TYPE_ID, + EvmTransactionKind::Eip1559 => EIP1559_TRANSACTION_TYPE_ID, + EvmTransactionKind::Eip7702 => EIP7702_TRANSACTION_TYPE_ID, + } + } + + fn tag(self) -> u8 { + self.type_id() + } +} + +impl Display for EvmTransactionKind { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + match self { + EvmTransactionKind::Legacy => formatter.write_str("legacy"), + EvmTransactionKind::Eip2930 => formatter.write_str("eip2930"), + EvmTransactionKind::Eip1559 => formatter.write_str("eip1559"), + EvmTransactionKind::Eip7702 => formatter.write_str("eip7702"), + } + } +} + +impl ToBytes for EvmTransactionKind { + 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 EvmTransactionKind { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let kind = match tag { + 0 => EvmTransactionKind::Legacy, + 1 => EvmTransactionKind::Eip2930, + 2 => EvmTransactionKind::Eip1559, + EIP7702_TRANSACTION_TYPE_ID => EvmTransactionKind::Eip7702, + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((kind, remainder)) + } +} + +/// A Casper approval plus the Ethereum recovery parity for the same signature. +/// +/// Casper [`Signature::Secp256k1`] stores the canonical 64-byte ECDSA +/// signature, `r || s`. Ethereum signed transactions carry one extra bit, +/// historically encoded as `v` and in typed transactions as `yParity`, so the +/// sender can be recovered from the transaction payload. `EvmApproval` keeps +/// that Ethereum recovery parity next to the normal Casper approval, allowing +/// the signed Ethereum envelope and transaction hash to be reconstructed +/// without guessing which recovery ID was present in the original payload. +#[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 EvmApproval { + approval: Approval, + y_parity: bool, +} + +impl EvmApproval { + /// Creates a new EVM approval from a Casper approval and Ethereum recovery parity. + pub fn new(approval: Approval, y_parity: bool) -> Self { + EvmApproval { approval, y_parity } + } + + /// Returns the Casper approval carrying the signer public key and `(r, s)` signature. + pub fn approval(&self) -> &Approval { + &self.approval + } + + /// Returns the Ethereum signature recovery parity. + pub fn y_parity(&self) -> bool { + self.y_parity + } + + /// Returns the public key of the EVM approval's signer. + pub fn signer(&self) -> &PublicKey { + self.approval.signer() + } + + /// Returns the secp256k1 signature stored in the wrapped Casper approval. + pub fn signature(&self) -> &Signature { + self.approval.signature() + } +} + +impl ToBytes for EvmApproval { + 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.approval.serialized_length() + self.y_parity.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.approval.write_bytes(writer)?; + self.y_parity.write_bytes(writer) + } +} + +impl FromBytes for EvmApproval { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (approval, remainder) = Approval::from_bytes(bytes)?; + let (y_parity, remainder) = bool::from_bytes(remainder)?; + Ok((EvmApproval { approval, y_parity }, 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))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum EvmTransactionError { + /// 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, + /// 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. + 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 { + /// EvmTransaction 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 { + /// EvmTransaction 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 { + /// EvmTransaction maximum priority fee per gas. + max_priority_fee_per_gas: u128, + }, + /// The transaction gas limit exceeds the configured EVM block gas limit. + GasLimitExceedsBlockGasLimit { + /// EvmTransaction gas limit. + gas_limit: u64, + /// 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, + /// EvmTransaction nonce. + actual: u64, + }, + /// The transaction does not contain an EVM approval. + MissingApproval, + /// 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), + /// Reconstructing the signed envelope produced metadata different from this transaction. + InconsistentEnvelope, +} + +impl Display for EvmTransactionError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + match self { + EvmTransactionError::Decode(error) => { + write!(formatter, "EVM transaction decode error: {error}") + } + EvmTransactionError::Disabled => formatter.write_str("EVM transactions are disabled"), + EvmTransactionError::UnsupportedTransactionType(kind) => { + write!(formatter, "unsupported EVM transaction type: {kind}") + } + EvmTransactionError::UnsupportedAccessList => { + formatter.write_str("unsupported EVM transaction access list") + } + EvmTransactionError::UnexpectedAuthorizationList => { + formatter.write_str("unexpected EVM set-code authorization list") + } + EvmTransactionError::EmptyAuthorizationList => { + formatter.write_str("missing EVM set-code authorization list") + } + EvmTransactionError::MissingSetCodeTarget => { + formatter.write_str("missing EVM set-code transaction target") + } + EvmTransactionError::MissingChainId => formatter.write_str("missing EVM chain ID"), + EvmTransactionError::MissingGasPrice => formatter.write_str("missing EVM gas price"), + EvmTransactionError::MissingTransactionLane => { + formatter.write_str("missing EVM transaction lane") + } + EvmTransactionError::ChainIdMismatch { expected, actual } => { + write!( + formatter, + "EVM chain ID mismatch: expected {expected}, got {actual}" + ) + } + EvmTransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee, + } => { + write!( + formatter, + "EVM gas price {gas_price} is below base fee {base_fee}" + ) + } + EvmTransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee, + } => { + write!( + formatter, + "EVM max fee per gas {max_fee_per_gas} is below base fee {base_fee}" + ) + } + EvmTransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas, + } => { + write!( + formatter, + "EVM max priority fee per gas {max_priority_fee_per_gas} must be zero" + ) + } + EvmTransactionError::GasLimitExceedsBlockGasLimit { + gas_limit, + block_gas_limit, + } => { + write!( + formatter, + "EVM gas limit {gas_limit} exceeds block gas limit {block_gas_limit}" + ) + } + EvmTransactionError::InvalidNonce { expected, actual } => { + write!( + formatter, + "EVM transaction nonce {actual} does not match account nonce {expected}" + ) + } + EvmTransactionError::MissingApproval => formatter.write_str("missing EVM approval"), + EvmTransactionError::NonSecp256k1Approval => { + formatter.write_str("EVM approval must use secp256k1") + } + EvmTransactionError::InvalidApprovalSignature => { + formatter.write_str("invalid EVM approval signature") + } + EvmTransactionError::SenderMismatch => { + formatter.write_str("EVM approval signer does not match transaction sender") + } + EvmTransactionError::HashMismatch => { + formatter.write_str("EVM transaction hash does not match approval") + } + EvmTransactionError::SenderRecovery(error) => { + write!(formatter, "EVM transaction sender recovery error: {error}") + } + EvmTransactionError::InconsistentEnvelope => { + formatter.write_str("EVM transaction fields do not match signed envelope") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for EvmTransactionError {} + +/// An unsigned Ethereum transaction payload plus one Ethereum-style approval. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct EvmTransaction { + timestamp: Timestamp, + ttl: TimeDiff, + initiator_addr: InitiatorAddr, + hash: EvmTransactionHash, + from: Address, + kind: EvmTransactionKind, + 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: U256, + input: Vec, + chain_id: Option, + authorization_list: Vec, + approval: Option, +} + +#[cfg(any(feature = "std", test))] +#[derive(Serialize)] +struct EvmTransactionSerHelper<'a> { + timestamp: Timestamp, + ttl: TimeDiff, + initiator_addr: &'a InitiatorAddr, + hash: EvmTransactionHash, + from: Address, + kind: EvmTransactionKind, + 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, + authorization_list: &'a Vec, + approval: &'a Option, +} + +#[cfg(any(feature = "std", test))] +#[derive(Deserialize)] +struct EvmTransactionDeserHelper { + timestamp: Timestamp, + ttl: TimeDiff, + initiator_addr: InitiatorAddr, + hash: EvmTransactionHash, + from: Address, + kind: EvmTransactionKind, + 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, + authorization_list: Vec, + approval: Option, +} + +#[cfg(any(feature = "std", test))] +impl Serialize for EvmTransaction { + fn serialize(&self, serializer: S) -> Result { + EvmTransactionSerHelper { + timestamp: self.timestamp, + ttl: self.ttl, + initiator_addr: &self.initiator_addr, + 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, + authorization_list: &self.authorization_list, + approval: &self.approval, + } + .serialize(serializer) + } +} + +#[cfg(any(feature = "std", test))] +impl<'de> Deserialize<'de> for EvmTransaction { + fn deserialize>(deserializer: D) -> Result { + let helper = EvmTransactionDeserHelper::deserialize(deserializer)?; + let transaction = EvmTransaction { + timestamp: helper.timestamp, + ttl: helper.ttl, + initiator_addr: helper.initiator_addr, + 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, + authorization_list: helper.authorization_list, + approval: helper.approval, + }; + transaction.verify().map_err(de::Error::custom)?; + Ok(transaction) + } +} + +impl EvmTransaction { + /// 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, + initiator_addr: InitiatorAddr, + chain_id: u64, + from: Address, + to: Option
, + value: U256, + input: Vec, + gas_limit: u64, + gas_price: u128, + ) -> Self { + let mut transaction = EvmTransaction { + timestamp, + ttl, + initiator_addr, + hash: EvmTransactionHash::default(), + from, + kind: EvmTransactionKind::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(), + approval: None, + }; + transaction.hash = transaction.unsigned_call_hash(); + transaction + } + + fn unsigned_call_hash(&self) -> EvmTransactionHash { + 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.initiator_addr + .write_bytes(&mut bytes) + .expect("initiator address 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"); + EvmTransactionHash::new(Digest::hash(bytes)) + } + + fn serialized_field_lengths(&self) -> Vec { + let input_length = Bytes::from(self.input.clone()).serialized_length(); + let mut field_lengths = vec![ + self.timestamp.serialized_length(), + self.ttl.serialized_length(), + self.kind.serialized_length(), + self.hash.serialized_length(), + self.from.serialized_length(), + self.to.serialized_length(), + self.nonce.serialized_length(), + self.gas_limit.serialized_length(), + ]; + match self.kind { + EvmTransactionKind::Legacy | EvmTransactionKind::Eip2930 => { + field_lengths.extend([ + self.gas_price.serialized_length(), + self.value.serialized_length(), + input_length, + self.chain_id.serialized_length(), + self.approval.serialized_length(), + self.initiator_addr.serialized_length(), + ]); + } + EvmTransactionKind::Eip1559 => { + field_lengths.extend([ + self.max_fee_per_gas.serialized_length(), + self.max_priority_fee_per_gas.serialized_length(), + self.value.serialized_length(), + input_length, + self.chain_id.serialized_length(), + self.approval.serialized_length(), + self.initiator_addr.serialized_length(), + ]); + } + EvmTransactionKind::Eip7702 => { + field_lengths.extend([ + self.max_fee_per_gas.serialized_length(), + self.max_priority_fee_per_gas.serialized_length(), + self.value.serialized_length(), + input_length, + self.chain_id.serialized_length(), + self.authorization_list.serialized_length(), + self.approval.serialized_length(), + self.initiator_addr.serialized_length(), + ]); + } + } + field_lengths + } + + /// Returns `true` if this is an unsigned read-only call transaction. + pub fn is_unsigned_call(&self) -> bool { + self.approval.is_none() + && self.hash == self.unsigned_call_hash() + && self.kind == EvmTransactionKind::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, + 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(EvmTransactionError::UnsupportedTransactionType( + raw_signed_rlp[0], + )); + } + + let mut encoded = raw_signed_rlp.as_slice(); + let envelope = TxEnvelope::decode_2718(&mut encoded) + .map_err(|error| EvmTransactionError::Decode(format!("{error:?}")))?; + if !encoded.is_empty() { + return Err(EvmTransactionError::Decode( + "trailing bytes after transaction envelope".to_string(), + )); + } + if envelope + .access_list() + .is_some_and(|access_list| !access_list.is_empty()) + { + return Err(EvmTransactionError::UnsupportedAccessList); + } + + let kind = if envelope.is_legacy() { + EvmTransactionKind::Legacy + } else if envelope.is_eip2930() { + EvmTransactionKind::Eip2930 + } else if envelope.is_eip1559() { + EvmTransactionKind::Eip1559 + } else if envelope.is_eip7702() { + EvmTransactionKind::Eip7702 + } else { + return Err(EvmTransactionError::UnsupportedTransactionType( + envelope.tx_type() as u8, + )); + }; + let to = match envelope.kind() { + AlloyTxKind::Call(address) => Some(alloy_address_to_address(address)), + AlloyTxKind::Create => None, + }; + let signature_hash = envelope.signature_hash(); + let approval = evm_approval_from_alloy_signature(envelope.signature(), &signature_hash)?; + let initiator_addr = InitiatorAddr::AccountHash(approval.signer().to_account_hash()); + + let from = envelope + .recover_signer() + .map_err(|error| EvmTransactionError::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(EvmTransactionError::EmptyAuthorizationList); + } + transaction + .tx() + .authorization_list + .iter() + .map(SetCodeAuthorization::from_alloy) + .collect() + } + None => Vec::new(), + }; + Ok(EvmTransaction { + timestamp, + ttl, + initiator_addr, + hash: b256_to_transaction_hash(*envelope.tx_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: alloy_u256_to_casper(envelope.value()), + input: envelope.input().to_vec(), + chain_id: envelope.chain_id(), + authorization_list, + approval: Some(approval), + }) + } + + /// Reconstructs the signed Ethereum envelope and validates sender/hash consistency. + pub fn verify(&self) -> Result<(), EvmTransactionError> { + let signed = self.signed_envelope()?; + if b256_to_transaction_hash(*signed.tx_hash()) != self.hash { + return Err(EvmTransactionError::HashMismatch); + } + let recovered = signed + .recover_signer() + .map_err(|error| EvmTransactionError::SenderRecovery(format!("{error:?}")))?; + if alloy_address_to_address(recovered) != self.from { + return Err(EvmTransactionError::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<(), EvmTransactionError> { + let SecretKey::Secp256k1(signing_key) = secret_key else { + return Err(EvmTransactionError::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(|_| EvmTransactionError::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(|_| EvmTransactionError::InvalidApprovalSignature)?; + let signer = PublicKey::from(secret_key); + let initiator_addr = InitiatorAddr::AccountHash(signer.to_account_hash()); + let y_parity = recovery_id.is_y_odd(); + let approval = EvmApproval::new(Approval::new(signer, signature), y_parity); + + let recovered_key = recover_verifying_key(&signature_hash, &signature_bytes, y_parity)?; + let alloy_signature = AlloySignature::from_bytes_and_parity(&signature_bytes, y_parity); + let signed = unsigned.into_envelope(alloy_signature); + + self.approval = Some(approval); + self.initiator_addr = initiator_addr; + 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, EvmTransactionError> { + Ok(self.signed_envelope()?.encoded_2718()) + } + + /// Returns the raw signed Ethereum RLP bytes reconstructed from the approval. + pub fn raw_signed_rlp(&self) -> Result, EvmTransactionError> { + 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 bytes Ethereum signs for this unsigned payload. + pub fn signing_payload(&self) -> Result, EvmTransactionError> { + Ok(self.unsigned_transaction()?.encoded_for_signing()) + } + + /// Returns the approval attached to this transaction, if any. + pub fn approval(&self) -> Option<&Approval> { + self.approval.as_ref().map(EvmApproval::approval) + } + + /// Returns the computed approvals hash identifying this EVM transaction's approval. + pub fn compute_approvals_hash(&self) -> Result { + let approvals = self.approval().cloned().into_iter().collect(); + ApprovalsHash::compute(&approvals) + } + + /// Returns the single public key that signed this EVM transaction. + pub fn signer(&self) -> Result<&PublicKey, EvmTransactionError> { + Ok(self + .approval + .as_ref() + .ok_or(EvmTransactionError::MissingApproval)? + .signer()) + } + + /// Returns the Casper initiator address attached to this EVM transaction. + pub fn initiator_addr(&self) -> &InitiatorAddr { + &self.initiator_addr + } + + /// Returns this transaction with a replacement EVM approval. + /// + /// The stored Ethereum transaction hash is intentionally left unchanged; + /// [`EvmTransaction::verify`] rejects a replacement approval that does not + /// reconstruct the same signed Ethereum transaction. + pub fn with_evm_approval(mut self, approval: Option) -> Self { + self.approval = approval; + self + } + + /// 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) -> EvmTransactionHash { + self.hash + } + + /// Returns the recovered Ethereum sender address. + pub fn from(&self) -> Address { + self.from + } + + /// Returns the transaction envelope kind. + pub fn kind(&self) -> EvmTransactionKind { + 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. + /// + /// 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 amount of wei transferred by this transaction. + pub fn value(&self) -> U256 { + 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 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. + /// 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 { + EvmTransactionKind::Legacy | EvmTransactionKind::Eip2930 => { + self.gas_price.unwrap_or(self.max_fee_per_gas) + } + EvmTransactionKind::Eip1559 | EvmTransactionKind::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 { + 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() + } + + /// Returns the timestamp of when the transaction expires. + 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(EvmTransactionError::SenderMismatch); + } + Ok(unsigned.into_envelope(signature)) + } + + fn validate_authorization_list(&self) -> Result<(), EvmTransactionError> { + match self.kind { + EvmTransactionKind::Eip7702 => { + if self.authorization_list.is_empty() { + return Err(EvmTransactionError::EmptyAuthorizationList); + } + if self.to.is_none() { + return Err(EvmTransactionError::MissingSetCodeTarget); + } + } + EvmTransactionKind::Legacy + | EvmTransactionKind::Eip2930 + | EvmTransactionKind::Eip1559 => { + if !self.authorization_list.is_empty() { + return Err(EvmTransactionError::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, + }; + let value = casper_u256_to_alloy(self.value); + let input = AlloyBytes::from(self.input.clone()); + match self.kind { + EvmTransactionKind::Legacy => Ok(TypedTransaction::Legacy(TxLegacy { + chain_id: self.chain_id, + nonce: self.nonce, + gas_price: self.gas_price.ok_or(EvmTransactionError::MissingGasPrice)?, + gas_limit: self.gas_limit, + to, + value, + input, + })), + EvmTransactionKind::Eip2930 => Ok(TypedTransaction::Eip2930(TxEip2930 { + chain_id: self.chain_id.ok_or(EvmTransactionError::MissingChainId)?, + nonce: self.nonce, + gas_price: self.gas_price.ok_or(EvmTransactionError::MissingGasPrice)?, + gas_limit: self.gas_limit, + to, + value, + access_list: AccessList::default(), + input, + })), + EvmTransactionKind::Eip1559 => Ok(TypedTransaction::Eip1559(TxEip1559 { + chain_id: self.chain_id.ok_or(EvmTransactionError::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, + })), + EvmTransactionKind::Eip7702 => { + let address = self.to.expect("EIP-7702 target validated above"); + Ok(TypedTransaction::Eip7702(TxEip7702 { + chain_id: self.chain_id.ok_or(EvmTransactionError::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, + })) + } + } + } + + fn approval_signature( + &self, + signature_hash: &B256, + ) -> Result<(AlloySignature, Address), EvmTransactionError> { + let approval = self + .approval + .as_ref() + .ok_or(EvmTransactionError::MissingApproval)?; + let raw_signature = secp256k1_signature_bytes(approval)?; + let expected_signer = approval.signer(); + let y_parity = approval.y_parity(); + 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 Err(EvmTransactionError::InvalidApprovalSignature); + } + let expected_initiator_addr = InitiatorAddr::AccountHash(expected_signer.to_account_hash()); + if self.initiator_addr != expected_initiator_addr { + return Err(EvmTransactionError::InvalidApprovalSignature); + } + Ok(( + alloy_signature, + evm_address_from_verifying_key(&recovered_key), + )) + } +} + +impl Display for EvmTransaction { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!( + formatter, + "EVM transaction {} from {}", + self.hash.to_hex_string(), + self.from + ) + } +} + +impl ToBytes for EvmTransaction { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let builder = CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? + .add_field(TIMESTAMP_FIELD_INDEX, &self.timestamp)? + .add_field(TTL_FIELD_INDEX, &self.ttl)? + .add_field(KIND_FIELD_INDEX, &self.kind)? + .add_field(HASH_FIELD_INDEX, &self.hash)? + .add_field(FROM_FIELD_INDEX, &self.from)? + .add_field(TO_FIELD_INDEX, &self.to)? + .add_field(NONCE_FIELD_INDEX, &self.nonce)? + .add_field(GAS_LIMIT_FIELD_INDEX, &self.gas_limit)?; + + match self.kind { + EvmTransactionKind::Legacy | EvmTransactionKind::Eip2930 => { + let input = Bytes::from(self.input.clone()); + builder + .add_field(LEGACY_GAS_PRICE_FIELD_INDEX, &self.gas_price)? + .add_field(LEGACY_VALUE_FIELD_INDEX, &self.value)? + .add_field(LEGACY_INPUT_FIELD_INDEX, &input)? + .add_field(LEGACY_CHAIN_ID_FIELD_INDEX, &self.chain_id)? + .add_field(LEGACY_APPROVAL_FIELD_INDEX, &self.approval)? + .add_field(INITIATOR_ADDR_FIELD_INDEX, &self.initiator_addr)? + .binary_payload_bytes() + } + EvmTransactionKind::Eip1559 => { + let input = Bytes::from(self.input.clone()); + builder + .add_field(DYNAMIC_MAX_FEE_PER_GAS_FIELD_INDEX, &self.max_fee_per_gas)? + .add_field( + DYNAMIC_MAX_PRIORITY_FEE_PER_GAS_FIELD_INDEX, + &self.max_priority_fee_per_gas, + )? + .add_field(DYNAMIC_VALUE_FIELD_INDEX, &self.value)? + .add_field(DYNAMIC_INPUT_FIELD_INDEX, &input)? + .add_field(DYNAMIC_CHAIN_ID_FIELD_INDEX, &self.chain_id)? + .add_field(DYNAMIC_APPROVAL_FIELD_INDEX, &self.approval)? + .add_field(INITIATOR_ADDR_FIELD_INDEX, &self.initiator_addr)? + .binary_payload_bytes() + } + EvmTransactionKind::Eip7702 => { + let input = Bytes::from(self.input.clone()); + builder + .add_field(DYNAMIC_MAX_FEE_PER_GAS_FIELD_INDEX, &self.max_fee_per_gas)? + .add_field( + DYNAMIC_MAX_PRIORITY_FEE_PER_GAS_FIELD_INDEX, + &self.max_priority_fee_per_gas, + )? + .add_field(DYNAMIC_VALUE_FIELD_INDEX, &self.value)? + .add_field(DYNAMIC_INPUT_FIELD_INDEX, &input)? + .add_field(DYNAMIC_CHAIN_ID_FIELD_INDEX, &self.chain_id)? + .add_field( + EIP7702_AUTHORIZATION_LIST_FIELD_INDEX, + &self.authorization_list, + )? + .add_field(EIP7702_APPROVAL_FIELD_INDEX, &self.approval)? + .add_field(INITIATOR_ADDR_FIELD_INDEX, &self.initiator_addr)? + .binary_payload_bytes() + } + } + } + + fn serialized_length(&self) -> usize { + CalltableSerializationEnvelope::estimate_size(self.serialized_field_lengths()) + } +} + +impl FromBytes for EvmTransaction { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + Self::from_calltable_bytes(bytes) + } +} + +impl EvmTransaction { + fn from_calltable_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (binary_payload, remainder) = + CalltableSerializationEnvelope::from_bytes(EVM_TRANSACTION_MAX_CURRENT_FIELDS, bytes)?; + let window = binary_payload + .start_consuming()? + .ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(TIMESTAMP_FIELD_INDEX)?; + let (timestamp, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(TTL_FIELD_INDEX)?; + let (ttl, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(KIND_FIELD_INDEX)?; + let (kind, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(HASH_FIELD_INDEX)?; + let (hash, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(FROM_FIELD_INDEX)?; + let (from, window) = window.deserialize_and_maybe_next::
()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(TO_FIELD_INDEX)?; + let (to, window) = window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(NONCE_FIELD_INDEX)?; + let (nonce, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(GAS_LIMIT_FIELD_INDEX)?; + let (gas_limit, window) = window.deserialize_and_maybe_next::()?; + + let transaction = match kind { + EvmTransactionKind::Legacy | EvmTransactionKind::Eip2930 => { + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(LEGACY_GAS_PRICE_FIELD_INDEX)?; + let (gas_price, window) = window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(LEGACY_VALUE_FIELD_INDEX)?; + let (value, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(LEGACY_INPUT_FIELD_INDEX)?; + let (input, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(LEGACY_CHAIN_ID_FIELD_INDEX)?; + let (chain_id, window) = window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(LEGACY_APPROVAL_FIELD_INDEX)?; + let (approval, window) = + window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(INITIATOR_ADDR_FIELD_INDEX)?; + let (initiator_addr, window) = + window.deserialize_and_maybe_next::()?; + if window.is_some() { + return Err(bytesrepr::Error::Formatting); + } + let max_fee_per_gas = if approval.is_none() { + 0 + } else { + gas_price.unwrap_or_default() + }; + EvmTransaction { + timestamp, + ttl, + initiator_addr, + hash, + from, + kind, + to, + nonce, + gas_limit, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas: None, + value, + input: input.into(), + chain_id, + authorization_list: Vec::new(), + approval, + } + } + EvmTransactionKind::Eip1559 => { + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_MAX_FEE_PER_GAS_FIELD_INDEX)?; + let (max_fee_per_gas, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_MAX_PRIORITY_FEE_PER_GAS_FIELD_INDEX)?; + let (max_priority_fee_per_gas, window) = + window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_VALUE_FIELD_INDEX)?; + let (value, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_INPUT_FIELD_INDEX)?; + let (input, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_CHAIN_ID_FIELD_INDEX)?; + let (chain_id, window) = window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_APPROVAL_FIELD_INDEX)?; + let (approval, window) = + window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(INITIATOR_ADDR_FIELD_INDEX)?; + let (initiator_addr, window) = + window.deserialize_and_maybe_next::()?; + if window.is_some() { + return Err(bytesrepr::Error::Formatting); + } + EvmTransaction { + timestamp, + ttl, + initiator_addr, + hash, + from, + kind, + to, + nonce, + gas_limit, + gas_price: None, + max_fee_per_gas, + max_priority_fee_per_gas, + value, + input: input.into(), + chain_id, + authorization_list: Vec::new(), + approval, + } + } + EvmTransactionKind::Eip7702 => { + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_MAX_FEE_PER_GAS_FIELD_INDEX)?; + let (max_fee_per_gas, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_MAX_PRIORITY_FEE_PER_GAS_FIELD_INDEX)?; + let (max_priority_fee_per_gas, window) = + window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_VALUE_FIELD_INDEX)?; + let (value, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_INPUT_FIELD_INDEX)?; + let (input, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(DYNAMIC_CHAIN_ID_FIELD_INDEX)?; + let (chain_id, window) = window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(EIP7702_AUTHORIZATION_LIST_FIELD_INDEX)?; + let (authorization_list, window) = + window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(EIP7702_APPROVAL_FIELD_INDEX)?; + let (approval, window) = + window.deserialize_and_maybe_next::>()?; + let window = window.ok_or(bytesrepr::Error::Formatting)?; + window.verify_index(INITIATOR_ADDR_FIELD_INDEX)?; + let (initiator_addr, window) = + window.deserialize_and_maybe_next::()?; + if window.is_some() { + return Err(bytesrepr::Error::Formatting); + } + EvmTransaction { + timestamp, + ttl, + initiator_addr, + hash, + from, + kind, + to, + nonce, + gas_limit, + gas_price: None, + max_fee_per_gas, + max_priority_fee_per_gas, + value, + input: input.into(), + chain_id, + authorization_list, + approval, + } + } + }; + Self::finish_from_bytes(transaction, remainder) + } + + fn finish_from_bytes( + transaction: EvmTransaction, + remainder: &[u8], + ) -> Result<(Self, &[u8]), bytesrepr::Error> { + if !transaction.is_unsigned_call() { + transaction + .verify() + .map_err(|_| bytesrepr::Error::Formatting)?; + } + Ok((transaction, remainder)) + } +} + +fn evm_approval_from_alloy_signature( + signature: &AlloySignature, + signature_hash: &B256, +) -> Result { + let y_parity = signature.v(); + 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, y_parity)?; + let signer = public_key_from_verifying_key(&recovered_key)?; + let signature = Signature::secp256k1(signature_bytes) + .map_err(|_| EvmTransactionError::InvalidApprovalSignature)?; + let approval = Approval::new(signer, signature); + Ok(EvmApproval::new(approval, y_parity)) +} + +fn secp256k1_signature_bytes(approval: &EvmApproval) -> Result<[u8; 64], EvmTransactionError> { + if !matches!(approval.signer(), PublicKey::Secp256k1(_)) + || !matches!(approval.signature(), Signature::Secp256k1(_)) + { + return Err(EvmTransactionError::NonSecp256k1Approval); + } + let signature_bytes = Vec::::from(approval.signature()); + signature_bytes + .try_into() + .map_err(|_| EvmTransactionError::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(|_| EvmTransactionError::InvalidApprovalSignature)?; + let recovery_id = RecoveryId::new(y_parity, false); + VerifyingKey::recover_from_prehash(signature_hash.as_slice(), &signature, recovery_id) + .map_err(|_| EvmTransactionError::InvalidApprovalSignature) +} + +fn public_key_from_verifying_key(key: &VerifyingKey) -> Result { + PublicKey::secp256k1_from_bytes(key.to_encoded_point(true).as_ref()) + .map_err(|_| EvmTransactionError::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()); + Address::new(bytes) +} + +fn b256_to_hash(hash: B256) -> Hash { + Hash::new(hash.0) +} + +fn b256_to_transaction_hash(hash: B256) -> EvmTransactionHash { + EvmTransactionHash::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) +} + +#[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: EvmTransaction = + 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")); + } + + #[test] + fn unsigned_call_transaction_bytesrepr_roundtrips_without_approvals() { + let transaction = EvmTransaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(300), + test_initiator_addr(), + 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.approval().is_none()); + assert!(transaction.is_unsigned_call()); + assert!(matches!( + transaction.verify(), + Err(EvmTransactionError::MissingApproval) + )); + bytesrepr::test_serialization_roundtrip(&transaction); + } + + #[test] + fn signed_legacy_transaction_bytesrepr_roundtrips() { + let transaction = signed_legacy_transaction(); + + assert_eq!( + transaction.max_fee_per_gas(), + transaction + .gas_price() + .expect("legacy gas price should exist") + ); + bytesrepr::test_serialization_roundtrip(&transaction); + } + + #[test] + fn non_marker_unsigned_transaction_bytesrepr_is_rejected() { + let mut transaction = EvmTransaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(300), + test_initiator_addr(), + 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.approval().is_none()); + assert!(!transaction.is_unsigned_call()); + assert!(EvmTransaction::from_bytes( + &transaction + .to_bytes() + .expect("transaction should serialize") + ) + .is_err()); + } + + fn signed_legacy_transaction() -> EvmTransaction { + 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(); + + EvmTransaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") + } + + fn test_initiator_addr() -> InitiatorAddr { + InitiatorAddr::AccountHash(crate::account::AccountHash::new([9; 32])) + } + + fn signed_eip7702_transaction() -> EvmTransaction { + 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(); + + EvmTransaction::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/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..9b980de710 --- /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, 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: evm::Address, + /// 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: evm::Address::new(rng.gen()), + 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) = 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)?; + 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); } } diff --git a/types/src/gens.rs b/types/src/gens.rs index 163384ba9a..302a6bd4ae 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::{ @@ -52,11 +53,11 @@ use crate::{ }, AccessRights, AddressableEntity, AddressableEntityHash, BlockTime, ByteCode, ByteCodeAddr, CLType, CLValue, Digest, EntityAddr, EntityEntryPoint, EntityKind, EntryPointAccess, - EntryPointAddr, EntryPointPayment, EntryPointType, EntryPoints, EraId, Group, InitiatorAddr, - Key, NamedArg, Package, Parameter, Phase, PricingMode, ProtocolVersion, PublicKey, RuntimeArgs, - SemVer, StoredValue, TimeDiff, Timestamp, Transaction, TransactionEntryPoint, - TransactionInvocationTarget, TransactionScheduling, TransactionTarget, TransactionV1, URef, - U128, U256, U512, + EntryPointAddr, EntryPointPayment, EntryPointType, EntryPoints, EraId, EvmAddr, Group, + InitiatorAddr, Key, NamedArg, Package, Parameter, Phase, PricingMode, ProtocolVersion, + PublicKey, RuntimeArgs, SemVer, StoredValue, TimeDiff, Timestamp, Transaction, + TransactionEntryPoint, TransactionInvocationTarget, TransactionScheduling, TransactionTarget, + TransactionV1, URef, U128, U256, U512, }; use proptest::{ array, bits, bool, @@ -197,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), ] } @@ -317,6 +319,21 @@ 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| EvmAddr::Account(evm::Address::new(bytes))), + u8_slice_32().prop_map(|bytes| EvmAddr::ByteCode(evm::Hash::new(bytes))), + (prop::array::uniform20(any::()), u256_arb()).prop_map(|(address, slot)| { + EvmAddr::Storage(evm::StorageAddr::new(evm::Address::new(address), slot)) + }), + prop::array::uniform20(any::()) + .prop_map(|bytes| EvmAddr::Nonce(evm::Address::new(bytes))), + prop::array::uniform20(any::()) + .prop_map(|bytes| EvmAddr::CodeHash(evm::Address::new(bytes))), + ] +} + pub fn u512_arb() -> impl Strategy { prop_oneof![ 1 => Just(U512::zero()), diff --git a/types/src/key.rs b/types/src/key.rs index 141a278838..bd0d320e78 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, EvmAddr, Hash as EvmHash, StorageAddr as EvmStorageAddr, + ADDRESS_LENGTH as EVM_ADDRESS_LENGTH, + }, package::PackageHash, system::{ auction::{BidAddr, BidAddrTag}, @@ -55,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-"; @@ -80,6 +84,12 @@ 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-"; +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 pub const BLAKE2B_DIGEST_LENGTH: usize = 32; @@ -167,13 +177,14 @@ pub enum KeyTag { EntryPoint = 23, State = 24, RewardsHandling = 25, + 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..=23) { + match rng.gen_range(0..=26) { 0 => KeyTag::Account, 1 => KeyTag::Hash, 2 => KeyTag::URef, @@ -199,7 +210,9 @@ impl KeyTag { 22 => KeyTag::BalanceHold, 23 => KeyTag::EntryPoint, 24 => KeyTag::State, - _ => panic!(), + 25 => KeyTag::RewardsHandling, + 26 => KeyTag::Evm, + _ => unreachable!(), } } } @@ -233,6 +246,7 @@ impl Display for KeyTag { KeyTag::State => write!(f, "State"), KeyTag::EntryPoint => write!(f, "EntryPoint"), KeyTag::RewardsHandling => write!(f, "RewardsHandling"), + KeyTag::Evm => write!(f, "Evm"), } } } @@ -284,6 +298,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::Evm as u8 => KeyTag::Evm, _ => return Err(Error::Formatting), }; Ok((tag, rem)) @@ -350,6 +365,8 @@ pub enum Key { State(EntityAddr), /// A `Key` under which we store rewards handling information RewardsHandling, + /// A `Key` under which EVM account, bytecode, or storage data is stored. + Evm(EvmAddr), } #[cfg(feature = "json-schema")] @@ -424,6 +441,16 @@ 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), + /// EVM nonce key parse error. + EvmNonce(String), + /// EVM code hash key parse error. + EvmCodeHash(String), RewardsHandling(String), /// Unknown prefix. UnknownPrefix, @@ -510,6 +537,21 @@ 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::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) @@ -518,6 +560,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)] @@ -549,6 +597,7 @@ impl Key { Key::EntryPoint(_) => String::from("Key::EntryPoint"), Key::State(_) => String::from("Key::State"), Key::RewardsHandling => String::from("Key::RewardsHandling"), + Key::Evm(_) => String::from("Key::Evm"), } } @@ -684,6 +733,26 @@ impl Key { base16::encode_lower(&PADDING_BYTES) ) } + Key::Evm(EvmAddr::Account(address)) => { + format!("{}{}", EVM_ACCOUNT_PREFIX, address.to_hex_string()) + } + Key::Evm(EvmAddr::ByteCode(hash)) => { + format!("{}{}", EVM_BYTE_CODE_PREFIX, hash.to_hex_string()) + } + Key::Evm(EvmAddr::Storage(addr)) => { + format!( + "{}{}{}", + EVM_STORAGE_PREFIX, + addr.address().to_hex_string(), + 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()) + } } } @@ -1011,6 +1080,58 @@ 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::Evm(EvmAddr::Account(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::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_STORAGE_FORMATTED_LENGTH { + return Err(FromStrError::EvmStorage(format!( + "expected {} bytes, got {}", + EVM_STORAGE_FORMATTED_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::Evm(EvmAddr::Storage(EvmStorageAddr::new( + EvmAddress::new(address), + U256::from_big_endian(&slot), + )))); + } + + 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) } @@ -1151,6 +1272,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::Evm(EvmAddr::Account(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::Evm(EvmAddr::Storage(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 +1468,8 @@ impl Key { | Key::Dictionary(_) | Key::Message(_) | Key::BlockGlobal(_) - | Key::EntryPoint(_) => true, + | Key::EntryPoint(_) + | Key::Evm(_) => true, _ => false, }; if !ret { @@ -1487,6 +1627,15 @@ impl Display for Key { "Key::RewardsHandling({})", base16::encode_lower(&PADDING_BYTES), ), + 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()) + } + Key::Evm(EvmAddr::Nonce(address)) => write!(f, "Key::Evm(Nonce({}))", address), + Key::Evm(EvmAddr::CodeHash(address)) => { + write!(f, "Key::Evm(CodeHash({}))", address) + } } } } @@ -1526,6 +1675,7 @@ impl Tagged for Key { Key::EntryPoint(_) => KeyTag::EntryPoint, Key::State(_) => KeyTag::State, Key::RewardsHandling => KeyTag::RewardsHandling, + Key::Evm(_) => KeyTag::Evm, } } } @@ -1644,6 +1794,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::Evm(addr) => KEY_ID_SERIALIZED_LENGTH + addr.serialized_length(), } } @@ -1679,6 +1830,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::Evm(addr) => addr.write_bytes(writer), } } } @@ -1801,6 +1953,10 @@ impl FromBytes for Key { let (_, rem) = <[u8; 32]>::from_bytes(remainder)?; Ok((Key::RewardsHandling, rem)) } + KeyTag::Evm => { + let (addr, rem) = EvmAddr::from_bytes(remainder)?; + Ok((Key::Evm(addr), rem)) + } } } } @@ -1836,13 +1992,14 @@ fn please_add_to_distribution_impl(key: Key) { Key::EntryPoint(_) => unimplemented!(), Key::State(_) => unimplemented!(), Key::RewardsHandling => 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..=24) { + match rng.gen_range(0..=26) { 0 => Key::Account(rng.gen()), 1 => Key::Hash(rng.gen()), 2 => Key::URef(rng.gen()), @@ -1868,6 +2025,8 @@ impl Distribution for Standard { 22 => Key::BalanceHold(rng.gen()), 23 => Key::EntryPoint(rng.gen()), 24 => Key::State(rng.gen()), + 25 => Key::RewardsHandling, + 26 => Key::Evm(rng.gen()), _ => unreachable!(), } } @@ -1905,6 +2064,7 @@ mod serde_helpers { EntryPoint(&'a EntryPointAddr), State(&'a EntityAddr), RewardsHandling, + Evm(&'a EvmAddr), } #[derive(Deserialize)] @@ -1936,6 +2096,7 @@ mod serde_helpers { EntryPoint(EntryPointAddr), State(EntityAddr), RewardsHandling, + Evm(EvmAddr), } impl<'a> From<&'a Key> for BinarySerHelper<'a> { @@ -1971,6 +2132,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::Evm(addr) => BinarySerHelper::Evm(addr), } } } @@ -2010,6 +2172,7 @@ mod serde_helpers { } BinaryDeserHelper::State(entity_addr) => Key::State(entity_addr), BinaryDeserHelper::RewardsHandling => Key::RewardsHandling, + BinaryDeserHelper::Evm(addr) => Key::Evm(addr), } } } diff --git a/types/src/lib.rs b/types/src/lib.rs index 5b038d9f21..264086c007 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; @@ -164,6 +165,10 @@ pub use digest::{ }; pub use display_iter::DisplayIter; pub use era_id::EraId; +pub use evm::{ + EvmAddr, EvmApproval, EvmConfig, EvmSpec, EvmTransaction, EvmTransactionError, + EvmTransactionHash, EvmTransactionKind, +}; pub use gas::Gas; #[cfg(feature = "json-schema")] pub use json_pretty_printer::json_pretty_print; @@ -194,9 +199,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/stored_value.rs b/types/src/stored_value.rs index 00e307b0b2..89d229fe5e 100644 --- a/types/src/stored_value.rs +++ b/types/src/stored_value.rs @@ -292,6 +292,14 @@ impl StoredValue { } } + /// Returns EVM bytecode if this is an EVM bytecode value. + pub fn as_evm_byte_code(&self) -> Option<&ByteCode> { + match self { + StoredValue::ByteCode(byte_code) => Some(byte_code), + _ => 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 { @@ -1154,7 +1162,7 @@ mod tests { "access": "Public", "entry_point_type": "Factory" } - + ], "protocol_version": "2.0.0" } 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 d01ef03978..6fa4dc640b 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -10,7 +10,7 @@ mod initiator_addr_and_secret_key; mod package_identifier; mod pricing_mode; mod runtime_args; -mod serialization; +pub(crate) mod serialization; mod transaction_entry_point; mod transaction_hash; mod transaction_id; @@ -57,7 +57,7 @@ use crate::testing::TestRng; use crate::{ account::AccountHash, bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - Digest, Phase, SecretKey, TimeDiff, Timestamp, + evm, Digest, EvmTransaction, EvmTransactionHash, Phase, SecretKey, TimeDiff, Timestamp, }; #[cfg(any(feature = "std", test))] use crate::{Chainspec, Gas, Motes, TransactionV1Config}; @@ -67,8 +67,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; @@ -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 transaction. + Evm(EvmTransaction), } impl Transaction { @@ -160,11 +164,17 @@ impl Transaction { Transaction::V1(v1) } + /// EVM variant ctor. + pub fn from_evm(evm: EvmTransaction) -> Self { + Transaction::Evm(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.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(txn) => txn.sign(secret_key), } } @@ -214,6 +229,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.approvals().clone(), Transaction::V1(v1) => v1.approvals().clone(), + Transaction::Evm(txn) => txn.approval().cloned().into_iter().collect(), } } @@ -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(txn) => txn.compute_approvals_hash()?, }; 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,14 +293,38 @@ impl Transaction { }); TransactionId::new(TransactionHash::V1(txn_hash), approvals_hash) } + Transaction::Evm(txn) => { + let approvals_hash = txn.compute_approvals_hash().unwrap_or_else(|error| { + error!(%error, "failed to serialize EVM approvals"); + ApprovalsHash::from(Digest::default()) + }); + TransactionId::new(TransactionHash::Evm(txn.hash()), approvals_hash) + } } } - /// Returns the address of the initiator of the transaction. + /// Returns the Casper initiator address. pub fn initiator_addr(&self) -> InitiatorAddr { match self { Transaction::Deploy(deploy) => InitiatorAddr::PublicKey(deploy.account().clone()), Transaction::V1(txn) => txn.initiator_addr().clone(), + Transaction::Evm(txn) => txn.initiator_addr().clone(), + } + } + + /// 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.hash()), + _ => None, } } @@ -287,6 +333,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 +342,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.header().expires(), Transaction::V1(txn) => txn.payload().expires(), + Transaction::Evm(txn) => txn.expires(), } } @@ -311,6 +359,11 @@ impl Transaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + Transaction::Evm(txn) => txn + .approval() + .into_iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), } } @@ -325,6 +378,7 @@ impl Transaction { Transaction::V1(transaction_v1) => { Transaction::V1(transaction_v1.with_approvals(approvals)) } + Transaction::Evm(txn) => Transaction::Evm(txn), } } @@ -333,6 +387,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<&EvmTransaction> { + match self { + Transaction::Evm(evm) => Some(evm), + _ => None, } } @@ -349,6 +412,11 @@ impl Transaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + Transaction::Evm(txn) => txn + .approval() + .into_iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), } } @@ -357,6 +425,7 @@ impl Transaction { match self { Transaction::Deploy(_) => true, Transaction::V1(_) => false, + Transaction::Evm(_) => false, } } @@ -412,6 +481,7 @@ impl Transaction { Err(err) => Err(err), } } + Transaction::Evm(txn) => Ok(Gas::new(txn.gas_limit())), } } @@ -442,6 +512,15 @@ impl Transaction { .gas_cost(chainspec, lane_id, gas_price) .map_err(InvalidTransaction::from) } + 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, + ))) + } } } @@ -501,10 +580,12 @@ 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 transaction. + Evm(EvmTransaction), } #[cfg(any(feature = "std", test))] @@ -519,9 +600,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!( @@ -530,6 +611,7 @@ impl TryFrom for Transaction { )) }) } + TransactionJson::Evm(evm) => Ok(Transaction::Evm(evm)), } } } @@ -539,8 +621,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!( @@ -548,6 +631,7 @@ impl TryFrom for TransactionJson { error )) }), + Transaction::Evm(evm) => Ok(TransactionJson::Evm(evm)), } } } @@ -583,6 +667,12 @@ impl From for Transaction { } } +impl From for Transaction { + fn from(txn: EvmTransaction) -> Self { + Self::Evm(txn) + } +} + impl ToBytes for Transaction { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut buffer = bytesrepr::allocate_buffer(self)?; @@ -595,6 +685,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 +699,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 +719,10 @@ impl FromBytes for Transaction { let (txn, remainder) = TransactionV1::from_bytes(remainder)?; Ok((Transaction::V1(txn), remainder)) } + EVM_TAG => { + let (txn, remainder) = EvmTransaction::from_bytes(remainder)?; + Ok((Transaction::Evm(txn), remainder)) + } _ => Err(bytesrepr::Error::Formatting), } } @@ -634,6 +733,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..15208ea1f8 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/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/error.rs b/types/src/transaction/error.rs index 6676a2e8ef..bff587e9fe 100644 --- a/types/src/transaction/error.rs +++ b/types/src/transaction/error.rs @@ -1,4 +1,4 @@ -use crate::InvalidDeploy; +use crate::{EvmTransactionError, 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(EvmTransactionError), } impl From for InvalidTransaction { @@ -34,12 +36,19 @@ impl From for InvalidTransaction { } } +impl From for InvalidTransaction { + fn from(value: EvmTransactionError) -> 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..215b035125 100644 --- a/types/src/transaction/initiator_addr.rs +++ b/types/src/transaction/initiator_addr.rs @@ -34,7 +34,7 @@ const ACCOUNT_HASH_FIELD_INDEX: u16 = 1; #[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 { @@ -45,7 +45,7 @@ pub enum InitiatorAddr { } impl InitiatorAddr { - /// Gets the account hash. + /// Returns the Casper account hash carried by this initiator. pub fn account_hash(&self) -> AccountHash { match self { InitiatorAddr::PublicKey(public_key) => public_key.to_account_hash(), 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/types/src/transaction/transaction_hash.rs b/types/src/transaction/transaction_hash.rs index 948be894da..31b1528a5d 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, + Digest, EvmTransactionHash, }; 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(EvmTransactionHash), } 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) => *transaction_hash.inner(), } } @@ -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(EvmTransactionHash::random(rng)), _ => panic!(), } } @@ -91,6 +96,18 @@ impl From<&TransactionV1Hash> for TransactionHash { } } +impl From for TransactionHash { + fn from(hash: EvmTransactionHash) -> Self { + Self::Evm(hash) + } +} + +impl From<&EvmTransactionHash> for TransactionHash { + fn from(hash: &EvmTransactionHash) -> 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) = EvmTransactionHash::from_bytes(remainder)?; + Ok((TransactionHash::Evm(hash), remainder)) + } _ => Err(bytesrepr::Error::Formatting), } } 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..4e20ec65de 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), } @@ -20,10 +22,11 @@ impl TransferTarget { /// Returns a random `TransferTarget`. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..3) { + match rng.gen_range(0..4) { 0 => TransferTarget::PublicKey(PublicKey::random(rng)), 1 => TransferTarget::AccountHash(rng.gen()), - 2 => TransferTarget::URef(rng.gen()), + 2 => TransferTarget::EvmAddress(evm::Address::new(rng.gen())), + 3 => TransferTarget::URef(rng.gen()), _ => unreachable!(), } } @@ -41,6 +44,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) 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 new file mode 100644 index 0000000000..9033437824 --- /dev/null +++ b/types/tests/evm_transaction.rs @@ -0,0 +1,521 @@ +use std::collections::BTreeSet; + +use alloy_consensus::{ + crypto::secp256k1, transaction::SignerRecoverable, SignableTransaction, TxEip1559, TxEip2930, + 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::{Address, Hash, EIP4844_TRANSACTION_TYPE_ID}, + Approval, ApprovalsHash, Digest, EvmApproval, EvmTransaction, EvmTransactionError, + EvmTransactionHash, EvmTransactionKind, 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() { + let signed_transaction = signed_legacy_transaction(); + let transaction = decode(signed_transaction.raw_rlp.clone()); + + assert_eq!(transaction.kind(), EvmTransactionKind::Legacy); + 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(), U256::from(123u64)); + assert_eq!(transaction.chain_id(), Some(7)); + transaction + .verify() + .expect("legacy transaction should verify"); + assert!(transaction.approval().is_some()); + assert_eq!( + transaction.signed_rlp().unwrap(), + signed_transaction.raw_rlp + ); +} + +#[test] +fn decodes_eip2930_signed_rlp() { + let signed_transaction = signed_eip2930_transaction(); + let transaction = decode(signed_transaction.raw_rlp); + + assert_eq!(transaction.kind(), EvmTransactionKind::Eip2930); + 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(), U256::from(456u64)); + assert_eq!(transaction.input(), &[0x12, 0x34]); + assert_eq!(transaction.chain_id(), Some(7)); + transaction + .verify() + .expect("EIP-2930 transaction should verify"); + bytesrepr_roundtrip(&transaction); +} + +#[test] +fn decodes_eip1559_signed_rlp() { + let signed_transaction = signed_eip1559_transaction(); + let transaction = decode(signed_transaction.raw_rlp); + + assert_eq!(transaction.kind(), EvmTransactionKind::Eip1559); + 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(), U256::from(789u64)); + assert_eq!(transaction.input(), &[0xab, 0xcd]); + assert_eq!(transaction.chain_id(), Some(7)); + transaction + .verify() + .expect("EIP-1559 transaction should verify"); + bytesrepr_roundtrip(&transaction); +} + +#[test] +fn decodes_eip7702_signed_rlp() { + let signed_transaction = signed_eip7702_transaction(); + let transaction = decode(signed_transaction.raw_rlp.clone()); + + assert_eq!(transaction.kind(), EvmTransactionKind::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(); + let ttl = TimeDiff::from_seconds(60); + + assert_eq!( + EvmTransaction::from_signed_rlp(vec![EIP4844_TRANSACTION_TYPE_ID], timestamp, ttl), + Err(EvmTransactionError::UnsupportedTransactionType( + EIP4844_TRANSACTION_TYPE_ID + )) + ); +} + +#[test] +fn non_empty_access_lists_are_rejected() { + let timestamp = Timestamp::zero(); + let ttl = TimeDiff::from_seconds(60); + + assert_eq!( + EvmTransaction::from_signed_rlp(signed_eip2930_with_access_list(), timestamp, ttl), + Err(EvmTransactionError::UnsupportedAccessList) + ); + assert_eq!( + EvmTransaction::from_signed_rlp(signed_eip7702_with_access_list(), timestamp, ttl), + Err(EvmTransactionError::UnsupportedAccessList) + ); +} + +#[test] +fn empty_eip7702_authorization_lists_are_rejected() { + assert_eq!( + EvmTransaction::from_signed_rlp( + signed_eip7702_with_authorization_list(Vec::new(), AccessList::default()), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ), + Err(EvmTransactionError::EmptyAuthorizationList) + ); +} + +#[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 evm_approvals = BTreeSet::from([evm_transaction.approval().unwrap().clone()]); + let approvals_hash = evm_transaction.compute_approvals_hash().unwrap(); + + assert_eq!(transaction.approvals(), evm_approvals); + assert_eq!( + ApprovalsHash::compute(&evm_approvals).unwrap(), + approvals_hash + ); + 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 evm_approvals = BTreeSet::from([evm_transaction.approval().unwrap().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_approvals + ); +} + +#[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.approval().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.approval(), evm_transaction.approval()); +} + +#[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(), EvmTransactionKind::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() { + 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 signed_transaction = signed_legacy_transaction(); + let transaction = decode(signed_transaction.raw_rlp); + assert_eq!( + transaction.clone().with_evm_approval(None).verify(), + Err(EvmTransactionError::MissingApproval) + ); + + let non_secp_approval = Approval::create( + &TransactionHash::from(transaction.hash()), + &ed_secret_key([42; SecretKey::ED25519_LENGTH]), + ); + assert_eq!( + transaction + .clone() + .with_evm_approval(Some(EvmApproval::new(non_secp_approval, false))) + .verify(), + Err(EvmTransactionError::NonSecp256k1Approval) + ); + + let wrong_y_parity = EvmApproval::new( + transaction + .approval() + .expect("signed transaction should have an approval") + .clone(), + !signed_transaction.signature_y_parity, + ); + assert_eq!( + transaction + .clone() + .with_evm_approval(Some(wrong_y_parity)) + .verify(), + Err(EvmTransactionError::InvalidApprovalSignature) + ); +} + +#[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 = EvmTransactionHash::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, + signature_y_parity: bool, + authorization_list: Vec, +} + +fn decode(bytes: Vec) -> EvmTransaction { + EvmTransaction::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_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 + .recover_signer() + .expect("signed transaction should recover sender"), + ); + let signature_y_parity = envelope.signature().v(); + SignedTransaction { + raw_rlp: envelope.encoded_2718(), + sender, + signature_y_parity, + authorization_list: envelope + .as_eip7702() + .map(|transaction| transaction.tx().authorization_list.clone()) + .unwrap_or_default(), + } +} + +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 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, + nonce: 0, + 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(vec![AccessListItem { + address: alloy_address(8), + storage_keys: vec![B256::from([9u8; 32])], + }]), + }; + let tx = tx.into_signed(Signature::test_signature()); + let envelope: TxEnvelope = tx.into(); + envelope.encoded_2718() +} 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),