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..f5bc7152a8 --- /dev/null +++ b/EVM.md @@ -0,0 +1,970 @@ +# 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 Casper `Approval`: + - `Approval.signer` is the recovered secp256k1 public key. + - `Approval.signature` is the 64-byte secp256k1 signature. +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 approval set contains the Ethereum +signature. + +## Approval Handling + +`Transaction::Evm` uses the same approval container and approval identity +mechanics as other transaction variants: + +- `Transaction::approvals()` returns the EVM approval set. +- `Transaction::compute_approvals_hash()` computes the hash of that approval + set. +- `Transaction::compute_id()` combines `TransactionHash::Evm` with the EVM + approvals hash. +- Finalized approvals checksums include EVM approvals through the same storage + mechanism used by other transactions. +- Block execution computes approvals checksums across EVM and non-EVM + transactions. + +The cryptographic verification is EVM-specific. Deploy and V1 transaction +approvals verify Casper signatures over Casper transaction hashes. +`Transaction::Evm::verify()` reconstructs the signed Ethereum envelope using +the stored payload and approval, then checks: + +- There is exactly one approval. +- The approval uses secp256k1. +- One of the valid secp256k1 recovery IDs recovers the approval signer. +- The recovered EVM address matches the stored `from`. +- The reconstructed Ethereum signed transaction hash matches the stored EVM + hash. + +This avoids double signing. The Ethereum signature becomes the single Casper +approval for the `Transaction::Evm` value. + +The generic `Transaction::sign` API also has an EVM implementation. For +`Transaction::Evm`, it requires a secp256k1 secret key, signs the Ethereum +signing hash using recoverable prehash semantics, replaces the approval set +with exactly one Ethereum-style approval, recomputes `from`, and recomputes the +Ethereum signed transaction hash. `evm::Transaction::try_sign` returns an +error for non-secp256k1 keys; the infallible top-level `Transaction::sign` +path panics with a clear message, matching the existing infallible signing API +style. + +Approvals for EVM transactions are part of the Ethereum transaction identity. +Changing the approval changes the reconstructed signed Ethereum envelope and +therefore the Ethereum transaction hash. For that reason finalized approval +handling treats the EVM approval as immutable transaction content. The +top-level approval replacement hook used after storage lookup leaves +`Transaction::Evm` unchanged. + +## Acceptor Validation + +`Transaction::Evm` is converted into `MetaTransaction::Evm` and routed through +the normal transaction acceptor skeleton. EVM branches only where the payload +genuinely differs from Deploy and 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..74e2d31876 100644 --- a/binary_port/src/error_code.rs +++ b/binary_port/src/error_code.rs @@ -1,6 +1,6 @@ use core::{convert::TryFrom, fmt}; -use casper_types::{InvalidDeploy, InvalidTransaction, InvalidTransactionV1}; +use casper_types::{evm, InvalidDeploy, InvalidTransaction, InvalidTransactionV1}; use num_derive::FromPrimitive; use num_traits::FromPrimitive; @@ -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(evm::TransactionError::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,7 @@ mod tests { use std::convert::TryFrom; use crate::ErrorCode; - use casper_types::{InvalidDeploy, InvalidTransactionV1}; + use casper_types::{evm, InvalidDeploy, InvalidTransaction, InvalidTransactionV1}; use strum::IntoEnumIterator; #[test] @@ -618,6 +627,19 @@ mod tests { } } + #[test] + fn evm_invalid_nonce_has_specific_error_code() { + let error = InvalidTransaction::Evm(evm::TransactionError::InvalidNonce { + expected: 0, + actual: 1, + }); + + assert_eq!( + ErrorCode::from(error), + ErrorCode::InvalidTransactionEvmInvalidNonce + ); + } + #[test] fn try_from_decoded_all_variants() { for variant in ErrorCode::iter() { diff --git a/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..56c92d1fde --- /dev/null +++ b/executor/evm/src/account_state.rs @@ -0,0 +1,338 @@ +//! Helpers for the EVM account records stored in Casper global state. +//! +//! This module is intentionally layout-focused. It translates split Casper +//! global-state records into the account shape revm needs, but it does not +//! recover transaction signers, create Casper accounts, or decide whether an EVM +//! address should be linked to a Casper account. Those policy decisions live in +//! contract runtime and transaction validation. + +use casper_storage::{tracking_copy::TrackingCopyError, TrackingCopy}; +use casper_types::{account::AccountHash, evm, CLValue, Key, StoredValue, URef}; + +/// Identity backing an EVM address. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum AccountIdentity { + /// The EVM address is linked to a Casper account. + /// + /// Balance reads and writes should go through that account's main purse. + Account(AccountHash), + /// The EVM address is backed only by an EVM purse. + /// + /// This is used for EVM-native externally owned accounts and contracts that + /// do not have a Casper account identity. + Purse(URef), +} + +/// EVM account metadata resolved into revm's account view. +/// +/// This is not a stored account type. It is the adapter result used to construct +/// revm's `AccountInfo` from independent identity, nonce, and code-hash records. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct AccountMetadata { + pub(crate) nonce: u64, + pub(crate) code_hash: evm::Hash, + pub(crate) main_purse: URef, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum AccountStorageError { + #[error(transparent)] + TrackingCopy(#[from] TrackingCopyError), + #[error("unexpected stored value for {key}: expected {expected}, found {found}")] + TypeMismatch { + key: Key, + expected: &'static str, + found: String, + }, + #[error("failed to decode {expected} at {key}: {error}")] + Decode { + key: Key, + expected: &'static str, + error: String, + }, + #[error("identity for {identity_key} points to missing account {account_key}")] + MissingAccount { identity_key: Key, account_key: Key }, +} + +pub(crate) fn read_account_metadata( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let identity = read_account_identity(tracking_copy, address)?; + let nonce = read_nonce(tracking_copy, address)?; + let code_hash = read_code_hash(tracking_copy, address)?; + + // No split records at all means revm should treat the account as absent. + // A partially present record still resolves to an account: missing nonce + // defaults to zero, missing code hash defaults to empty code, and missing + // identity falls back to the deterministic purse for this EVM address. + if identity.is_none() && nonce.is_none() && code_hash.is_none() { + return Ok(None); + } + + let main_purse = match identity { + Some(AccountIdentity::Account(account_hash)) => { + // A linked identity is only valid while the target Casper account + // exists. Treat a dangling bridge as state corruption rather than + // silently falling back to the deterministic EVM purse. + let identity_key = Key::Evm(evm::EvmAddr::Account(address)); + account_main_purse(tracking_copy, account_hash)?.ok_or( + AccountStorageError::MissingAccount { + identity_key, + account_key: Key::Account(account_hash), + }, + )? + } + Some(AccountIdentity::Purse(main_purse)) => main_purse, + // This fallback lets runtime-created contract addresses or partially + // initialized EVM-native records be read without forcing a Casper + // account link. + None => evm::deterministic_purse(address), + }; + + Ok(Some(AccountMetadata { + nonce: nonce.unwrap_or(0), + code_hash: code_hash.unwrap_or(evm::EMPTY_CODE_HASH), + main_purse, + })) +} + +pub(crate) fn read_account_identity( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let key = Key::Evm(evm::EvmAddr::Account(address)); + match tracking_copy.read(&key)? { + Some(StoredValue::CLValue(cl_value)) => { + let identity_key = cl_value_to_key(key, cl_value)?; + match identity_key { + // The only valid identity pointer variants are a Casper account + // hash or a purse. Nonce/code/storage are not stored here. + Key::Account(account_hash) => Ok(Some(AccountIdentity::Account(account_hash))), + Key::URef(uref) => Ok(Some(AccountIdentity::Purse(uref))), + other => Err(AccountStorageError::TypeMismatch { + key, + expected: "CLValue(Key::Account) or CLValue(Key::URef)", + found: other.type_string(), + }), + } + } + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key, + expected: "StoredValue::CLValue(Key)", + found: stored_value.type_name(), + }), + None => Ok(None), + } +} + +pub(crate) fn account_main_purse( + tracking_copy: &mut TrackingCopy, + account_hash: AccountHash, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let account_key = Key::Account(account_hash); + match tracking_copy.read(&account_key)? { + // Support both account representations because this helper is used by + // executor reads, runtime linking, and validation against the current + // global-state model. + Some(StoredValue::Account(account)) => Ok(Some(account.main_purse())), + Some(StoredValue::CLValue(cl_value)) => { + let key = cl_value_to_key(account_key, cl_value)?; + let Key::AddressableEntity(entity_addr) = key else { + return Err(AccountStorageError::TypeMismatch { + key: account_key, + expected: "CLValue(Key::AddressableEntity)", + found: key.type_string(), + }); + }; + let entity_key = Key::AddressableEntity(entity_addr); + match tracking_copy.read(&entity_key)? { + Some(StoredValue::AddressableEntity(entity)) => Ok(Some(entity.main_purse())), + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key: entity_key, + expected: "StoredValue::AddressableEntity", + found: stored_value.type_name(), + }), + None => Err(AccountStorageError::MissingAccount { + identity_key: account_key, + account_key: entity_key, + }), + } + } + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key: account_key, + expected: "StoredValue::Account or StoredValue::CLValue(Key::AddressableEntity)", + found: stored_value.type_name(), + }), + None => Ok(None), + } +} + +pub(crate) fn write_account_identity( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + identity_key: Key, +) -> Result<(), AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + // This helper only serializes the caller's chosen identity key. Runtime may + // write `Key::Account`; executor state application writes `Key::URef` only + // for EVM-native accounts and preserves existing `Key::Account` links. + let key = Key::Evm(evm::EvmAddr::Account(address)); + let cl_value = CLValue::from_t(identity_key).map_err(|error| AccountStorageError::Decode { + key, + expected: "Key", + error: error.to_string(), + })?; + tracking_copy.write(key, StoredValue::CLValue(cl_value)); + Ok(()) +} + +pub(crate) fn write_nonce( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + nonce: u64, +) -> Result<(), AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + // Nonce is deliberately independent from the identity pointer so linking an + // address to a Casper account does not move or rewrite EVM replay state. + let key = Key::Evm(evm::EvmAddr::Nonce(address)); + let cl_value = CLValue::from_t(nonce).map_err(|error| AccountStorageError::Decode { + key, + expected: "u64", + error: error.to_string(), + })?; + tracking_copy.write(key, StoredValue::CLValue(cl_value)); + Ok(()) +} + +pub(crate) fn write_code_hash( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + code_hash: evm::Hash, +) -> Result<(), AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + // Code hash is deliberately independent from the identity pointer so + // contracts can remain EVM-native even when EOAs may link to Casper + // accounts. + let key = Key::Evm(evm::EvmAddr::CodeHash(address)); + let cl_value = CLValue::from_t(code_hash).map_err(|error| AccountStorageError::Decode { + key, + expected: "evm::Hash", + error: error.to_string(), + })?; + tracking_copy.write(key, StoredValue::CLValue(cl_value)); + Ok(()) +} + +fn read_nonce( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let key = Key::Evm(evm::EvmAddr::Nonce(address)); + match tracking_copy.read(&key)? { + Some(StoredValue::CLValue(cl_value)) => { + cl_value + .into_t::() + .map(Some) + .map_err(|error| AccountStorageError::Decode { + key, + expected: "u64", + error: error.to_string(), + }) + } + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key, + expected: "StoredValue::CLValue(u64)", + found: stored_value.type_name(), + }), + None => Ok(None), + } +} + +fn read_code_hash( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Result, AccountStorageError> +where + R: casper_storage::global_state::state::StateReader< + Key, + StoredValue, + Error = casper_storage::global_state::error::Error, + >, +{ + let key = Key::Evm(evm::EvmAddr::CodeHash(address)); + match tracking_copy.read(&key)? { + Some(StoredValue::CLValue(cl_value)) => { + cl_value + .into_t::() + .map(Some) + .map_err(|error| AccountStorageError::Decode { + key, + expected: "evm::Hash", + error: error.to_string(), + }) + } + Some(stored_value) => Err(AccountStorageError::TypeMismatch { + key, + expected: "StoredValue::CLValue(evm::Hash)", + found: stored_value.type_name(), + }), + None => Ok(None), + } +} + +fn cl_value_to_key(key: Key, cl_value: CLValue) -> Result { + cl_value + .into_t::() + .map_err(|error| AccountStorageError::Decode { + key, + expected: "Key", + error: error.to_string(), + }) +} diff --git a/executor/evm/src/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..ad88f289a7 --- /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, 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(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(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..7139ac97a9 --- /dev/null +++ b/executor/evm/src/executor.rs @@ -0,0 +1,206 @@ +//! Public executor entry point. + +use casper_storage::{ + global_state::{error::Error as GlobalStateError, state::StateReader}, + TrackingCopy, +}; +use casper_types::{evm, Key, StoredValue}; +use revm::{ + context_interface::result::{EVMError, 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: evm::EvmConfig, +} + +impl EvmExecutor { + /// Creates a new executor from chainspec EVM configuration. + pub fn new(config: evm::EvmConfig) -> Self { + Self { config } + } + + /// Returns the immutable EVM configuration used by this executor. + pub fn config(&self) -> &evm::EvmConfig { + &self.config + } + + /// Executes an EVM transaction or call against the supplied tracking copy. + pub fn execute( + &self, + tracking_copy: &mut TrackingCopy, + request: ExecuteRequest, + ) -> Result + where + R: StateReader, + { + 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, spec, &request, &result_and_state.result); + state::remove_disabled_fee_transfers(&mut state, disabled_fee_transfers)?; + state::apply(tracking_copy, state)?; + Ok(outcome) + } +} + +fn disabled_fee_transfers( + config: &evm::EvmConfig, + spec: SpecId, + request: &ExecuteRequest, + result: &RevmExecutionResult, +) -> state::DisabledFeeTransfers { + let gas = result_gas(result); + let base_fee = u128::from(request.block.base_fee.unwrap_or(config.base_fee)); + let (caller, gas_limit, effective_gas_price) = match &request.kind { + ExecuteKind::Transaction(transaction) => ( + tx::to_revm_address(transaction.from()), + transaction.gas_limit(), + transaction.effective_gas_price(base_fee as u64), + ), + ExecuteKind::Call(call) => ( + tx::to_revm_address(call.from), + call.gas_limit, + call.gas_price, + ), + }; + + let reimbursed_gas = gas_limit + .saturating_sub(gas.total_gas_spent()) + .saturating_add(gas.inner_refunded()); + let caller_reimbursement = U256::from(effective_gas_price) * U256::from(reimbursed_gas); + // Revm still computes Ethereum fee transfers internally before we remove + // them for Casper-owned fee accounting. For node-accepted EIP-1559 + // transactions the priority fee is zero by policy, but this stays generic + // for executor callers and pre-London specs. + let coinbase_gas_price = if spec.is_enabled_in(SpecId::LONDON) { + effective_gas_price.saturating_sub(base_fee) + } else { + effective_gas_price + }; + let beneficiary_reward = U256::from(coinbase_gas_price) * U256::from(gas.tx_gas_used()); + + state::DisabledFeeTransfers { + caller, + caller_reimbursement, + beneficiary: tx::to_revm_address(request.block.beneficiary), + beneficiary_reward, + } +} + +fn result_gas(result: &RevmExecutionResult) -> &ResultGas { + match result { + RevmExecutionResult::Success { gas, .. } + | RevmExecutionResult::Revert { gas, .. } + | RevmExecutionResult::Halt { gas, .. } => gas, + } +} + +fn spec_id(spec: evm::EvmSpec) -> SpecId { + match spec { + evm::EvmSpec::Frontier => SpecId::FRONTIER, + evm::EvmSpec::FrontierThawing => SpecId::FRONTIER_THAWING, + evm::EvmSpec::Homestead => SpecId::HOMESTEAD, + evm::EvmSpec::DaoFork => SpecId::DAO_FORK, + evm::EvmSpec::Tangerine => SpecId::TANGERINE, + evm::EvmSpec::SpuriousDragon => SpecId::SPURIOUS_DRAGON, + evm::EvmSpec::Byzantium => SpecId::BYZANTIUM, + evm::EvmSpec::Constantinople => SpecId::CONSTANTINOPLE, + evm::EvmSpec::Petersburg => SpecId::PETERSBURG, + evm::EvmSpec::Istanbul => SpecId::ISTANBUL, + evm::EvmSpec::MuirGlacier => SpecId::MUIR_GLACIER, + evm::EvmSpec::Berlin => SpecId::BERLIN, + evm::EvmSpec::London => SpecId::LONDON, + evm::EvmSpec::ArrowGlacier => SpecId::ARROW_GLACIER, + evm::EvmSpec::GrayGlacier => SpecId::GRAY_GLACIER, + evm::EvmSpec::Merge => SpecId::MERGE, + evm::EvmSpec::Shanghai => SpecId::SHANGHAI, + evm::EvmSpec::Cancun => SpecId::CANCUN, + evm::EvmSpec::Prague => SpecId::PRAGUE, + evm::EvmSpec::Osaka => SpecId::OSAKA, + } +} + +fn map_revm_error(error: EVMError) -> Error { + match error { + EVMError::Database(error) => Error::Database(error), + other => Error::Revm(other.to_string()), + } +} diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs new file mode 100644 index 0000000000..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..d805fd70db --- /dev/null +++ b/executor/evm/src/request.rs @@ -0,0 +1,91 @@ +//! Public execution request types. + +use casper_types::{evm, 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(evm::Transaction), + /// 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: &evm::EvmConfig) -> revm::context::BlockEnv { + revm::context::BlockEnv { + number: revm::primitives::U256::from(self.number), + beneficiary: tx::to_revm_address(self.beneficiary), + timestamp: revm::primitives::U256::from(self.timestamp), + gas_limit: self.gas_limit.unwrap_or(config.block_gas_limit), + basefee: self.base_fee.unwrap_or(config.base_fee), + ..Default::default() + } + } +} diff --git a/executor/evm/src/state.rs b/executor/evm/src/state.rs new file mode 100644 index 0000000000..f925ba7c66 --- /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, 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(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(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(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(evm::EvmAddr::Nonce(address))); + tracking_copy.prune(Key::Evm(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..e79c0d1762 --- /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, U256 as CasperU256}; +use revm::{ + context::TxEnv, + primitives::{Address, Bytes, TxKind, B256, U256}, +}; + +use crate::{Error, ExecuteKind}; + +pub(crate) fn build_tx_env(config: &evm::EvmConfig, kind: &ExecuteKind) -> Result { + let tx_env = match kind { + ExecuteKind::Transaction(transaction) => { + let mut builder = TxEnv::builder() + .caller(to_revm_address(transaction.from())) + .gas_limit(transaction.gas_limit()) + .value(to_revm_u256(transaction.value())) + .data(Bytes::from(transaction.input().to_vec())) + .nonce(transaction.nonce()) + .chain_id(transaction.chain_id().or(Some(config.chain_id))); + + builder = match transaction.kind() { + evm::TransactionKind::Legacy | evm::TransactionKind::Eip2930 => builder.gas_price( + transaction + .gas_price() + .unwrap_or_else(|| transaction.max_fee_per_gas()), + ), + evm::TransactionKind::Eip1559 => { + 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) + } + evm::TransactionKind::Eip7702 => { + let max_priority_fee_per_gas = + Some(transaction.max_priority_fee_per_gas().unwrap_or(0)); + builder + .max_fee_per_gas(transaction.max_fee_per_gas()) + .gas_priority_fee(max_priority_fee_per_gas) + .tx_type(Some(evm::EIP7702_TRANSACTION_TYPE_ID)) + .authorization_list_signed( + transaction + .authorization_list() + .iter() + .map(to_revm_authorization) + .collect(), + ) + } + }; + + 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..551acc67c5 --- /dev/null +++ b/executor/evm/tests/executor.rs @@ -0,0 +1,1272 @@ +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, 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: evm::EvmSpec) -> EvmExecutor { + EvmExecutor::new(evm::EvmConfig { + enabled: true, + chain_id: 7, + spec, + block_gas_limit: 30_000_000, + base_fee: 0, + }) +} + +fn block() -> BlockContext { + BlockContext { + number: 1, + timestamp: 1_714_000_000, + beneficiary: evm::Address::ZERO, + gas_limit: None, + base_fee: None, + } +} + +#[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: evm::Transaction, +) -> casper_executor_evm::ExecutionOutcome { + executor + .execute( + tracking_copy, + ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(transaction), + }, + ) + .expect("EVM transaction execution should succeed") +} + +fn deploy_code>( + executor: &EvmExecutor, + tracking_copy: &mut TrackingCopy, + from: evm::Address, + code: Vec, +) -> evm::Address { + execute_call(executor, tracking_copy, from, None, code) + .created_contract_address + .expect("deploy should return a contract address") +} + +fn deploy>( + executor: &EvmExecutor, + tracking_copy: &mut TrackingCopy, + 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) -> evm::Transaction { + let tx = TxLegacy { + chain_id, + nonce: 0, + gas_price: 1, + gas_limit: 21_000, + to: TxKind::Call(AlloyAddress::from([1u8; 20])), + value: U256::ZERO, + input: Default::default(), + }; + let tx = tx.into_signed(Signature::test_signature().with_parity(true)); + let envelope: TxEnvelope = tx.into(); + evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + casper_types::TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") +} + +fn eip7702_transaction( + to: evm::Address, + delegate: evm::Address, + authorization_nonce: u64, + transaction_nonce: u64, + input: Vec, +) -> (evm::Transaction, evm::Address) { + let authorization = signed_authorization(delegate, authorization_nonce); + let authority = alloy_address_to_evm( + authorization + .recover_authority() + .expect("authorization should recover authority"), + ); + let tx = TxEip7702 { + chain_id: 7, + nonce: transaction_nonce, + gas_limit: 1_000_000, + max_fee_per_gas: 1, + max_priority_fee_per_gas: 0, + to: to_alloy_address(to), + value: U256::ZERO, + access_list: Default::default(), + authorization_list: vec![authorization], + input: input.into(), + }; + let signature = secp256k1::sign_message(B256::from(SIGNING_SECRET), tx.signature_hash()) + .expect("transaction signing should succeed"); + let envelope: TxEnvelope = tx.into_signed(signature).into(); + let transaction = evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + casper_types::TimeDiff::from_seconds(60), + ) + .expect("transaction should decode"); + (transaction, authority) +} + +fn eip7702_transaction_without_priority_fee(transaction: evm::Transaction) -> evm::Transaction { + assert_eq!(transaction.max_priority_fee_per_gas(), Some(0)); + let mut bytes = transaction + .to_bytes() + .expect("transaction should serialize"); + let mut offset = 0; + offset += transaction.timestamp().serialized_length(); + offset += transaction.ttl().serialized_length(); + offset += transaction.hash().serialized_length(); + offset += transaction.from().serialized_length(); + offset += transaction.kind().serialized_length(); + offset += transaction.to().serialized_length(); + offset += transaction.nonce().serialized_length(); + offset += transaction.gas_limit().serialized_length(); + offset += transaction.gas_price().serialized_length(); + offset += transaction.max_fee_per_gas().serialized_length(); + + assert_eq!(bytes[offset], 1); + let some_priority_length = transaction.max_priority_fee_per_gas().serialized_length(); + let none_priority = Option::::None + .to_bytes() + .expect("none priority fee should serialize"); + bytes.splice(offset..offset + some_priority_length, none_priority); + + let (transaction, remainder) = + evm::Transaction::from_bytes(&bytes).expect("transaction should deserialize"); + assert!(remainder.is_empty()); + assert_eq!(transaction.max_priority_fee_per_gas(), None); + transaction +} + +fn signed_authorization(delegate: evm::Address, nonce: u64) -> AlloySignedAuthorization { + let authorization = AlloyAuthorization { + chain_id: U256::from(7), + address: to_alloy_address(delegate), + nonce, + }; + let signature = secp256k1::sign_message( + B256::from(AUTHORIZATION_SECRET), + authorization.signature_hash(), + ) + .expect("authorization signing should succeed"); + authorization.into_signed(signature) +} + +fn authorization_authority() -> evm::Address { + alloy_address_to_evm( + signed_authorization(evm::Address::ZERO, 0) + .recover_authority() + .expect("authorization should recover authority"), + ) +} + +fn legacy_transaction_without_chain_id() -> evm::Transaction { + legacy_transaction(None) +} + +fn read_storage>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + slot: CasperU256, +) -> Option { + match tracking_copy + .read(&Key::Evm(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(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(evm::EvmAddr::Account(address)), + StoredValue::CLValue(CLValue::from_t(Key::URef(main_purse)).unwrap()), + ); + tracking_copy.write( + Key::Evm(evm::EvmAddr::Nonce(address)), + StoredValue::CLValue(CLValue::from_t(0u64).unwrap()), + ); + tracking_copy.write( + Key::Evm(evm::EvmAddr::CodeHash(address)), + StoredValue::CLValue(CLValue::from_t(EMPTY_CODE_HASH).unwrap()), + ); + tracking_copy.write( + Key::Balance(main_purse.addr()), + 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(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(evm::EvmAddr::CodeHash(address))) + .expect("code hash read should not fail") + { + Some(StoredValue::CLValue(value)) => value.into_t::().unwrap(), + Some(other) => panic!("unexpected code hash value: {other:?}"), + None => EMPTY_CODE_HASH, + } +} + +fn read_code>( + tracking_copy: &mut TrackingCopy, + code_hash: evm::Hash, +) -> Option> { + match tracking_copy + .read(&Key::Evm(evm::EvmAddr::ByteCode(code_hash))) + .expect("bytecode read should not fail") + { + Some(StoredValue::ByteCode(byte_code)) => Some(byte_code.bytes().to_vec()), + Some(other) => panic!("unexpected bytecode value: {other:?}"), + None => None, + } +} + +fn delegation_code(delegate: evm::Address) -> Vec { + let mut code = vec![0xef, 0x01, 0x00]; + code.extend_from_slice(delegate.as_bytes()); + code +} + +#[test] +fn blockhash_uses_supplied_provider() { + let executor = executor(evm::EvmSpec::Prague); + 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(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + return_word_contract_init_code(42), + ); + let (transaction, recovered_authority) = + eip7702_transaction(authority, delegate, 0, 0, Vec::new()); + assert_eq!(recovered_authority, authority); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(decode_word(&outcome.output), 42); + let code_hash = read_code_hash(&mut tracking_copy, authority); + assert_ne!(code_hash, EMPTY_CODE_HASH); + assert_eq!( + read_code(&mut tracking_copy, code_hash), + Some(delegation_code(delegate)) + ); +} + +#[test] +fn eip7702_missing_priority_fee_defaults_to_zero_for_execution() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + return_word_contract_init_code(42), + ); + let (transaction, _) = eip7702_transaction(authority, delegate, 0, 0, Vec::new()); + let transaction = eip7702_transaction_without_priority_fee(transaction); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(decode_word(&outcome.output), 42); +} + +#[test] +fn eip7702_delegation_persists_when_call_reverts() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + reverting_contract_init_code(), + ); + let (transaction, _) = eip7702_transaction(authority, delegate, 0, 0, Vec::new()); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + + assert_eq!(outcome.status, ExecutionStatus::Revert); + let code_hash = read_code_hash(&mut tracking_copy, authority); + assert_eq!( + read_code(&mut tracking_copy, code_hash), + Some(delegation_code(delegate)) + ); +} + +#[test] +fn eip7702_stale_authorization_is_skipped() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + return_word_contract_init_code(42), + ); + let (transaction, _) = eip7702_transaction(authority, delegate, 1, 0, Vec::new()); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert!(outcome.output.is_empty()); + assert_eq!(read_evm_nonce(&mut tracking_copy, authority), 0); + assert_eq!( + read_code_hash(&mut tracking_copy, authority), + EMPTY_CODE_HASH + ); +} + +#[test] +fn eip7702_zero_address_authorization_clears_delegation() { + let executor = executor(evm::EvmSpec::Prague); + let deployer = evm::Address::new([1; 20]); + let authority = authorization_authority(); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let delegate = deploy_code( + &executor, + &mut tracking_copy, + deployer, + return_word_contract_init_code(42), + ); + let (transaction, _) = eip7702_transaction(authority, delegate, 0, 0, Vec::new()); + seed_evm_balance( + &mut tracking_copy, + transaction.from(), + U512::from(1_000_000_000u64), + ); + seed_evm_balance(&mut tracking_copy, authority, U512::zero()); + let outcome = execute_transaction(&executor, &mut tracking_copy, transaction); + assert_eq!(outcome.status, ExecutionStatus::Success); + + let (clear_transaction, _) = + eip7702_transaction(authority, evm::Address::ZERO, 1, 1, Vec::new()); + let outcome = execute_transaction(&executor, &mut tracking_copy, clear_transaction); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!( + read_code_hash(&mut tracking_copy, authority), + EMPTY_CODE_HASH + ); +} + +#[test] +fn counter_supports_committed_and_discarded_execution() { + let executor = executor(evm::EvmSpec::Prague); + let from = evm::Address::new([1; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let counter = deploy(&executor, &mut tracking_copy, from, "Counter"); + + let increment = execute_call( + &executor, + &mut tracking_copy, + from, + Some(counter), + selector("increment()"), + ); + assert_eq!(decode_word(&increment.output), 1); + + let mut view = tracking_copy.fork(); + let view_increment = execute_call( + &executor, + &mut view, + from, + Some(counter), + selector("increment()"), + ); + assert_eq!(decode_word(&view_increment.output), 2); + + let get = execute_call( + &executor, + &mut tracking_copy, + from, + Some(counter), + selector("get()"), + ); + assert_eq!(decode_word(&get.output), 1); +} + +#[test] +fn erc20_and_native_purse_balances_update() { + let executor = executor(evm::EvmSpec::Prague); + let owner = evm::Address::new([1; 20]); + let recipient = evm::Address::new([2; 20]); + let spender = evm::Address::new([3; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_balance(&mut tracking_copy, owner, U512::from(1_000u64)); + let transfer_value = 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(evm::EvmSpec::Prague); + let sender = evm::Address::new([1; 20]); + let recipient = evm::Address::new([2; 20]); + let beneficiary = evm::Address::new([3; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let initial_balance = U512::from(10_000_000u64); + let transfer_value = CasperU256::from(250u64); + + seed_evm_balance(&mut tracking_copy, sender, initial_balance); + let mut block_context = block(); + block_context.beneficiary = beneficiary; + let request = ExecuteRequest { + block: block_context, + kind: ExecuteKind::Call(CallRequest { + from: sender, + to: Some(recipient), + value: transfer_value, + input: Vec::new(), + gas_limit: 100_000, + gas_price: 2, + nonce: 0, + validation: CallValidation::Checked, + }), + }; + + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("native EVM transfer should succeed"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!( + read_balance(&mut tracking_copy, sender), + initial_balance - U512::from(250u64) + ); + assert_eq!( + read_balance(&mut tracking_copy, recipient), + U512::from(250u64) + ); + assert_eq!(read_balance(&mut tracking_copy, beneficiary), U512::zero()); +} + +#[test] +fn erc721_mint_approve_and_transfer() { + let executor = executor(evm::EvmSpec::Prague); + let owner = evm::Address::new([1; 20]); + let recipient = evm::Address::new([2; 20]); + let approved = evm::Address::new([3; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let nft = deploy(&executor, &mut tracking_copy, owner, "MinimalERC721"); + + execute_call( + &executor, + &mut tracking_copy, + owner, + Some(nft), + calldata("mint(address,uint256)", &[address_word(owner), word(42)]), + ); + let initial_owner = execute_call( + &executor, + &mut tracking_copy, + owner, + Some(nft), + calldata("ownerOf(uint256)", &[word(42)]), + ); + assert_eq!(decode_address(&initial_owner.output), owner); + + execute_call( + &executor, + &mut tracking_copy, + owner, + Some(nft), + calldata( + "approve(address,uint256)", + &[address_word(approved), word(42)], + ), + ); + execute_call( + &executor, + &mut tracking_copy, + approved, + Some(nft), + calldata( + "transferFrom(address,address,uint256)", + &[address_word(owner), address_word(recipient), word(42)], + ), + ); + let final_owner = execute_call( + &executor, + &mut tracking_copy, + owner, + Some(nft), + calldata("ownerOf(uint256)", &[word(42)]), + ); + assert_eq!(decode_address(&final_owner.output), recipient); +} + +#[test] +fn storage_zeroes_are_pruned() { + let executor = executor(evm::EvmSpec::Prague); + let from = evm::Address::new([1; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let contract = deploy(&executor, &mut tracking_copy, from, "StorageDelete"); + + execute_call( + &executor, + &mut tracking_copy, + from, + Some(contract), + calldata("set(uint256)", &[word(123)]), + ); + assert_eq!( + read_storage(&mut tracking_copy, contract, 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_cleanup_follows_selected_fork() { + let from = evm::Address::new([1; 20]); + let beneficiary = evm::Address::new([2; 20]); + + let shanghai_executor = executor(evm::EvmSpec::Shanghai); + let (mut shanghai_tracking_copy, _shanghai_tempdir) = tracking_copy(); + let shanghai_contract = deploy( + &shanghai_executor, + &mut shanghai_tracking_copy, + from, + "SelfDestruct", + ); + assert_eq!( + read_storage( + &mut shanghai_tracking_copy, + shanghai_contract, + CasperU256::zero() + ), + Some(storage_word(7)) + ); + execute_call( + &shanghai_executor, + &mut shanghai_tracking_copy, + from, + Some(shanghai_contract), + calldata("destroy(address)", &[address_word(beneficiary)]), + ); + assert_eq!( + shanghai_tracking_copy + .read(&Key::Evm(evm::EvmAddr::Account(shanghai_contract))) + .unwrap(), + None + ); + assert_eq!( + shanghai_tracking_copy + .read(&Key::Balance( + evm::deterministic_purse(shanghai_contract).addr() + )) + .unwrap(), + None + ); + assert_eq!( + read_storage( + &mut shanghai_tracking_copy, + shanghai_contract, + CasperU256::zero() + ), + None + ); + + let prague_executor = executor(evm::EvmSpec::Prague); + let (mut prague_tracking_copy, _prague_tempdir) = tracking_copy(); + let prague_contract = deploy( + &prague_executor, + &mut prague_tracking_copy, + from, + "SelfDestruct", + ); + execute_call( + &prague_executor, + &mut prague_tracking_copy, + from, + Some(prague_contract), + calldata("destroy(address)", &[address_word(beneficiary)]), + ); + assert!(prague_tracking_copy + .read(&Key::Evm(evm::EvmAddr::Account(prague_contract))) + .unwrap() + .is_some()); +} + +#[test] +fn signed_transactions_require_configured_chain_id() { + let executor = executor(evm::EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let missing_chain_id = legacy_transaction_without_chain_id(); + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(missing_chain_id), + }; + assert!(matches!( + executor.execute(&mut tracking_copy, request), + Err(Error::MissingChainId) + )); + + let wrong_chain_executor = EvmExecutor::new(evm::EvmConfig { + enabled: true, + chain_id: 8, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }); + let transaction = legacy_transaction(Some(7)); + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(transaction), + }; + assert!(matches!( + wrong_chain_executor.execute(&mut tracking_copy, request), + Err(Error::ChainIdMismatch { + expected: 8, + actual: 7 + }) + )); +} + +#[test] +fn signed_transaction_sender_uses_linked_casper_account_identity() { + let executor = executor(evm::EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let transaction = legacy_transaction(Some(7)); + let signer = transaction + .signer() + .expect("transaction should have a signer"); + let account_hash = signer.to_account_hash(); + let main_purse = URef::new([9; 32], AccessRights::READ_ADD_WRITE); + let initial_balance = U512::from(1_000_000u64); + + tracking_copy.write( + Key::Account(account_hash), + StoredValue::Account(Account::create(account_hash, NamedKeys::new(), main_purse)), + ); + tracking_copy.write( + Key::Balance(main_purse.addr()), + StoredValue::CLValue(CLValue::from_t(initial_balance).unwrap()), + ); + tracking_copy.write( + Key::Evm(evm::EvmAddr::Account(transaction.from())), + StoredValue::CLValue(CLValue::from_t(Key::Account(account_hash)).unwrap()), + ); + + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(transaction.clone()), + }; + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("EVM execution should succeed"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(read_evm_nonce(&mut tracking_copy, transaction.from()), 1); + assert_eq!( + read_balance(&mut tracking_copy, transaction.from()), + initial_balance + ); + match tracking_copy + .read(&Key::Evm(evm::EvmAddr::Account(transaction.from()))) + .expect("identity read should not fail") + { + Some(StoredValue::CLValue(value)) => { + assert_eq!(value.into_t::().unwrap(), Key::Account(account_hash)); + } + other => panic!("unexpected EVM identity value: {other:?}"), + } +} + +#[test] +fn signed_transaction_sender_keeps_evm_native_identity() { + let executor = executor(evm::EvmSpec::Prague); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let transaction = legacy_transaction(Some(7)); + let signer = transaction + .signer() + .expect("transaction should have a signer"); + let account_hash = signer.to_account_hash(); + let initial_balance = U512::from(1_000_000u64); + + seed_evm_balance(&mut tracking_copy, transaction.from(), initial_balance); + + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(transaction.clone()), + }; + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("EVM execution should succeed"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(read_evm_nonce(&mut tracking_copy, transaction.from()), 1); + assert_eq!( + read_balance(&mut tracking_copy, transaction.from()), + initial_balance + ); + assert_eq!( + tracking_copy + .read(&Key::Account(account_hash)) + .expect("account read should not fail"), + None + ); + match tracking_copy + .read(&Key::Evm(evm::EvmAddr::Account(transaction.from()))) + .expect("identity read should not fail") + { + Some(StoredValue::CLValue(value)) => { + assert_eq!( + value.into_t::().unwrap(), + Key::URef(evm::deterministic_purse(transaction.from())) + ); + } + other => panic!("unexpected EVM identity value: {other:?}"), + } +} + +#[test] +fn checked_calls_enforce_transaction_validation() { + let executor = executor(evm::EvmSpec::Prague); + 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..ec3daa6204 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::evm::Transaction, +) -> Result { + let address = transaction.from(); + // The signer gives us a Casper `AccountHash` preimage from the secp256k1 + // public key. That account hash is not derivable from the 20-byte EVM + // address alone, so identity linking must happen while the signed + // transaction is available. + let signer = transaction + .signer() + .map_err(|error| BlockExecutionError::TransactionConversion(error.to_string()))?; + let account_hash = signer.to_account_hash(); + // Native EVM identities use a deterministic purse derived from the EVM + // address. Linked Casper accounts use the account's existing main purse + // instead, so the same key pair can spend the same funds from Casper and + // Ethereum-style transaction paths. + let deterministic_purse = casper_types::evm::deterministic_purse(address); + let mut tracking_copy = scratch_state + .tracking_copy(state_root_hash)? + .ok_or(BlockExecutionError::RootNotFound(state_root_hash))?; + + // `EvmAddr::Account` is now only an identity pointer. It is either + // `Key::Account` for a linked Casper account or `Key::URef` for an + // EVM-native purse identity. + let identity_key = Key::Evm(casper_types::evm::EvmAddr::Account(address)); + match tracking_copy + .read(&identity_key) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))? + { + Some(StoredValue::CLValue(cl_value)) => { + let key = cl_value + .into_t::() + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + match key { + // Existing bridge records are authoritative. Once an EVM + // address is linked, the payer is the linked Casper account's + // main purse. + Key::Account(account_hash) => Ok(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::evm::EvmAddr::CodeHash(address)); + match tracking_copy + .read(&key) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))? + { + Some(StoredValue::CLValue(cl_value)) => { + let code_hash = cl_value + .into_t::() + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + Ok(code_hash != casper_types::evm::EMPTY_CODE_HASH) + } + Some(stored_value) => Err(BlockExecutionError::PaymentError(format!( + "unexpected stored value for {key}: expected StoredValue::CLValue(evm::Hash), found {}", + stored_value.type_name() + ))), + None => Ok(false), + } +} + +fn account_main_purse( + tracking_copy: &mut TrackingCopy, + protocol_version: ProtocolVersion, + account_hash: AccountHash, +) -> Result, BlockExecutionError> +where + R: StateReader, +{ + // Runtime footprints cover both legacy `StoredValue::Account` accounts and + // addressable-entity-backed accounts, so this is the authoritative account + // existence check for identity linking. + match tracking_copy.runtime_footprint_by_account_hash(protocol_version, account_hash) { + Ok((_, entity)) => entity + .main_purse() + .map(Some) + .ok_or_else(|| BlockExecutionError::PaymentError("missing account main purse".into())), + Err(TrackingCopyError::KeyNotFound(_)) => Ok(None), + Err(error) => Err(BlockExecutionError::PaymentError(error.to_string())), + } +} + +fn apply_evm_identity_plan( + tracking_copy: &mut TrackingCopy, + protocol_version: ProtocolVersion, + plan: EvmIdentityPlan, +) -> Result<(), BlockExecutionError> +where + R: StateReader, +{ + // Identity writes are intentionally delayed until after payment + // preconditions pass. They are applied to the same tracking copy as EVM + // execution so the identity record and nonce/code/storage updates commit or + // discard together. + match plan { + EvmIdentityPlan::None => Ok(()), + EvmIdentityPlan::LinkExisting { + address, + account_hash, + } => write_evm_identity(tracking_copy, address, Key::Account(account_hash)), + EvmIdentityPlan::CreateAccount { + address, + account_hash, + main_purse, + } => { + // Another transaction in the same block may have already created + // the account through this scratch state. Avoid recreating it, but + // still write the EVM identity pointer below. + if account_main_purse(tracking_copy, protocol_version, account_hash)?.is_none() { + let account = Account::create(account_hash, NamedKeys::new(), main_purse); + tracking_copy + .create_addressable_entity_from_account(account, protocol_version) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + } + write_evm_identity(tracking_copy, address, Key::Account(account_hash)) + } + } +} + +fn write_evm_identity( + tracking_copy: &mut TrackingCopy, + address: EvmAddress, + identity: Key, +) -> Result<(), BlockExecutionError> +where + R: StateReader, +{ + // Keep the bridge record minimal: a CLValue containing the identity `Key`. + // Nonce, code hash, bytecode, and storage live under their own EVM keys. + let key = Key::Evm(casper_types::evm::EvmAddr::Account(address)); + let cl_value = CLValue::from_t(identity) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + tracking_copy.write(key, StoredValue::CLValue(cl_value)); + Ok(()) +} + /// Executes a finalized block. #[allow(clippy::too_many_arguments)] pub fn execute_finalized_block( @@ -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,21 @@ 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() + .ok_or(BlockExecutionError::InvalidTransactionVariant)?; + RuntimeOrigin::from_initiator_addr(initiator_addr) + }; + let payer_balance_identifier = runtime_origin.payer_balance_identifier(); + let refund_purse_active = is_custom_payment; if refund_purse_active { // if custom payment before doing any processing, initialize the initiator's main purse @@ -310,7 +742,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 +764,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 +777,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 +794,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 +814,26 @@ pub fn execute_finalized_block( Ok(None) => { // the initiating account pays using its main purse trace!(%transaction_hash, "direct invocation with account payment"); - initiator_addr.clone().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 +877,11 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys.clone(), BalanceIdentifierTransferArgs::new( None, - initiator_addr.clone().into(), + payer_balance_identifier.clone(), BalanceIdentifier::Payment, baseline_motes_amount, None, @@ -473,7 +922,7 @@ pub fn execute_finalized_block( BalanceIdentifier::Payment } } else { - BalanceIdentifier::PenalizedAccount(initiator_addr.clone().account_hash()) + BalanceIdentifier::PenalizedAccount(runtime_origin.account_hash()?) } }; @@ -486,8 +935,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 +943,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 +1002,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 +1014,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys, runtime_args.clone(), )); @@ -571,7 +1030,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys, runtime_args.clone(), )); @@ -589,9 +1048,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 +1060,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + runtime_origin.initiator_addr()?, authorization_keys, auction_method, )); @@ -622,6 +1083,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 +1236,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 +1285,7 @@ pub fn execute_finalized_block( // placing a hold on the correct purse. balance_identifier = BalanceIdentifier::Refund; Some(HandleRefundMode::RefundNoFeeCustomPayment { - initiator_addr: Box::new(initiator_addr.clone()), + initiator_addr: Box::new(runtime_origin.initiator_addr()?), limit: artifact_builder.limit(), gas_price: current_gas_price, cost: artifact_builder.cost_to_use(), @@ -760,15 +1294,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 +1326,7 @@ pub fn execute_finalized_block( // logic, which is interpreted by inner logic to use the currently set // refund purse. Some(HandleRefundMode::Refund { - initiator_addr: Box::new(initiator_addr.clone()), + initiator_addr: Box::new(runtime_origin.initiator_addr()?), limit: artifact_builder.limit(), gas_price: current_gas_price, consumed, @@ -805,9 +1346,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 +1437,7 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleFeeMode::pay( - Box::new(initiator_addr.clone()), + runtime_origin.fee_initiator(), balance_identifier, BalanceIdentifier::Public(*(proposer.clone())), fee_amount, @@ -908,7 +1454,7 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleFeeMode::pay( - Box::new(initiator_addr.clone()), + runtime_origin.fee_initiator(), balance_identifier, BalanceIdentifier::Accumulate, fee_amount, @@ -1338,6 +1884,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 +1911,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(); @@ -1393,7 +1941,9 @@ where *state_root_hash, protocol_version, transaction_hash, - initiator_addr.clone(), + initiator_addr + .cloned() + .expect("native speculative execution requires a Casper initiator"), authorization_keys, runtime_args, )); @@ -1425,6 +1975,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 +2002,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 +2018,110 @@ where } } +fn speculatively_execute_evm( + state_provider: &S, + chainspec: &Chainspec, + block_header: BlockHeader, + block_hashes: BTreeMap, + evm_transaction: &casper_types::evm::Transaction, +) -> SpeculativeExecutionResult +where + S: StateProvider, +{ + if !chainspec.evm_config.enabled { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Disabled, + )); + } + if evm_transaction.gas_limit() > chainspec.evm_config.block_gas_limit { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit: evm_transaction.gas_limit(), + block_gas_limit: chainspec.evm_config.block_gas_limit, + }, + )); + } + + let state_root_hash = block_header.state_root_hash(); + let mut tracking_copy = match state_provider.tracking_copy(*state_root_hash) { + Ok(Some(tracking_copy)) => tracking_copy, + Ok(None) => { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Decode(format!( + "state root {state_root_hash} not found" + )), + )) + } + Err(error) => { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::evm::TransactionError::Decode(format!( + "failed to check out EVM speculative execution state: {error}" + )), + )) + } + }; + let block_time = block_header + .timestamp() + .saturating_add(chainspec.core_config.minimum_block_time); + 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::evm::TransactionError::Decode(error.to_string()), + )) + } + }; + let effects = tracking_copy.effects(); + let effective_gas_price = if evm_transaction.is_unsigned_call() { + u128::from(chainspec.evm_config.base_fee) + } else { + evm_transaction.effective_gas_price(chainspec.evm_config.base_fee) + }; + let receipt = outcome.to_receipt(effective_gas_price); + let error = receipt.status.message().map(str::to_string); + SpeculativeExecutionResult::Evm(Box::new( + casper_binary_port::EvmSpeculativeExecutionResult::new( + block_header.block_hash(), + Gas::new(evm_transaction.gas_limit()), + Gas::new(outcome.gas_used), + effects, + error, + receipt, + Bytes::from(outcome.output), + ), + )) +} + 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..f50c5cb7f7 100644 --- a/node/src/components/contract_runtime/operations/wasm_v2_request.rs +++ b/node/src/components/contract_runtime/operations/wasm_v2_request.rs @@ -101,7 +101,9 @@ impl WasmV2Request { transaction: &MetaTransaction, ) -> Result { let transaction_hash = transaction.hash(); - let initiator_addr = transaction.initiator_addr(); + let initiator_addr = transaction + .initiator_addr() + .expect("Wasm v2 transaction requires a Casper initiator"); let gas_limit: u64 = gas_limit .value() @@ -213,8 +215,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 +236,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..17e93256e7 100644 --- a/node/src/components/contract_runtime/types.rs +++ b/node/src/components/contract_runtime/types.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, sync::Arc}; use crate::{contract_runtime::StateResultError, types::TransactionHeader}; -use casper_types::{InitiatorAddr, Transfer}; +use casper_types::{evm, InitiatorAddr, Transfer}; use datasize::DataSize; use serde::Serialize; @@ -17,7 +17,7 @@ use casper_storage::{ }; use casper_types::{ contract_messages::Messages, - execution::{Effects, ExecutionResult, ExecutionResultV2}, + execution::{Effects, EvmExecutionResult, ExecutionResult, ExecutionResultV2}, BlockHash, BlockHeaderV2, BlockV2, Digest, EraId, Gas, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, ProtocolVersion, PublicKey, Transaction, TransactionHash, U512, }; @@ -74,7 +74,8 @@ pub(crate) struct ExecutionArtifactBuilder { error_message: Option, messages: Messages, transfers: Vec, - initiator: InitiatorAddr, + initiator: Option, + evm_initiator: Option, current_price: u8, cost: U512, limit: Gas, @@ -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, @@ -456,7 +480,7 @@ impl ExecutionArtifactBuilder { #[allow(unused)] pub fn with_initiator_addr(&mut self, initiator_addr: InitiatorAddr) -> &mut Self { - self.initiator = initiator_addr; + self.initiator = Some(initiator_addr); self } @@ -467,19 +491,37 @@ 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 + .expect("Wasm execution result requires a Casper 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 +608,7 @@ pub struct BlockAndExecutionArtifacts { pub enum SpeculativeExecutionResult { InvalidTransaction(InvalidTransaction), WasmV1(Box), + Evm(Box), } impl SpeculativeExecutionResult { @@ -577,6 +620,11 @@ impl SpeculativeExecutionResult { Transaction::V1(_) => SpeculativeExecutionResult::InvalidTransaction( InvalidTransaction::V1(InvalidTransactionV1::UnableToCalculateGasLimit), ), + Transaction::Evm(_) => SpeculativeExecutionResult::InvalidTransaction( + InvalidTransaction::Evm(evm::TransactionError::Decode( + "EVM transactions are not routed through contract runtime".to_string(), + )), + ), } } diff --git a/node/src/components/contract_runtime/utils.rs b/node/src/components/contract_runtime/utils.rs index b2324d8dfd..a4789cb33b 100644 --- a/node/src/components/contract_runtime/utils.rs +++ b/node/src/components/contract_runtime/utils.rs @@ -1,3 +1,6 @@ +use casper_executor_evm::{ + BlockHashProvider as EvmBlockHashProvider, BlockHashProviderResult, BLOCK_HASH_HISTORY, +}; use casper_executor_wasm::ExecutorV2; use num_rational::Ratio; use once_cell::sync::Lazy; @@ -39,7 +42,9 @@ use casper_storage::{ }, global_state::state::{lmdb::LmdbGlobalState, CommitProvider, StateProvider}, }; -use casper_types::{BlockHash, Chainspec, Digest, EraId, Gas, Key, ProtocolUpgradeConfig}; +use casper_types::{ + BlockHash, Chainspec, Digest, EraId, Gas, Key, ProtocolUpgradeConfig, Transaction, +}; /// Maximum number of resource intensive tasks that can be run in parallel. /// @@ -49,6 +54,47 @@ const MAX_PARALLEL_INTENSIVE_TASKS: usize = 4; static INTENSIVE_TASKS_SEMAPHORE: Lazy = Lazy::new(|| tokio::sync::Semaphore::new(MAX_PARALLEL_INTENSIVE_TASKS)); +#[derive(Clone, Debug, Default)] +struct RecentBlockHashProvider { + block_hashes: BTreeMap, +} + +impl RecentBlockHashProvider { + async fn load(effect_builder: EffectBuilder, current_block_height: u64) -> Self + where + REv: From, + { + let block_hashes = load_recent_evm_block_hashes(effect_builder, current_block_height).await; + RecentBlockHashProvider { block_hashes } + } +} + +impl EvmBlockHashProvider for RecentBlockHashProvider { + fn block_hash(&self, block_height: u64) -> BlockHashProviderResult> { + Ok(self.block_hashes.get(&block_height).copied()) + } +} + +pub(crate) async fn load_recent_evm_block_hashes( + effect_builder: EffectBuilder, + current_block_height: u64, +) -> BTreeMap +where + REv: From, +{ + let earliest_block_height = current_block_height.saturating_sub(BLOCK_HASH_HISTORY); + let mut block_hashes = BTreeMap::new(); + for block_height in earliest_block_height..current_block_height { + if let Some(header) = effect_builder + .get_block_header_at_height_from_storage(block_height, true) + .await + { + block_hashes.insert(block_height, header.block_hash()); + } + } + block_hashes +} + /// Asynchronously runs a resource intensive task. /// At most `MAX_PARALLEL_INTENSIVE_TASKS` are being run in parallel at any time. /// @@ -274,6 +320,15 @@ pub(super) async fn exec_and_check_next( }; let current_gas_price = executable_block.current_gas_price; + let evm_block_hash_provider = if executable_block + .transactions + .iter() + .any(|transaction| matches!(transaction, Transaction::Evm(_))) + { + RecentBlockHashProvider::load(effect_builder, executable_block.height).await + } else { + RecentBlockHashProvider::default() + }; let contract_runtime_metrics = metrics.clone(); let task = move || { debug!("ContractRuntime: execute_finalized_block"); @@ -284,6 +339,7 @@ pub(super) async fn exec_and_check_next( chainspec.as_ref(), Some(contract_runtime_metrics), current_pre_state, + &evm_block_hash_provider, executable_block, key_block_height_for_activation_point, current_gas_price, diff --git a/node/src/components/event_stream_server.rs b/node/src/components/event_stream_server.rs index e8d9820e3c..d93ffc47ff 100644 --- a/node/src/components/event_stream_server.rs +++ b/node/src/components/event_stream_server.rs @@ -299,21 +299,33 @@ where execution_result, messages, } => { - let (initiator_addr, timestamp, ttl) = match *transaction_header { - TransactionHeader::Deploy(deploy_header) => ( - InitiatorAddr::PublicKey(deploy_header.account().clone()), - deploy_header.timestamp(), - deploy_header.ttl(), - ), - TransactionHeader::V1(metadata) => ( - metadata.initiator_addr().clone(), - metadata.timestamp(), - metadata.ttl(), - ), - }; + let (initiator_addr, evm_initiator_addr, timestamp, ttl) = + match *transaction_header { + TransactionHeader::Deploy(deploy_header) => ( + Some(Box::new(InitiatorAddr::PublicKey( + deploy_header.account().clone(), + ))), + None, + deploy_header.timestamp(), + deploy_header.ttl(), + ), + TransactionHeader::V1(metadata) => ( + Some(Box::new(metadata.initiator_addr().clone())), + None, + metadata.timestamp(), + metadata.ttl(), + ), + TransactionHeader::Evm(metadata) => ( + None, + Some(Box::new(metadata.initiator_addr())), + metadata.timestamp(), + metadata.ttl(), + ), + }; self.broadcast(SseData::TransactionProcessed { transaction_hash: Box::new(transaction_hash), - initiator_addr: Box::new(initiator_addr), + initiator_addr, + evm_initiator_addr, timestamp, ttl, block_hash: Box::new(block_hash), diff --git a/node/src/components/event_stream_server/sse_server.rs b/node/src/components/event_stream_server/sse_server.rs index 051d0fd762..caa6ca186f 100644 --- a/node/src/components/event_stream_server/sse_server.rs +++ b/node/src/components/event_stream_server/sse_server.rs @@ -34,6 +34,7 @@ use warp::{ use casper_types::{ contract_messages::Messages, + evm, execution::{Effects, ExecutionResult}, Block, BlockHash, EraId, FinalitySignature, InitiatorAddr, ProtocolVersion, PublicKey, TimeDiff, Timestamp, Transaction, TransactionHash, @@ -71,7 +72,8 @@ pub enum SseData { /// The given transaction has been executed, committed and forms part of the given block. TransactionProcessed { transaction_hash: Box, - initiator_addr: Box, + initiator_addr: Option>, + evm_initiator_addr: Option>, timestamp: Timestamp, ttl: TimeDiff, block_hash: Box, @@ -124,15 +126,24 @@ 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, evm_initiator_addr) = match &txn { + Transaction::Deploy(_) | Transaction::V1(_) => { + (txn.initiator_addr().map(Box::new), None) + } + Transaction::Evm(evm_transaction) => (None, Some(Box::new(evm_transaction.from()))), + }; + SseData::TransactionProcessed { transaction_hash: Box::new(txn.hash()), - initiator_addr: Box::new(txn.initiator_addr()), + initiator_addr, + evm_initiator_addr, timestamp, ttl, block_hash: Box::new(BlockHash::random(rng)), diff --git a/node/src/components/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..41e496ab9d 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -13,13 +13,15 @@ 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, + account::AccountHash, addressable_entity::AddressableEntity, evm, system::auction::ARG_AMOUNT, + AddressableEntityHash, AddressableEntityIdentifier, BlockHeader, CLType, Chainspec, EntityAddr, EntityKind, EntityVersion, EntityVersionKey, ExecutableDeployItem, - ExecutableDeployItemIdentifier, InitiatorAddr, Package, PackageAddr, PackageHash, - PackageIdentifier, Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, + ExecutableDeployItemIdentifier, Key, Package, PackageAddr, PackageHash, PackageIdentifier, + StoredValue, Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, TransactionTarget, DEFAULT_ENTRY_POINT_NAME, U512, }; @@ -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( + evm::TransactionError::MissingApproval, + )), + ); + } + // We only perform expiry checks on transactions received from the client. let current_node_timestamp = event_metadata.verification_start_timestamp; if event_metadata.source.is_client() @@ -211,11 +293,31 @@ impl TransactionAcceptor { } }; + if let Some(evm_transaction) = event_metadata.meta_transaction.as_evm() { + // EVM senders are validated from the EVM identity record first. A + // Casper account lookup would be wrong here because an EVM address + // may be either linked to a Casper account or backed by an + // EVM-native purse. + let query_request = QueryRequest::new( + *block_header.state_root_hash(), + Key::Evm(evm::EvmAddr::Account(evm_transaction.from())), + 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() + .expect("non-EVM transaction initiator must be a Casper account"); + let account_hash = initiator_addr.account_hash(); let entity_addr = EntityAddr::Account(account_hash.value()); effect_builder .get_addressable_entity(*block_header.state_root_hash(), entity_addr) @@ -229,6 +331,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( + evm::TransactionError::Decode(error_message), + )); + self.reject_transaction(effect_builder, *event_metadata, error) + } + EvmAccountLookup::Missing => { + // A missing identity pointer does not necessarily mean all EVM + // metadata is missing. Runtime still checks split nonce and + // code-hash records before deciding whether the address can be + // linked to a Casper account or must remain EVM-native. + let query_request = QueryRequest::new( + *block_header.state_root_hash(), + Key::Evm(evm::EvmAddr::Nonce(evm_transaction.from())), + vec![], + ); + effect_builder + .query_global_state(query_request) + .event( + move |query_result| Event::GetMissingEvmIdentityNonceResult { + event_metadata, + block_header, + nonce: evm_nonce_from_query_result(query_result), + }, + ) + } + } + } + + fn handle_get_missing_evm_identity_nonce_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + nonce: EvmNonceLookup, + ) -> Effects { + let evm_transaction = event_metadata + .meta_transaction + .as_evm() + .expect("missing EVM identity nonce lookup should only be used for EVM transactions"); + let expected_nonce = match nonce { + EvmNonceLookup::Value(nonce) => nonce, + EvmNonceLookup::Missing => 0, + EvmNonceLookup::Invalid(error_message) => { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::Decode(error_message), + )), + ); + } + }; + let query_request = QueryRequest::new( + *block_header.state_root_hash(), + Key::Evm(evm::EvmAddr::CodeHash(evm_transaction.from())), + vec![], + ); + effect_builder + .query_global_state(query_request) + .event( + move |query_result| Event::GetMissingEvmIdentityCodeHashResult { + event_metadata, + block_header, + expected_nonce, + code_hash: evm_code_hash_from_query_result(query_result), + }, + ) + } + + fn handle_get_missing_evm_identity_code_hash_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + expected_nonce: u64, + code_hash: EvmCodeHashLookup, + ) -> Effects { + let evm_transaction = event_metadata.meta_transaction.as_evm().expect( + "missing EVM identity code-hash lookup should only be used for EVM transactions", + ); + let address = evm_transaction.from(); + let code_hash = match code_hash { + EvmCodeHashLookup::Value(code_hash) => code_hash, + EvmCodeHashLookup::Missing => evm::EMPTY_CODE_HASH, + EvmCodeHashLookup::Invalid(error_message) => { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::Decode(error_message), + )), + ); + } + }; + + 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(evm::EvmAddr::Nonce(evm_transaction.from())), + vec![], + ); + effect_builder + .query_global_state(query_request) + .event(move |query_result| Event::GetEvmNonceResult { + event_metadata, + block_header, + balance_source, + nonce: evm_nonce_from_query_result(query_result), + }) + } + + fn handle_get_evm_nonce_result( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + balance_source: EvmBalanceSource, + nonce: EvmNonceLookup, + ) -> Effects { + let expected_nonce = match nonce { + EvmNonceLookup::Value(nonce) => nonce, + EvmNonceLookup::Missing => 0, + EvmNonceLookup::Invalid(error_message) => { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::Decode(error_message), + )), + ); + } + }; + self.validate_evm_nonce_and_balance( + effect_builder, + event_metadata, + block_header, + expected_nonce, + balance_source, + ) + } + + fn validate_evm_nonce_and_balance( + &self, + effect_builder: EffectBuilder, + event_metadata: Box, + block_header: Box, + expected: u64, + balance_source: EvmBalanceSource, + ) -> Effects { + let evm_transaction = event_metadata + .meta_transaction + .as_evm() + .expect("EVM account validation should only be used for EVM transactions"); + let actual = evm_transaction.nonce(); + // EVM nonce validation is independent of the identity pointer. Linking + // an EVM address to a Casper account does not change the EVM replay + // counter. + if actual != expected { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::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, @@ -238,7 +642,10 @@ impl TransactionAcceptor { ) -> Effects { match maybe_entity { None => { - let initiator_addr = event_metadata.transaction.initiator_addr(); + let initiator_addr = event_metadata + .transaction + .initiator_addr() + .expect("missing entity check requires a Casper initiator"); let error = Error::parameter_failure( &block_header, ParameterFailure::NoSuchAddressableEntity { initiator_addr }, @@ -291,7 +698,10 @@ impl TransactionAcceptor { } match maybe_balance { None => { - let initiator_addr = event_metadata.transaction.initiator_addr(); + let initiator_addr = event_metadata + .transaction + .initiator_addr() + .expect("balance check requires a Casper initiator"); let error = Error::parameter_failure( &block_header, ParameterFailure::UnknownBalance { initiator_addr }, @@ -302,7 +712,10 @@ impl TransactionAcceptor { let has_minimum_balance = balance >= self.chainspec.core_config.baseline_motes_amount_u512(); if !has_minimum_balance { - let initiator_addr = event_metadata.transaction.initiator_addr(); + let initiator_addr = event_metadata + .transaction + .initiator_addr() + .expect("balance check requires a Casper initiator"); let error = Error::parameter_failure( &block_header, ParameterFailure::InsufficientBalance { initiator_addr }, @@ -405,6 +818,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 +835,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 +857,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 +979,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 +1078,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 +1293,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 +1484,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..3c2a3eb6a4 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,6 +41,7 @@ use casper_types::{ contracts::{ ContractHash, ContractPackage, ContractPackageStatus, ContractVersionKey, NamedKeys, }, + evm, global_state::TrieMerkleProof, testing::TestRng, Block, BlockV2, CLValue, Chainspec, ChainspecRawBytes, Contract, Deploy, EraId, Groups, @@ -69,6 +77,8 @@ use crate::{ const POLL_INTERVAL: Duration = Duration::from_millis(10); const TIMEOUT: Duration = Duration::from_secs(30); +const EVM_TEST_CHAIN_ID: u64 = 1_129_533_695; +const EVM_TEST_GAS_PRICE: u128 = 1_000_000; /// Top-level event for the reactor. #[derive(Debug, From, Serialize)] @@ -194,6 +204,7 @@ enum TxnType { #[derive(Clone, PartialEq, Eq, Debug)] enum TestScenario { + FromPeerEvmInvalidNonce, FromPeerInvalidTransaction(TxnType), FromPeerInvalidTransactionZeroPayment(TxnType), FromPeerExpired(TxnType), @@ -207,6 +218,8 @@ enum TestScenario { FromPeerSessionContract(TxnType, ContractScenario), FromPeerSessionContractPackage(TxnType, ContractPackageScenario), FromClientInvalidTransaction(TxnType), + FromClientEvmInvalidNonce, + FromClientEvmMissingIdentityWithCodeHash, FromClientInvalidTransactionZeroPayment(TxnType), FromClientSlightlyFutureDatedTransaction(TxnType), FromClientFutureDatedTransaction(TxnType), @@ -256,6 +269,7 @@ impl TestScenario { fn source(&self, rng: &mut NodeRng) -> Source { match self { TestScenario::FromPeerInvalidTransaction(_) + | TestScenario::FromPeerEvmInvalidNonce | TestScenario::FromPeerInvalidTransactionZeroPayment(_) | TestScenario::FromPeerExpired(_) | TestScenario::FromPeerValidTransaction(_) @@ -270,6 +284,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 +340,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 +895,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 +987,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) -> evm::Transaction { + let recipient = evm::Address::new([1; evm::ADDRESS_LENGTH]); + let transaction = TxLegacy { + chain_id: Some(EVM_TEST_CHAIN_ID), + nonce, + gas_price: EVM_TEST_GAS_PRICE, + gas_limit: 21_000, + to: TxKind::Call(AlloyAddress::from(recipient.value())), + value: AlloyU256::ZERO, + input: AlloyBytes::new(), + }; + let signing_key = + SigningKey::from_slice(&[0x11; 32]).expect("test EVM private key should be valid"); + let (signature, recovery_id) = signing_key + .sign_prehash(transaction.signature_hash().as_ref()) + .expect("test EVM transaction signing should succeed"); + let signed = transaction.into_signed(AlloySignature::from((signature, recovery_id))); + let envelope = TxEnvelope::from(signed); + evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::now(), + TimeDiff::from_seconds(300), + ) + .expect("test EVM transaction should decode") } fn create_account(account_hash: AccountHash, test_scenario: &TestScenario) -> Account { @@ -1036,9 +1095,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(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 +1181,36 @@ impl reactor::Reactor for Reactor { self.test_scenario ), } + } else if let Key::Evm(evm::EvmAddr::Nonce(_)) = query_request.key() { + let nonce = if matches!( + self.test_scenario, + TestScenario::FromClientEvmMissingIdentityWithCodeHash + ) { + 1u64 + } else { + 0u64 + }; + QueryResult::Success { + value: Box::new(StoredValue::CLValue(CLValue::from_t(nonce).unwrap())), + proofs: vec![], + } + } else if let Key::Evm(evm::EvmAddr::CodeHash(_)) = query_request.key() { + let code_hash = if matches!( + self.test_scenario, + TestScenario::FromClientEvmMissingIdentityWithCodeHash + ) { + evm::Hash::new([0x11; evm::HASH_LENGTH]) + } else { + evm::EMPTY_CODE_HASH + }; + QueryResult::Success { + value: Box::new(StoredValue::CLValue( + CLValue::from_t(code_hash).unwrap(), + )), + proofs: vec![], + } } else { - panic!("expect only queries using Key::Package variant"); + panic!("unexpected query: {query_request:?}"); }; responder.respond(query_result).ignore() } @@ -1454,6 +1556,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 +1644,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 +1733,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 +1776,7 @@ async fn run_transaction_acceptor_without_timeout( // `AcceptedNewTransaction` announcement with the appropriate source. TestScenario::FromClientValidTransaction(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) + | TestScenario::FromClientEvmMissingIdentityWithCodeHash | TestScenario::FromClientSignedByAdmin(_) => { matches!( event, @@ -1840,6 +1949,20 @@ async fn should_reject_invalid_transaction_v1_from_peer() { )) } +#[tokio::test] +async fn should_reject_evm_transaction_with_invalid_nonce_from_peer() { + let result = run_transaction_acceptor(TestScenario::FromPeerEvmInvalidNonce).await; + assert!(matches!( + result, + Err(super::Error::InvalidTransaction(InvalidTransaction::Evm( + evm::TransactionError::InvalidNonce { + expected: 0, + actual: 1 + } + ))) + )) +} + #[tokio::test] async fn should_reject_zero_payment_transaction_v1_from_peer() { let result = run_transaction_acceptor(TestScenario::FromPeerInvalidTransactionZeroPayment( @@ -1940,6 +2063,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( + evm::TransactionError::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..49492cfe1b 100644 --- a/node/src/reactor/main_reactor/tests/configs_override.rs +++ b/node/src/reactor/main_reactor/tests/configs_override.rs @@ -3,8 +3,8 @@ use std::collections::BTreeSet; use num_rational::Ratio; use casper_types::{ - ConsensusProtocolName, FeeHandling, HoldBalanceHandling, PricingHandling, PublicKey, - RefundHandling, TimeDiff, TransactionV1Config, + evm::EvmConfig, ConsensusProtocolName, FeeHandling, HoldBalanceHandling, PricingHandling, + PublicKey, RefundHandling, TimeDiff, TransactionV1Config, }; use crate::types::SyncHandling; @@ -36,6 +36,7 @@ pub(crate) struct ConfigsOverride { pub chain_name: Option, pub gas_hold_balance_handling: Option, pub transaction_v1_override: Option, + pub evm_config_override: Option, pub node_config_override: NodeConfigOverride, pub minimum_delegation_rate: u8, } @@ -128,6 +129,11 @@ impl ConfigsOverride { self } + pub(crate) fn with_evm_config(mut self, evm_config: EvmConfig) -> Self { + self.evm_config_override = Some(evm_config); + self + } + pub(crate) fn with_idle_tolerance(mut self, idle_tolernace: TimeDiff) -> Self { let config = NodeConfigOverride { idle_tolerance: Some(idle_tolernace), @@ -171,6 +177,7 @@ impl Default for ConfigsOverride { chain_name: None, gas_hold_balance_handling: None, transaction_v1_override: None, + evm_config_override: None, node_config_override: NodeConfigOverride::default(), minimum_delegation_rate: 0, } diff --git a/node/src/reactor/main_reactor/tests/fixture.rs b/node/src/reactor/main_reactor/tests/fixture.rs index 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..e215eef22c 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,6 +35,7 @@ use crate::reactor::main_reactor::tests::{ }; use casper_types::{ bytesrepr::{Bytes, ToBytes}, + evm, execution::ExecutionResultV1, }; @@ -342,6 +355,50 @@ async fn transfer_to_account>( ) } +async fn transfer_to_evm_address>( + fixture: &mut TestFixture, + amount: A, + from: &SecretKey, + to: evm::Address, + pricing: PricingMode, + transfer_id: Option, +) -> (TransactionHash, u64, ExecutionResult) { + let chain_name = fixture.chainspec.network_config.name.clone(); + + let mut txn = Transaction::from( + TransactionV1Builder::new_transfer(amount, None, to, transfer_id) + .unwrap() + .with_initiator_addr(PublicKey::from(from)) + .with_pricing_mode(pricing) + .with_chain_name(chain_name) + .build() + .unwrap(), + ); + + txn.sign(from); + let txn_hash = txn.hash(); + + fixture.inject_transaction(txn).await; + fixture + .run_until_executed_transaction(&txn_hash, TEN_SECS) + .await; + + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let exec_info = runner + .main_reactor() + .storage() + .read_execution_info(txn_hash) + .expect("Expected transaction to be included in a block."); + + ( + txn_hash, + exec_info.block_height, + exec_info + .execution_result + .expect("Exec result should have been stored."), + ) +} + async fn send_add_bid>( fixture: &mut TestFixture, amount: A, @@ -759,11 +816,714 @@ 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) -> evm::Transaction { + let transaction = TxLegacy { + chain_id: Some(chain_id), + nonce: 0, + gas_price: EVM_TEST_GAS_PRICE, + gas_limit: EVM_TEST_GAS_LIMIT, + to: TxKind::Create, + value: U256::ZERO, + input: AlloyBytes::from(evm_log_emitting_init_code()), + }; + signed_evm_legacy_transaction(transaction) +} + +fn signed_evm_value_transfer_transaction( + chain_id: u64, + recipient: evm::Address, + gas_limit: u64, + value: u64, +) -> evm::Transaction { + let transaction = TxLegacy { + chain_id: Some(chain_id), + nonce: 0, + gas_price: EVM_TEST_GAS_PRICE, + gas_limit, + to: TxKind::Call(AlloyAddress::from(recipient.value())), + value: U256::from(value), + input: AlloyBytes::new(), + }; + signed_evm_legacy_transaction(transaction) +} + +fn signed_evm_legacy_transaction(transaction: TxLegacy) -> evm::Transaction { + let signing_key = + SigningKey::from_slice(&[0x11; 32]).expect("test EVM private key should be valid"); + let (signature, recovery_id) = signing_key + .sign_prehash(transaction.signature_hash().as_ref()) + .expect("test EVM transaction signing should succeed"); + let signed = transaction.into_signed(AlloySignature::from((signature, recovery_id))); + let envelope = TxEnvelope::from(signed); + evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::now(), + TimeDiff::from_seconds(60), + ) + .expect("test EVM transaction should decode") +} + +fn seed_evm_account(fixture: &mut TestFixture, address: evm::Address, balance: U512) { + let main_purse = evm::deterministic_purse(address); + let values_to_write = vec![ + ( + Key::Evm(evm::EvmAddr::Account(address)), + StoredValue::CLValue(CLValue::from_t(Key::URef(main_purse)).unwrap()), + ), + ( + Key::Evm(evm::EvmAddr::Nonce(address)), + StoredValue::CLValue(CLValue::from_t(0u64).unwrap()), + ), + ( + Key::Evm(evm::EvmAddr::CodeHash(address)), + StoredValue::CLValue(CLValue::from_t(EMPTY_CODE_HASH).unwrap()), + ), + ( + Key::Balance(main_purse.addr()), + 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(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(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(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 = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let evm_transaction = signed_evm_deploy_transaction(evm_config.chain_id); + let sender = evm_transaction.from(); + let expected_sender = alloy_address_to_evm_address(AlloyAddress::from_private_key( + &SigningKey::from_slice(&[0x11; 32]).unwrap(), + )); + assert_eq!(sender, expected_sender); + + let highest_block = test.fixture.highest_complete_block(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + let initial_balance = U512::from(EVM_INITIAL_BALANCE); + + let (_txn_hash, block_height, execution_result) = test + .send_transaction(Transaction::from(evm_transaction.clone())) + .await; + let ExecutionResult::Evm(execution_result) = execution_result else { + panic!("expected EVM execution result"); + }; + + assert_eq!(execution_result.initiator, 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 = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_refund_handling(RefundHandling::Refund { + refund_ratio: Ratio::new(1, 1), + }) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let evm_transaction = signed_evm_deploy_transaction(evm_config.chain_id); + let sender = evm_transaction.from(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + let initial_balance = U512::from(EVM_INITIAL_BALANCE); + + let (_txn_hash, block_height, execution_result) = test + .send_transaction(Transaction::from(evm_transaction.clone())) + .await; + let ExecutionResult::Evm(execution_result) = execution_result else { + panic!("expected EVM execution result"); + }; + + let max_fee_amount = U512::from(evm_transaction.gas_limit()) * U512::from(EVM_TEST_GAS_PRICE); + let consumed_fee_amount = + U512::from(execution_result.receipt.gas_used) * U512::from(EVM_TEST_GAS_PRICE); + + assert_eq!(execution_result.receipt.status, evm::ReceiptStatus::Success); + assert_eq!(execution_result.cost, max_fee_amount); + assert_eq!( + execution_result.refund, + max_fee_amount - consumed_fee_amount + ); + + let final_balance = evm_balance(&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 = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let gas_limit = 21_000; + let value = EVM_INITIAL_BALANCE - gas_limit + 1; + let recipient = evm::Address::new([0x22; evm::ADDRESS_LENGTH]); + let evm_transaction = + signed_evm_value_transfer_transaction(evm_config.chain_id, recipient, gas_limit, value); + let sender = evm_transaction.from(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + + let (_txn_hash, block_height, execution_result) = test + .send_transaction(Transaction::from(evm_transaction)) + .await; + let ExecutionResult::Evm(execution_result) = execution_result else { + panic!("expected EVM execution result"); + }; + + assert_eq!( + execution_result.receipt.status, + evm::ReceiptStatus::Halt(evm::HaltReason::Unknown) + ); + assert_eq!(execution_result.receipt.gas_used, 0); + assert_eq!(execution_result.cost, U512::zero()); + assert_eq!(execution_result.refund, U512::zero()); + assert!(execution_result.effects.is_empty()); + + let final_balance = evm_balance(&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(evm::EvmAddr::Account(recipient)) + ) + .is_none()); +} + +#[tokio::test] +async fn should_not_seed_evm_accounts_at_genesis() { + let evm_config = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config().with_evm_config(evm_config); + let alice_secret_key = Arc::new( + SecretKey::secp256k1_from_bytes([0x11; SecretKey::SECP256K1_LENGTH]) + .expect("secp256k1 key should be valid"), + ); + let bob_secret_key = Arc::new( + SecretKey::secp256k1_from_bytes([0x22; SecretKey::SECP256K1_LENGTH]) + .expect("secp256k1 key should be valid"), + ); + let charlie_secret_key = Arc::new( + SecretKey::secp256k1_from_bytes([0x33; SecretKey::SECP256K1_LENGTH]) + .expect("secp256k1 key should be valid"), + ); + let alice_evm_address = evm::Address::from_public_key(&PublicKey::from(&*alice_secret_key)) + .expect("secp256k1 public key should have an EVM address"); + let mut test = SingleTransactionTestCase::new( + alice_secret_key, + bob_secret_key, + charlie_secret_key, + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let (_node_id, runner) = test.fixture.network.nodes().iter().next().unwrap(); + let block_header = runner + .main_reactor() + .storage() + .read_block_header_by_height(0, true) + .expect("failure to read block header") + .expect("should have header"); + assert!(query_global_state( + &mut test.fixture, + *block_header.state_root_hash(), + Key::Evm(evm::EvmAddr::Account(alice_evm_address)), + ) + .is_none()); +} + +#[tokio::test] +async fn should_transfer_to_evm_address_with_native_transfer() { + let evm_config = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_pricing_handling(PricingHandling::Fixed) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::NoFee); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let recipient = evm::Address::new([0x44; evm::ADDRESS_LENGTH]); + let transfer_amount = test + .fixture + .chainspec + .transaction_config + .native_transfer_minimum_motes + + 100; + let alice_secret_key = Arc::clone(&test.fixture.node_contexts[0].secret_key); + let (_txn_hash, block_height, execution_result) = transfer_to_evm_address( + &mut test.fixture, + transfer_amount, + &alice_secret_key, + recipient, + PricingMode::Fixed { + gas_price_tolerance: 1, + additional_computation_factor: 0, + }, + Some(0xE0), + ) + .await; + + assert!( + exec_result_is_success(&execution_result), + "{execution_result:?}" + ); + let account = evm_account_at(&mut test.fixture, block_height, recipient); + let expected_purse = evm::deterministic_purse(recipient); + assert_eq!(account.main_purse(), expected_purse); + assert_eq!( + evm_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 = evm::EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: evm::EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_pricing_handling(PricingHandling::Fixed) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let evm_transaction = signed_evm_deploy_transaction(evm_config.chain_id); + let evm_sender = evm_transaction.from(); + seed_evm_account( + &mut test.fixture, + evm_sender, + U512::from(EVM_INITIAL_BALANCE), + ); + + let (_txn_hash, deploy_block_height, deploy_execution_result) = test + .send_transaction(Transaction::from(evm_transaction)) + .await; + let ExecutionResult::Evm(deploy_execution_result) = deploy_execution_result else { + panic!("expected EVM execution result"); + }; + assert_eq!( + deploy_execution_result.receipt.status, + evm::ReceiptStatus::Success + ); + + let contract_address = deploy_execution_result + .receipt + .contract_address + .expect("EVM deployment should create a contract"); + assert_ne!( + evm_code_hash_at(&mut test.fixture, deploy_block_height, contract_address), + EMPTY_CODE_HASH + ); + + let contract_balance_before = + evm_balance(&mut test.fixture, contract_address, deploy_block_height); + assert_eq!(contract_balance_before, U512::zero()); + + let transfer_amount = test + .fixture + .chainspec + .transaction_config + .native_transfer_minimum_motes + + 100; + let alice_secret_key = Arc::clone(&test.fixture.node_contexts[0].secret_key); + let (_txn_hash, transfer_block_height, transfer_execution_result) = transfer_to_evm_address( + &mut test.fixture, + transfer_amount, + &alice_secret_key, + contract_address, + PricingMode::Fixed { + gas_price_tolerance: 1, + additional_computation_factor: 0, + }, + Some(0xE1), + ) + .await; + + assert!( + !exec_result_is_success(&transfer_execution_result), + "native transfer to EVM contract address should fail: {transfer_execution_result:?}" + ); + assert_eq!( + evm_balance(&mut test.fixture, contract_address, transfer_block_height), + contract_balance_before + ); +} + #[tokio::test] async fn should_accept_transfer_without_id() { let initial_stakes = InitialStakes::FromVec(vec![u128::MAX, 1]); diff --git a/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..444637c262 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -1,17 +1,20 @@ mod meta_deploy; +mod meta_evm; mod meta_transaction_v1; mod transaction_header; use casper_execution_engine::engine_state::{SessionDataDeploy, SessionDataV1, SessionInputData}; #[cfg(test)] use casper_types::InvalidTransactionV1; use casper_types::{ - account::AccountHash, bytesrepr::ToBytes, Approval, Chainspec, Digest, ExecutableDeployItem, - Gas, GasLimited, HashAddr, InitiatorAddr, InvalidTransaction, Phase, PricingHandling, - PricingMode, TimeDiff, Timestamp, Transaction, TransactionArgs, TransactionConfig, - TransactionEntryPoint, TransactionHash, TransactionTarget, INSTALL_UPGRADE_LANE_ID, + account::AccountHash, bytesrepr::ToBytes, evm, Approval, Chainspec, Digest, + ExecutableDeployItem, Gas, GasLimited, HashAddr, InitiatorAddr, InvalidTransaction, Phase, + PricingHandling, PricingMode, TimeDiff, Timestamp, Transaction, TransactionArgs, + TransactionConfig, TransactionEntryPoint, TransactionHash, TransactionTarget, + INSTALL_UPGRADE_LANE_ID, }; use core::fmt::{self, Debug, Display, Formatter}; use meta_deploy::MetaDeploy; +use meta_evm::MetaEvmTransaction; pub(crate) use meta_transaction_v1::MetaTransactionV1; use serde::Serialize; use std::{borrow::Cow, collections::BTreeSet}; @@ -23,6 +26,7 @@ use super::fields_container::{ARGS_MAP_KEY, ENTRY_POINT_MAP_KEY, TARGET_MAP_KEY} #[derive(Clone, Debug, Serialize)] pub(crate) enum MetaTransaction { Deploy(MetaDeploy), + Evm(MetaEvmTransaction), V1(MetaTransactionV1), } @@ -33,6 +37,7 @@ impl MetaTransaction { MetaTransaction::Deploy(meta_deploy) => { TransactionHash::from(*meta_deploy.deploy().hash()) } + MetaTransaction::Evm(evm) => evm.hash(), MetaTransaction::V1(txn) => TransactionHash::from(*txn.hash()), } } @@ -41,6 +46,7 @@ impl MetaTransaction { pub(crate) fn timestamp(&self) -> Timestamp { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().header().timestamp(), + MetaTransaction::Evm(evm) => evm.timestamp(), MetaTransaction::V1(v1) => v1.timestamp(), } } @@ -49,6 +55,7 @@ impl MetaTransaction { pub(crate) fn ttl(&self) -> TimeDiff { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().header().ttl(), + MetaTransaction::Evm(evm) => evm.ttl(), MetaTransaction::V1(v1) => v1.ttl(), } } @@ -57,15 +64,17 @@ impl MetaTransaction { pub(crate) fn approvals(&self) -> BTreeSet { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().approvals().clone(), + MetaTransaction::Evm(evm) => evm.approvals().clone(), MetaTransaction::V1(v1) => v1.approvals().clone(), } } - /// Returns the address of the initiator of the transaction. - pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { + /// Returns the Casper initiator address, if this transaction has one. + pub(crate) fn initiator_addr(&self) -> Option<&InitiatorAddr> { match self { - MetaTransaction::Deploy(meta_deploy) => meta_deploy.initiator_addr(), - MetaTransaction::V1(txn) => txn.initiator_addr(), + MetaTransaction::Deploy(meta_deploy) => Some(meta_deploy.initiator_addr()), + MetaTransaction::Evm(_) => None, + MetaTransaction::V1(txn) => Some(txn.initiator_addr()), } } @@ -78,6 +87,11 @@ impl MetaTransaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + MetaTransaction::Evm(evm) => evm + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), MetaTransaction::V1(txn) => txn .approvals() .iter() @@ -90,6 +104,7 @@ impl MetaTransaction { pub(crate) fn is_native(&self) -> bool { match self { MetaTransaction::Deploy(meta_deploy) => meta_deploy.deploy().is_transfer(), + MetaTransaction::Evm(_) => false, MetaTransaction::V1(v1_txn) => *v1_txn.target() == TransactionTarget::Native, } } @@ -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 + .approvals() + .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<&evm::Transaction> { + match self { + MetaTransaction::Evm(evm) => Some(evm.transaction()), + _ => None, + } + } } impl Display for MetaTransaction { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { match self { MetaTransaction::Deploy(meta_deploy) => Display::fmt(meta_deploy.deploy(), formatter), + MetaTransaction::Evm(evm) => Display::fmt(evm, formatter), MetaTransaction::V1(txn) => Display::fmt(txn, formatter), } } @@ -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,432 @@ 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::TransactionLaneDefinition; + + const CHAIN_ID: u64 = 7; + const BASE_FEE: u64 = 1_000_000; + const EVM_LANE: u8 = 4; + + #[test] + fn evm_from_transaction_exposes_metadata() { + let chainspec = chainspec(); + let evm_transaction = legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000); + let transaction = Transaction::Evm(evm_transaction.clone()); + let meta = MetaTransaction::from_transaction( + &transaction, + chainspec.core_config.pricing_handling, + &chainspec.transaction_config, + ) + .expect("EVM transaction metadata should be created"); + + assert_eq!(meta.hash(), transaction.hash()); + assert_eq!(meta.timestamp(), evm_transaction.timestamp()); + assert_eq!(meta.ttl(), evm_transaction.ttl()); + assert_eq!(meta.approvals(), evm_transaction.approvals().clone()); + assert_eq!(meta.initiator_addr(), None); + assert_eq!(meta.transaction_lane(), EVM_LANE); + assert_eq!(meta.gas_limit(&chainspec).unwrap(), Gas::new(21_000)); + assert_eq!(meta.gas_price_tolerance().unwrap(), u8::MAX); + assert_eq!(meta.size_estimate(), evm_transaction.serialized_length()); + assert!(meta.is_standard_payment()); + assert!(!meta.is_custom_payment()); + assert!(!meta.is_v1_wasm()); + assert!(!meta.is_v2_wasm()); + assert!(meta.seed().is_none()); + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()) + .expect("valid EVM transaction should be config compliant"); + } + + #[test] + fn evm_from_transaction_requires_lane() { + let mut chainspec = chainspec(); + chainspec + .transaction_config + .transaction_v1_config + .set_wasm_lanes(vec![]); + let transaction = + Transaction::Evm(legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000)); + let error = MetaTransaction::from_transaction( + &transaction, + chainspec.core_config.pricing_handling, + &chainspec.transaction_config, + ) + .expect_err("EVM transaction should need a lane"); + assert!(matches!( + error, + InvalidTransaction::Evm(evm::TransactionError::MissingTransactionLane) + )); + } + + #[test] + fn evm_config_compliance_rejects_disabled_evm() { + let mut chainspec = chainspec(); + chainspec.evm_config.enabled = false; + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::Disabled)) + )); + } + + #[test] + fn evm_config_compliance_rejects_missing_chain_id() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + legacy_transaction(None, BASE_FEE.into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + evm::TransactionError::MissingChainId + )) + )); + } + + #[test] + fn evm_config_compliance_rejects_mismatched_chain_id() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID + 1), BASE_FEE.into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::ChainIdMismatch { + expected: CHAIN_ID, + actual + })) if actual == CHAIN_ID + 1 + )); + } + + #[test] + fn evm_config_compliance_rejects_gas_price_below_base_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID), (BASE_FEE - 1).into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee + })) if gas_price == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + + #[test] + fn evm_config_compliance_accepts_unsigned_call() { + let chainspec = chainspec(); + let meta = evm_meta(&chainspec, unsigned_call(CHAIN_ID, BASE_FEE.into(), 21_000)); + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()) + .expect("unsigned EVM call should be config compliant"); + } + + #[test] + fn evm_config_compliance_rejects_unsigned_call_mismatched_chain_id() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + unsigned_call(CHAIN_ID + 1, BASE_FEE.into(), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::ChainIdMismatch { + expected: CHAIN_ID, + actual + })) if actual == CHAIN_ID + 1 + )); + } + + #[test] + fn evm_config_compliance_rejects_unsigned_call_gas_price_below_base_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + unsigned_call(CHAIN_ID, u128::from(BASE_FEE - 1), 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee + })) if gas_price == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + + #[test] + fn evm_config_compliance_rejects_max_fee_below_base_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip1559_transaction(u128::from(BASE_FEE - 1), 0, 60_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee + })) if max_fee_per_gas == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + + #[test] + fn evm_config_compliance_rejects_non_zero_priority_fee() { + let chainspec = chainspec(); + let meta = evm_meta(&chainspec, eip1559_transaction(BASE_FEE.into(), 1, 60_000)); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + evm::TransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas: 1 + } + )) + )); + } + + #[test] + fn evm_config_compliance_accepts_eip7702() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID, BASE_FEE.into(), 0, 60_000), + ); + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()) + .expect("valid EIP-7702 transaction should be config compliant"); + } + + #[test] + fn evm_config_compliance_rejects_eip7702_mismatched_chain_id() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID + 1, BASE_FEE.into(), 0, 60_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::ChainIdMismatch { + expected: CHAIN_ID, + actual + })) if actual == CHAIN_ID + 1 + )); + } + + #[test] + fn evm_config_compliance_rejects_eip7702_max_fee_below_base_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID, u128::from(BASE_FEE - 1), 0, 60_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee + })) if max_fee_per_gas == u128::from(BASE_FEE - 1) && base_fee == u128::from(BASE_FEE) + )); + } + + #[test] + fn evm_config_compliance_rejects_eip7702_non_zero_priority_fee() { + let chainspec = chainspec(); + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID, BASE_FEE.into(), 1, 60_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + evm::TransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas: 1 + } + )) + )); + } + + #[test] + fn evm_config_compliance_rejects_gas_limit_above_block_limit() { + let chainspec = chainspec(); + let gas_limit = chainspec.evm_config.block_gas_limit + 1; + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), gas_limit), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit: actual_gas_limit, + block_gas_limit + })) if actual_gas_limit == gas_limit && block_gas_limit == chainspec.evm_config.block_gas_limit + )); + } + + #[test] + fn evm_config_compliance_rejects_eip7702_gas_limit_above_block_limit() { + let chainspec = chainspec(); + let gas_limit = chainspec.evm_config.block_gas_limit + 1; + let meta = evm_meta( + &chainspec, + eip7702_transaction(CHAIN_ID, BASE_FEE.into(), 0, gas_limit), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(evm::TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit: actual_gas_limit, + block_gas_limit + })) if actual_gas_limit == gas_limit && block_gas_limit == chainspec.evm_config.block_gas_limit + )); + } + + #[test] + fn evm_config_compliance_rejects_invalid_approval() { + let chainspec = chainspec(); + let approvals = BTreeSet::new(); + let evm_transaction = + legacy_transaction(Some(CHAIN_ID), BASE_FEE.into(), 21_000).with_approvals(approvals); + let meta = evm_meta(&chainspec, evm_transaction); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm( + evm::TransactionError::MissingApproval + )) + )); + } + + fn chainspec() -> Chainspec { + let mut chainspec = Chainspec::default(); + chainspec.evm_config.enabled = true; + chainspec.evm_config.chain_id = CHAIN_ID; + chainspec.evm_config.base_fee = BASE_FEE; + chainspec.evm_config.block_gas_limit = 30_000_000; + chainspec + .transaction_config + .transaction_v1_config + .set_wasm_lanes(vec![TransactionLaneDefinition::new( + EVM_LANE, + u64::MAX, + 10_000, + u64::MAX, + 10, + )]); + chainspec + } + + fn evm_meta(chainspec: &Chainspec, evm_transaction: evm::Transaction) -> MetaTransaction { + MetaTransaction::from_transaction( + &Transaction::Evm(evm_transaction), + chainspec.core_config.pricing_handling, + &chainspec.transaction_config, + ) + .expect("EVM transaction metadata should be created") + } + + fn unsigned_call(chain_id: u64, gas_price: u128, gas_limit: u64) -> evm::Transaction { + evm::Transaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(60), + chain_id, + evm::Address::new([1u8; 20]), + Some(evm::Address::new([2u8; 20])), + casper_types::U256::zero(), + Default::default(), + gas_limit, + gas_price, + ) + } + + fn legacy_transaction( + chain_id: Option, + gas_price: u128, + gas_limit: u64, + ) -> evm::Transaction { + // Ethereum legacy transactions are the original, untyped transaction + // envelope. With EIP-155 replay protection they include a chain ID, + // but they still use a single fixed `gas_price` instead of separate + // base-fee and priority-fee fields. + let tx = TxLegacy { + chain_id, + nonce: 0, + gas_price, + gas_limit, + to: TxKind::Call(AlloyAddress::from([1u8; 20])), + value: U256::ZERO, + input: Default::default(), + }; + signed_transaction(tx.into_signed(Signature::test_signature()).into()) + } + + fn eip1559_transaction( + max_fee_per_gas: u128, + max_priority_fee_per_gas: u128, + gas_limit: u64, + ) -> evm::Transaction { + // EIP-1559 transactions are typed dynamic-fee transactions. Casper + // currently accepts this envelope for tooling compatibility, but + // requires `max_priority_fee_per_gas == 0` because transactions are + // not packed by priority fee. + let tx = TxEip1559 { + chain_id: CHAIN_ID, + nonce: 0, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to: TxKind::Call(AlloyAddress::from([1u8; 20])), + value: U256::ZERO, + access_list: Default::default(), + input: Default::default(), + }; + signed_transaction(tx.into_signed(Signature::test_signature()).into()) + } + + fn eip7702_transaction( + chain_id: u64, + max_fee_per_gas: u128, + max_priority_fee_per_gas: u128, + gas_limit: u64, + ) -> evm::Transaction { + let authorization = AlloyAuthorization { + chain_id: U256::from(chain_id), + address: AlloyAddress::from([2u8; 20]), + nonce: 0, + } + .into_signed(Signature::test_signature()); + let tx = TxEip7702 { + chain_id, + nonce: 0, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to: AlloyAddress::from([1u8; 20]), + value: U256::ZERO, + access_list: Default::default(), + authorization_list: vec![authorization], + input: Default::default(), + }; + signed_transaction(tx.into_signed(Signature::test_signature()).into()) + } + + fn signed_transaction(envelope: TxEnvelope) -> evm::Transaction { + evm::Transaction::from_signed_rlp( + envelope.encoded_2718(), + 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..6ae905a8f8 --- /dev/null +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -0,0 +1,165 @@ +use std::{ + collections::BTreeSet, + fmt::{self, Display, Formatter}, +}; + +use casper_types::{ + bytesrepr::ToBytes, evm, Approval, Chainspec, Digest, Gas, InvalidTransaction, TimeDiff, + Timestamp, TransactionConfig, TransactionHash, +}; +use serde::Serialize; + +/// Metadata extracted from a Casper EVM transaction. +#[derive(Clone, Debug, Serialize)] +pub(crate) struct MetaEvmTransaction { + transaction: evm::Transaction, + lane_id: u8, + payload_hash: Digest, +} + +impl MetaEvmTransaction { + pub(crate) fn from_evm_transaction( + transaction: &evm::Transaction, + transaction_config: &TransactionConfig, + ) -> Result { + let lane_id = transaction_config + .transaction_v1_config + .wasm_lanes() + .iter() + .last() + .map(|lane| lane.id()) + .ok_or(evm::TransactionError::MissingTransactionLane)?; + let payload_hash = Digest::hash(transaction.signing_payload()?); + Ok(MetaEvmTransaction { + transaction: transaction.clone(), + lane_id, + payload_hash, + }) + } + + pub(crate) fn transaction(&self) -> &evm::Transaction { + &self.transaction + } + + pub(crate) fn hash(&self) -> TransactionHash { + TransactionHash::from(self.transaction.hash()) + } + + pub(crate) fn timestamp(&self) -> Timestamp { + self.transaction.timestamp() + } + + pub(crate) fn ttl(&self) -> TimeDiff { + self.transaction.ttl() + } + + pub(crate) fn approvals(&self) -> &BTreeSet { + self.transaction.approvals() + } + + pub(crate) fn lane_id(&self) -> u8 { + self.lane_id + } + + pub(crate) fn gas_limit(&self) -> Gas { + Gas::new(self.transaction.gas_limit()) + } + + pub(crate) fn gas_price_tolerance(&self) -> u8 { + u8::MAX + } + + pub(crate) fn serialized_length(&self) -> usize { + self.transaction.serialized_length() + } + + pub(crate) fn payload_hash(&self) -> Digest { + self.payload_hash + } + + pub(crate) fn verify(&self) -> Result<(), evm::TransactionError> { + self.transaction.verify() + } + + pub(crate) fn is_config_compliant( + &self, + chainspec: &Chainspec, + ) -> Result<(), evm::TransactionError> { + let transaction = &self.transaction; + let evm_config = &chainspec.evm_config; + if !evm_config.enabled { + return Err(evm::TransactionError::Disabled); + } + + if !transaction.is_unsigned_call() { + transaction.verify()?; + } + + let expected = evm_config.chain_id; + let actual = transaction + .chain_id() + .ok_or(evm::TransactionError::MissingChainId)?; + if actual != expected { + return Err(evm::TransactionError::ChainIdMismatch { expected, actual }); + } + + let gas_limit = transaction.gas_limit(); + let block_gas_limit = evm_config.block_gas_limit; + if gas_limit > block_gas_limit { + return Err(evm::TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit, + block_gas_limit, + }); + } + + let base_fee = u128::from(evm_config.base_fee); + match transaction.kind() { + evm::TransactionKind::Legacy | evm::TransactionKind::Eip2930 => { + let gas_price = transaction + .gas_price() + .ok_or(evm::TransactionError::MissingGasPrice)?; + if gas_price < base_fee { + return Err(evm::TransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee, + }); + } + } + evm::TransactionKind::Eip1559 | evm::TransactionKind::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(evm::TransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee, + }); + } + let max_priority_fee_per_gas = transaction.max_priority_fee_per_gas().unwrap_or(0); + if max_priority_fee_per_gas != 0 { + // Casper does not currently prioritize transactions based + // on transaction gas parameters. Accepting a non-zero + // EIP-1559 priority fee would charge users for a priority + // signal that the node does not honor, so this prototype + // only accepts EIP-1559 as a max-fee compatibility + // envelope with zero priority fee. + return Err(evm::TransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas, + }); + } + } + } + + Ok(()) + } +} + +impl Display for MetaEvmTransaction { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.transaction, formatter) + } +} diff --git a/node/src/types/transaction/meta_transaction/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..0f8c694006 100644 --- a/node/src/types/transaction/meta_transaction/transaction_header.rs +++ b/node/src/types/transaction/meta_transaction/transaction_header.rs @@ -1,16 +1,18 @@ -use casper_types::{DeployHeader, InitiatorAddr, TimeDiff, Timestamp, Transaction, TransactionV1}; +use casper_types::{ + evm, DeployHeader, InitiatorAddr, TimeDiff, Timestamp, Transaction, TransactionV1, +}; use core::fmt::{self, Display, Formatter}; use datasize::DataSize; use serde::Serialize; #[derive(Debug, Clone, DataSize, PartialEq, Eq, Serialize)] -pub(crate) struct TransactionV1Metadata { +pub(crate) struct TransactionMetadata { initiator_addr: InitiatorAddr, timestamp: Timestamp, ttl: TimeDiff, } -impl TransactionV1Metadata { +impl TransactionMetadata { pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { &self.initiator_addr } @@ -24,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: EVM address {}]", + self.initiator_addr, + ) + } +} + +#[derive(Debug, Clone, DataSize, PartialEq, Eq, Serialize)] +pub(crate) struct EvmTransactionMetadata { + initiator_addr: evm::Address, + timestamp: Timestamp, + ttl: TimeDiff, +} + +impl EvmTransactionMetadata { + pub(crate) fn initiator_addr(&self) -> evm::Address { + self.initiator_addr + } + + pub(crate) fn timestamp(&self) -> Timestamp { + self.timestamp + } + + pub(crate) fn ttl(&self) -> TimeDiff { + self.ttl + } +} + #[derive(Debug, Clone, DataSize, Serialize, PartialEq, Eq)] /// A versioned wrapper for a transaction header or deploy header. pub(crate) enum TransactionHeader { Deploy(DeployHeader), - V1(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<&evm::Transaction> for TransactionHeader { + fn from(transaction: &evm::Transaction) -> Self { + let meta = EvmTransactionMetadata { + initiator_addr: transaction.from(), + timestamp: transaction.timestamp(), + ttl: transaction.ttl(), + }; + Self::Evm(meta) + } +} + impl From<&Transaction> for TransactionHeader { fn from(transaction: &Transaction) -> Self { match transaction { Transaction::Deploy(deploy) => deploy.header().clone().into(), Transaction::V1(v1) => v1.into(), + Transaction::Evm(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..696d414f3e 100644 --- a/node/src/utils/chain_specification/parse_toml.rs +++ b/node/src/utils/chain_specification/parse_toml.rs @@ -32,10 +32,10 @@ use std::{convert::TryFrom, path::Path}; use serde::{Deserialize, Serialize}; use casper_types::{ - bytesrepr::Bytes, file_utils, AccountsConfig, ActivationPoint, Chainspec, ChainspecRawBytes, - CoreConfig, GlobalStateUpdate, GlobalStateUpdateConfig, HighwayConfig, NetworkConfig, - ProtocolConfig, ProtocolVersion, StorageCosts, SystemConfig, TransactionConfig, VacancyConfig, - WasmConfig, + bytesrepr::Bytes, evm::EvmConfig, file_utils, AccountsConfig, ActivationPoint, Chainspec, + ChainspecRawBytes, CoreConfig, GlobalStateUpdate, GlobalStateUpdateConfig, HighwayConfig, + NetworkConfig, ProtocolConfig, ProtocolVersion, StorageCosts, SystemConfig, TransactionConfig, + VacancyConfig, WasmConfig, }; use crate::utils::{ @@ -77,6 +77,8 @@ pub(super) struct TomlChainspec { network: TomlNetwork, core: CoreConfig, transactions: TransactionConfig, + #[serde(default)] + evm: EvmConfig, highway: HighwayConfig, wasm: WasmConfig, system_costs: SystemConfig, @@ -97,6 +99,7 @@ impl From<&Chainspec> for TomlChainspec { }; let core = chainspec.core_config.clone(); let transactions = chainspec.transaction_config.clone(); + let evm = chainspec.evm_config; let highway = chainspec.highway_config; let wasm = chainspec.wasm_config; let system_costs = chainspec.system_costs_config; @@ -108,6 +111,7 @@ impl From<&Chainspec> for TomlChainspec { network, core, transactions, + evm, highway, wasm, system_costs, @@ -163,6 +167,7 @@ pub(super) fn parse_toml>( network_config, core_config: toml_chainspec.core, transaction_config: toml_chainspec.transactions, + evm_config: toml_chainspec.evm, highway_config: toml_chainspec.highway, wasm_config: toml_chainspec.wasm, system_costs_config: toml_chainspec.system_costs, diff --git a/node/src/utils/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/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..20b8e015b2 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::{ @@ -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(evm::EvmAddr::Account(address)), + StoredValue::CLValue(identity), + ); + tc.borrow_mut().write( + Key::Evm(evm::EvmAddr::Nonce(address)), + StoredValue::CLValue(nonce), + ); + tc.borrow_mut().write( + Key::Evm(evm::EvmAddr::CodeHash(address)), + StoredValue::CLValue(code_hash), + ); + tc.borrow_mut().write( + Key::Balance(main_purse.addr()), + 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..fcaa515b56 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -4,6 +4,7 @@ use thiserror::Error; use casper_types::{ account::AccountHash, bytesrepr::FromBytes, + evm, system::{mint, mint::Error as MintError}, AccessRights, CLType, CLTyped, CLValue, CLValueError, Key, ProtocolVersion, RuntimeArgs, RuntimeFootprint, StoredValue, StoredValueTypeMismatch, URef, U512, @@ -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(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(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..d3f197e026 100644 --- a/types/src/byte_code.rs +++ b/types/src/byte_code.rs @@ -135,6 +135,7 @@ impl ByteCodeAddr { ByteCodeKind::V1CasperWasm => Ok(ByteCodeAddr::V1CasperWasm(byte_code_addr)), ByteCodeKind::V2CasperWasm => Ok(ByteCodeAddr::V2CasperWasm(byte_code_addr)), ByteCodeKind::Empty => Ok(ByteCodeAddr::Empty), + ByteCodeKind::EvmPrague => Err(FromStrError::InvalidPrefix), }; } @@ -187,6 +188,7 @@ impl FromBytes for ByteCodeAddr { let (addr, remainder) = HashAddr::from_bytes(remainder)?; Ok((ByteCodeAddr::V2CasperWasm(addr), remainder)) } + ByteCodeKind::EvmPrague => Err(Error::Formatting), } } } @@ -420,6 +422,20 @@ pub enum ByteCodeKind { V1CasperWasm = 1, /// Byte code to be executed with the version 2 Casper execution engine. V2CasperWasm = 2, + /// Prague-compatible EVM bytecode. + /// + /// This variant records bytecode that is valid for the Prague EVM rules. + /// When support for a future bytecode-affecting EVM spec such as Osaka is + /// officially added, introduce a new `Evm` variant instead of + /// changing the meaning of this one. + EvmPrague = 3, +} + +impl ByteCodeKind { + /// Returns whether this bytecode kind is executable by the EVM executor. + pub fn is_evm(self) -> bool { + matches!(self, ByteCodeKind::EvmPrague) + } } impl ToBytes for ByteCodeKind { @@ -449,6 +465,9 @@ impl FromBytes for ByteCodeKind { byte_code_kind if byte_code_kind == ByteCodeKind::V2CasperWasm as u8 => { Ok((ByteCodeKind::V2CasperWasm, remainder)) } + byte_code_kind if byte_code_kind == ByteCodeKind::EvmPrague as u8 => { + Ok((ByteCodeKind::EvmPrague, remainder)) + } _ => Err(Error::Formatting), } } @@ -466,6 +485,9 @@ impl Display for ByteCodeKind { ByteCodeKind::V2CasperWasm => { write!(f, "v2-casper-wasm") } + ByteCodeKind::EvmPrague => { + write!(f, "evm-prague") + } } } } @@ -473,10 +495,11 @@ impl Display for ByteCodeKind { #[cfg(any(feature = "testing", test))] impl Distribution for Standard { fn sample(&self, rng: &mut R) -> ByteCodeKind { - match rng.gen_range(0..=2) { + match rng.gen_range(0..=3) { 0 => ByteCodeKind::Empty, 1 => ByteCodeKind::V1CasperWasm, 2 => ByteCodeKind::V2CasperWasm, + 3 => ByteCodeKind::EvmPrague, _ => unreachable!(), } } diff --git a/types/src/chainspec.rs b/types/src/chainspec.rs index f29bcfcdba..57078cb50a 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -28,13 +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}, + evm::EvmConfig, ChainNameDigest, Digest, EraId, ProtocolVersion, Timestamp, }; pub use accounts_config::{ @@ -43,14 +44,14 @@ pub use accounts_config::{ }; pub use activation_point::ActivationPoint; pub use chainspec_raw_bytes::ChainspecRawBytes; +#[cfg(any(all(feature = "std", feature = "testing"), test))] +pub use core_config::DEFAULT_FEE_HANDLING; pub use core_config::{ ConsensusProtocolName, CoreConfig, LegacyRequiredFinality, DEFAULT_GAS_HOLD_INTERVAL, DEFAULT_MINIMUM_BID_AMOUNT, }; #[cfg(any(feature = "std", test))] -pub use core_config::{ - DEFAULT_BASELINE_MOTES_AMOUNT, DEFAULT_FEE_HANDLING, DEFAULT_REFUND_HANDLING, -}; +pub use core_config::{DEFAULT_BASELINE_MOTES_AMOUNT, DEFAULT_REFUND_HANDLING}; pub use fee_handling::FeeHandling; #[cfg(any(feature = "std", test))] pub use genesis_config::GenesisConfig; @@ -97,7 +98,7 @@ pub use vm_config::{ /// A collection of configuration settings describing the state of the system at genesis and after /// upgrades to basic system functionality occurring after genesis. -#[derive(Clone, PartialEq, Eq, Serialize, Debug, Default)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Default)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[serde(deny_unknown_fields)] pub struct Chainspec { @@ -121,6 +122,10 @@ pub struct Chainspec { #[serde(rename = "transactions")] pub transaction_config: TransactionConfig, + /// EVM config. + #[serde(rename = "evm")] + pub evm_config: EvmConfig, + /// Wasm config. #[serde(rename = "wasm")] pub wasm_config: WasmConfig, @@ -276,6 +281,7 @@ impl Chainspec { let core_config = CoreConfig::random(rng); let highway_config = HighwayConfig::random(rng); let transaction_config = TransactionConfig::random(rng); + let evm_config = EvmConfig::default(); let wasm_config = rng.gen(); let system_costs_config = SystemConfig::random(rng); let vacancy_config = VacancyConfig::random(rng); @@ -286,6 +292,7 @@ impl Chainspec { core_config, highway_config, transaction_config, + evm_config, wasm_config, system_costs_config, vacancy_config, @@ -337,6 +344,7 @@ impl ToBytes for Chainspec { self.core_config.write_bytes(writer)?; self.highway_config.write_bytes(writer)?; self.transaction_config.write_bytes(writer)?; + self.evm_config.write_bytes(writer)?; self.wasm_config.write_bytes(writer)?; self.system_costs_config.write_bytes(writer)?; self.vacancy_config.write_bytes(writer)?; @@ -355,6 +363,7 @@ impl ToBytes for Chainspec { + self.core_config.serialized_length() + self.highway_config.serialized_length() + self.transaction_config.serialized_length() + + self.evm_config.serialized_length() + self.wasm_config.serialized_length() + self.system_costs_config.serialized_length() + self.vacancy_config.serialized_length() @@ -369,6 +378,7 @@ impl FromBytes for Chainspec { let (core_config, remainder) = CoreConfig::from_bytes(remainder)?; let (highway_config, remainder) = HighwayConfig::from_bytes(remainder)?; let (transaction_config, remainder) = TransactionConfig::from_bytes(remainder)?; + let (evm_config, remainder) = EvmConfig::from_bytes(remainder)?; let (wasm_config, remainder) = WasmConfig::from_bytes(remainder)?; let (system_costs_config, remainder) = SystemConfig::from_bytes(remainder)?; let (vacancy_config, remainder) = VacancyConfig::from_bytes(remainder)?; @@ -379,6 +389,7 @@ impl FromBytes for Chainspec { core_config, highway_config, transaction_config, + evm_config, wasm_config, system_costs_config, vacancy_config, diff --git a/types/src/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..e7569b5c3a --- /dev/null +++ b/types/src/evm.rs @@ -0,0 +1,30 @@ +//! 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 eth_u256; +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 config::{EvmConfig, EvmSpec}; +pub use eth_u256::EthU256; +pub use evm_addr::EvmAddr; +pub use hash::{Hash, HASH_LENGTH}; +pub use receipt::{HaltReason, Log, OutOfGasError, Receipt, ReceiptStatus}; +pub use topic::Topic; +pub use transaction::{ + SetCodeAuthorization, Transaction, TransactionError, TransactionHash, TransactionKind, + EIP1559_TRANSACTION_TYPE_ID, EIP2930_TRANSACTION_TYPE_ID, EIP4844_TRANSACTION_TYPE_ID, + EIP7702_TRANSACTION_TYPE_ID, LEGACY_TRANSACTION_TYPE_ID, +}; diff --git a/types/src/evm/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..3f1b3e3a52 --- /dev/null +++ b/types/src/evm/config.rs @@ -0,0 +1,206 @@ +use alloc::vec::Vec; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}; + +/// Supported EVM hardfork specifications for chainspec configuration. +#[derive( + Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize, +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(rename_all = "snake_case")] +pub enum EvmSpec { + /// Frontier. + Frontier, + /// Frontier thawing. + FrontierThawing, + /// Homestead. + Homestead, + /// DAO fork. + DaoFork, + /// Tangerine Whistle. + Tangerine, + /// Spurious Dragon. + SpuriousDragon, + /// Byzantium. + Byzantium, + /// Constantinople. + Constantinople, + /// Petersburg. + Petersburg, + /// Istanbul. + Istanbul, + /// Muir Glacier. + MuirGlacier, + /// Berlin. + Berlin, + /// London. + London, + /// Arrow Glacier. + ArrowGlacier, + /// Gray Glacier. + GrayGlacier, + /// Paris, also known as the merge. + Merge, + /// Shanghai. + Shanghai, + /// Cancun. + Cancun, + /// Prague. + #[default] + Prague, + /// Osaka. + Osaka, +} + +impl EvmSpec { + fn tag(self) -> u8 { + match self { + EvmSpec::Frontier => 0, + EvmSpec::FrontierThawing => 1, + EvmSpec::Homestead => 2, + EvmSpec::DaoFork => 3, + EvmSpec::Tangerine => 4, + EvmSpec::SpuriousDragon => 5, + EvmSpec::Byzantium => 6, + EvmSpec::Constantinople => 7, + EvmSpec::Petersburg => 8, + EvmSpec::Istanbul => 9, + EvmSpec::MuirGlacier => 10, + EvmSpec::Berlin => 11, + EvmSpec::London => 12, + EvmSpec::ArrowGlacier => 13, + EvmSpec::GrayGlacier => 14, + EvmSpec::Merge => 15, + EvmSpec::Shanghai => 16, + EvmSpec::Cancun => 17, + EvmSpec::Prague => 18, + EvmSpec::Osaka => 19, + } + } +} + +impl ToBytes for EvmSpec { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Ok(vec![self.tag()]) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.push(self.tag()); + Ok(()) + } +} + +impl FromBytes for EvmSpec { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let spec = match tag { + 0 => EvmSpec::Frontier, + 1 => EvmSpec::FrontierThawing, + 2 => EvmSpec::Homestead, + 3 => EvmSpec::DaoFork, + 4 => EvmSpec::Tangerine, + 5 => EvmSpec::SpuriousDragon, + 6 => EvmSpec::Byzantium, + 7 => EvmSpec::Constantinople, + 8 => EvmSpec::Petersburg, + 9 => EvmSpec::Istanbul, + 10 => EvmSpec::MuirGlacier, + 11 => EvmSpec::Berlin, + 12 => EvmSpec::London, + 13 => EvmSpec::ArrowGlacier, + 14 => EvmSpec::GrayGlacier, + 15 => EvmSpec::Merge, + 16 => EvmSpec::Shanghai, + 17 => EvmSpec::Cancun, + 18 => EvmSpec::Prague, + 19 => EvmSpec::Osaka, + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((spec, remainder)) + } +} + +/// Chainspec configuration for EVM execution. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct EvmConfig { + /// Whether EVM execution is enabled. + pub enabled: bool, + /// EVM chain ID used for the `CHAINID` opcode and transaction validation. + pub chain_id: u64, + /// Hardfork specification used by the EVM executor. + pub spec: EvmSpec, + /// Per-block gas limit supplied to the EVM block context. + pub block_gas_limit: u64, + /// Base fee supplied to the EVM block context. + pub base_fee: u64, +} + +impl Default for EvmConfig { + fn default() -> Self { + EvmConfig { + enabled: false, + chain_id: 0, + spec: EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + } + } +} + +impl ToBytes for EvmConfig { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.enabled.serialized_length() + + self.chain_id.serialized_length() + + self.spec.serialized_length() + + self.block_gas_limit.serialized_length() + + self.base_fee.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.enabled.write_bytes(writer)?; + self.chain_id.write_bytes(writer)?; + self.spec.write_bytes(writer)?; + self.block_gas_limit.write_bytes(writer)?; + self.base_fee.write_bytes(writer) + } +} + +impl FromBytes for EvmConfig { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (enabled, remainder) = bool::from_bytes(bytes)?; + let (chain_id, remainder) = u64::from_bytes(remainder)?; + let (spec, remainder) = EvmSpec::from_bytes(remainder)?; + let (block_gas_limit, remainder) = u64::from_bytes(remainder)?; + let (base_fee, remainder) = u64::from_bytes(remainder)?; + Ok(( + EvmConfig { + enabled, + chain_id, + spec, + block_gas_limit, + base_fee, + }, + remainder, + )) + } +} diff --git a/types/src/evm/eth_u256.rs b/types/src/evm/eth_u256.rs new file mode 100644 index 0000000000..dc04643ae2 --- /dev/null +++ b/types/src/evm/eth_u256.rs @@ -0,0 +1,216 @@ +use alloc::{ + format, + string::{String, ToString}, +}; +use core::fmt::{self, Display, Formatter}; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::U256; + +const ETH_U256_BYTES: usize = 32; + +/// A 256-bit unsigned integer encoded as an Ethereum JSON-RPC quantity. +/// +/// Human-readable serializers use canonical `0x`-prefixed hexadecimal without +/// leading zeroes. Binary serializers delegate to [`U256`]. +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +pub struct EthU256(U256); + +impl EthU256 { + /// The zero quantity. + pub const ZERO: EthU256 = EthU256(U256::MIN); + + /// Returns the underlying 256-bit integer. + pub fn value(self) -> U256 { + self.0 + } + + /// Converts this value into a `u64` if it fits. + pub fn as_u64(self) -> Result { + if self.0 > U256::from(u64::MAX) { + Err("quantity exceeds u64") + } else { + Ok(self.0.low_u64()) + } + } + + fn to_quantity_hex(self) -> String { + if self.0.is_zero() { + return "0x0".to_string(); + } + let mut bytes = [0u8; ETH_U256_BYTES]; + self.0.to_big_endian(&mut bytes); + let first_non_zero = bytes + .iter() + .position(|byte| *byte != 0) + .expect("non-zero U256 has a non-zero byte"); + let hex = base16::encode_lower(&bytes[first_non_zero..]); + format!("0x{}", hex.trim_start_matches('0')) + } +} + +impl From for EthU256 { + fn from(value: U256) -> Self { + EthU256(value) + } +} + +impl From for U256 { + fn from(value: EthU256) -> Self { + value.value() + } +} + +impl From for EthU256 { + fn from(value: u8) -> Self { + EthU256(U256::from(value)) + } +} + +impl From for EthU256 { + fn from(value: u64) -> Self { + EthU256(U256::from(value)) + } +} + +impl From for EthU256 { + fn from(value: u128) -> Self { + EthU256(U256::from(value)) + } +} + +impl From for EthU256 { + fn from(value: usize) -> Self { + EthU256(U256::from(value)) + } +} + +impl Display for EthU256 { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.to_quantity_hex()) + } +} + +#[cfg(feature = "json-schema")] +impl JsonSchema for EthU256 { + fn schema_name() -> String { + String::from("EthU256") + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + let schema = gen.subschema_for::(); + let mut schema_object = schema.into_object(); + schema_object.metadata().description = + Some("Ethereum JSON-RPC quantity encoded as canonical 0x-prefixed hexadecimal.".into()); + schema_object.into() + } +} + +impl Serialize for EthU256 { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + serializer.collect_str(self) + } else { + self.0.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for EthU256 { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let value = String::deserialize(deserializer)?; + parse_eth_u256(&value) + .map(EthU256) + .map_err(D::Error::custom) + } else { + U256::deserialize(deserializer).map(EthU256) + } + } +} + +fn parse_eth_u256(value: &str) -> Result { + let hex = value + .strip_prefix("0x") + .ok_or_else(|| "quantity must start with 0x".to_string())?; + if hex.is_empty() { + return Err("quantity must not be empty".to_string()); + } + if hex.len() > ETH_U256_BYTES * 2 { + return Err("quantity exceeds 256 bits".to_string()); + } + let padded = if hex.len() % 2 == 0 { + hex.to_string() + } else { + format!("0{hex}") + }; + let bytes = base16::decode(padded.as_bytes()).map_err(|error| format!("{error}"))?; + Ok(U256::from_big_endian(&bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn human_readable_serde_uses_canonical_ethereum_quantity_hex() { + let value = EthU256::from(U256::from_big_endian(&[0x01, 0x23])); + + let encoded = serde_json::to_string(&value).expect("quantity should serialize"); + assert_eq!(encoded, "\"0x123\""); + + let decoded: EthU256 = serde_json::from_str(&encoded).expect("quantity should deserialize"); + assert_eq!(decoded, value); + } + + #[test] + fn zero_serializes_as_zero_quantity() { + let encoded = serde_json::to_string(&EthU256::ZERO).expect("quantity should serialize"); + assert_eq!(encoded, "\"0x0\""); + } + + #[test] + fn human_readable_serde_accepts_full_width_quantity() { + let encoded = format!("\"0x{}\"", "f".repeat(ETH_U256_BYTES * 2)); + let decoded: EthU256 = serde_json::from_str(&encoded).expect("quantity should deserialize"); + + assert_eq!(decoded.value(), U256::MAX); + } + + #[test] + fn human_readable_serde_rejects_invalid_quantities() { + for encoded in [ + "\"1\"", + "\"0x\"", + "\"0xg\"", + "\"0x10000000000000000000000000000000000000000000000000000000000000000\"", + ] { + serde_json::from_str::(encoded) + .expect_err("invalid quantity should fail to deserialize"); + } + } + + #[test] + fn as_u64_rejects_overflow() { + assert_eq!(EthU256::from(u64::MAX).as_u64(), Ok(u64::MAX)); + assert_eq!( + EthU256::from(U256::from(u64::MAX) + U256::from(1)).as_u64(), + Err("quantity exceeds u64") + ); + } + + #[test] + fn non_human_readable_serde_roundtrip() { + let value = EthU256::from(U256::from_big_endian(&[0xab; ETH_U256_BYTES])); + let encoded = bincode::serialize(&value).expect("quantity should serialize"); + let decoded: EthU256 = bincode::deserialize(&encoded).expect("quantity should deserialize"); + + assert_eq!(decoded, value); + } +} 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..c6148fb741 --- /dev/null +++ b/types/src/evm/transaction.rs @@ -0,0 +1,1510 @@ +use alloc::{ + collections::BTreeSet, + 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}, + Approval, AsymmetricType, Digest, PublicKey, SecretKey, Signature, TimeDiff, Timestamp, U256, + U512, +}; + +const TRANSACTION_KIND_SERIALIZED_LENGTH: usize = U8_SERIALIZED_LENGTH; + +/// Ethereum transaction type ID for legacy transactions. +pub const LEGACY_TRANSACTION_TYPE_ID: u8 = 0; + +/// Ethereum transaction type ID for EIP-2930 access-list transactions. +pub const EIP2930_TRANSACTION_TYPE_ID: u8 = 1; + +/// Ethereum transaction type ID for EIP-1559 dynamic-fee transactions. +pub const EIP1559_TRANSACTION_TYPE_ID: u8 = 2; + +/// Ethereum transaction type ID for EIP-4844 blob transactions. +pub const EIP4844_TRANSACTION_TYPE_ID: u8 = EIP4844_TX_TYPE_ID; + +/// 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))] +#[cfg_attr(feature = "json-schema", schemars(rename = "EvmTransactionHash"))] +pub struct TransactionHash(Digest); + +impl TransactionHash { + /// Creates a transaction hash from a raw digest. + pub const fn new(hash: Digest) -> Self { + TransactionHash(hash) + } + + /// Returns a new `TransactionHash` directly initialized with the provided bytes. + pub const fn from_raw(raw_digest: [u8; HASH_LENGTH]) -> Self { + TransactionHash(Digest::from_raw(raw_digest)) + } + + /// Returns the wrapped inner digest. + pub fn inner(&self) -> &Digest { + &self.0 + } + + /// Returns the wrapped hash. + pub 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 { + TransactionHash(Digest::from(rng.gen::<[u8; HASH_LENGTH]>())) + } +} + +impl AsRef<[u8]> for TransactionHash { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Display for TransactionHash { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "0x{}", base16::encode_lower(&self.0)) + } +} + +impl ToBytes for TransactionHash { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + self.0.to_bytes() + } + + fn serialized_length(&self) -> usize { + self.0.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.0.write_bytes(writer) + } +} + +impl FromBytes for TransactionHash { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + Digest::from_bytes(bytes).map(|(hash, remainder)| (TransactionHash(hash), remainder)) + } +} + +impl From for TransactionHash { + fn from(digest: Digest) -> Self { + TransactionHash(digest) + } +} + +impl From for Digest { + fn from(transaction_hash: TransactionHash) -> Self { + transaction_hash.0 + } +} + +/// The supported Ethereum transaction envelope kinds. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum TransactionKind { + /// A legacy Ethereum transaction. + Legacy, + /// An EIP-2930 access-list transaction. + Eip2930, + /// An EIP-1559 dynamic-fee transaction. + Eip1559, + /// An EIP-7702 set-code transaction. + Eip7702, +} + +impl TransactionKind { + /// Returns the Ethereum transaction type ID for this transaction kind. + pub const fn type_id(self) -> u8 { + match self { + TransactionKind::Legacy => LEGACY_TRANSACTION_TYPE_ID, + TransactionKind::Eip2930 => EIP2930_TRANSACTION_TYPE_ID, + TransactionKind::Eip1559 => EIP1559_TRANSACTION_TYPE_ID, + TransactionKind::Eip7702 => EIP7702_TRANSACTION_TYPE_ID, + } + } + + fn tag(self) -> u8 { + self.type_id() + } +} + +impl Display for TransactionKind { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + match self { + TransactionKind::Legacy => formatter.write_str("legacy"), + TransactionKind::Eip2930 => formatter.write_str("eip2930"), + TransactionKind::Eip1559 => formatter.write_str("eip1559"), + TransactionKind::Eip7702 => formatter.write_str("eip7702"), + } + } +} + +impl ToBytes for TransactionKind { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Ok(vec![self.tag()]) + } + + fn serialized_length(&self) -> usize { + TRANSACTION_KIND_SERIALIZED_LENGTH + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.push(self.tag()); + Ok(()) + } +} + +impl FromBytes for TransactionKind { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let kind = match tag { + 0 => TransactionKind::Legacy, + 1 => TransactionKind::Eip2930, + 2 => TransactionKind::Eip1559, + EIP7702_TRANSACTION_TYPE_ID => TransactionKind::Eip7702, + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((kind, remainder)) + } +} + +/// A signed EIP-7702 authorization-list item. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct SetCodeAuthorization { + /// Chain ID that scopes the authorization; zero follows EIP-7702 wildcard semantics. + pub chain_id: U256, + /// Address whose code the authorized account delegates to. + pub address: Address, + /// Nonce expected on the authorizing account. + pub nonce: u64, + /// secp256k1 signature recovery parity. + pub y_parity: u8, + /// secp256k1 signature `r` value. + pub r: U256, + /// secp256k1 signature `s` value. + pub s: U256, +} + +impl SetCodeAuthorization { + fn from_alloy(value: &AlloyAuthorizationListItem) -> Self { + SetCodeAuthorization { + chain_id: alloy_u256_to_casper(*value.chain_id()), + address: alloy_address_to_address(*value.address()), + nonce: value.nonce(), + y_parity: value.y_parity(), + r: alloy_u256_to_casper(value.r()), + s: alloy_u256_to_casper(value.s()), + } + } + + fn to_alloy(&self) -> AlloyAuthorizationListItem { + AlloyAuthorizationListItem::new_unchecked( + AlloyAuthorization { + chain_id: casper_u256_to_alloy(self.chain_id), + address: to_alloy_address(self.address), + nonce: self.nonce, + }, + self.y_parity, + casper_u256_to_alloy(self.r), + casper_u256_to_alloy(self.s), + ) + } +} + +impl ToBytes for SetCodeAuthorization { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.chain_id.serialized_length() + + self.address.serialized_length() + + self.nonce.serialized_length() + + self.y_parity.serialized_length() + + self.r.serialized_length() + + self.s.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.chain_id.write_bytes(writer)?; + self.address.write_bytes(writer)?; + self.nonce.write_bytes(writer)?; + self.y_parity.write_bytes(writer)?; + self.r.write_bytes(writer)?; + self.s.write_bytes(writer) + } +} + +impl FromBytes for SetCodeAuthorization { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (chain_id, remainder) = U256::from_bytes(bytes)?; + let (address, remainder) = Address::from_bytes(remainder)?; + let (nonce, remainder) = u64::from_bytes(remainder)?; + let (y_parity, remainder) = u8::from_bytes(remainder)?; + let (r, remainder) = U256::from_bytes(remainder)?; + let (s, remainder) = U256::from_bytes(remainder)?; + Ok(( + SetCodeAuthorization { + chain_id, + address, + nonce, + y_parity, + r, + s, + }, + remainder, + )) + } +} + +/// Errors returned while decoding or validating EVM transactions. +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub enum TransactionError { + /// 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 { + /// Transaction gas price. + gas_price: u128, + /// Active block base fee. + base_fee: u128, + }, + /// The EIP-1559 maximum fee per gas is lower than the active block base fee. + MaxFeePerGasBelowBaseFee { + /// Transaction maximum fee per gas. + max_fee_per_gas: u128, + /// Active block base fee. + base_fee: u128, + }, + /// The EIP-1559 maximum priority fee per gas must be zero because Casper + /// does not prioritize transactions based on transaction gas parameters. + NonZeroMaxPriorityFeePerGas { + /// Transaction maximum priority fee per gas. + max_priority_fee_per_gas: u128, + }, + /// The transaction gas limit exceeds the configured EVM block gas limit. + GasLimitExceedsBlockGasLimit { + /// Transaction gas limit. + gas_limit: u64, + /// Configured EVM block gas limit. + block_gas_limit: u64, + }, + /// The transaction nonce does not match the account nonce in global state. + InvalidNonce { + /// Expected account nonce. + expected: u64, + /// Transaction nonce. + actual: u64, + }, + /// The transaction does not contain an EVM approval. + MissingApproval, + /// The transaction contains more than one approval. + MultipleApprovals, + /// The approval is not a secp256k1 signature and public key. + NonSecp256k1Approval, + /// The approval signature could not be recovered against the stored payload. + InvalidApprovalSignature, + /// The recovered signer address does not match the stored EVM sender. + SenderMismatch, + /// The reconstructed Ethereum transaction hash does not match the stored hash. + HashMismatch, + /// The sender address could not be recovered from the signature. + SenderRecovery(String), + /// Reconstructing the signed envelope produced metadata different from this transaction. + InconsistentEnvelope, +} + +impl Display for TransactionError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + match self { + TransactionError::Decode(error) => { + write!(formatter, "EVM transaction decode error: {error}") + } + TransactionError::Disabled => formatter.write_str("EVM transactions are disabled"), + TransactionError::UnsupportedTransactionType(kind) => { + write!(formatter, "unsupported EVM transaction type: {kind}") + } + TransactionError::UnsupportedAccessList => { + formatter.write_str("unsupported EVM transaction access list") + } + TransactionError::UnexpectedAuthorizationList => { + formatter.write_str("unexpected EVM set-code authorization list") + } + TransactionError::EmptyAuthorizationList => { + formatter.write_str("missing EVM set-code authorization list") + } + TransactionError::MissingSetCodeTarget => { + formatter.write_str("missing EVM set-code transaction target") + } + TransactionError::MissingChainId => formatter.write_str("missing EVM chain ID"), + TransactionError::MissingGasPrice => formatter.write_str("missing EVM gas price"), + TransactionError::MissingTransactionLane => { + formatter.write_str("missing EVM transaction lane") + } + TransactionError::ChainIdMismatch { expected, actual } => { + write!( + formatter, + "EVM chain ID mismatch: expected {expected}, got {actual}" + ) + } + TransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee, + } => { + write!( + formatter, + "EVM gas price {gas_price} is below base fee {base_fee}" + ) + } + TransactionError::MaxFeePerGasBelowBaseFee { + max_fee_per_gas, + base_fee, + } => { + write!( + formatter, + "EVM max fee per gas {max_fee_per_gas} is below base fee {base_fee}" + ) + } + TransactionError::NonZeroMaxPriorityFeePerGas { + max_priority_fee_per_gas, + } => { + write!( + formatter, + "EVM max priority fee per gas {max_priority_fee_per_gas} must be zero" + ) + } + TransactionError::GasLimitExceedsBlockGasLimit { + gas_limit, + block_gas_limit, + } => { + write!( + formatter, + "EVM gas limit {gas_limit} exceeds block gas limit {block_gas_limit}" + ) + } + TransactionError::InvalidNonce { expected, actual } => { + write!( + formatter, + "EVM transaction nonce {actual} does not match account nonce {expected}" + ) + } + TransactionError::MissingApproval => formatter.write_str("missing EVM approval"), + TransactionError::MultipleApprovals => formatter.write_str("multiple EVM approvals"), + TransactionError::NonSecp256k1Approval => { + formatter.write_str("EVM approval must use secp256k1") + } + TransactionError::InvalidApprovalSignature => { + formatter.write_str("invalid EVM approval signature") + } + TransactionError::SenderMismatch => { + formatter.write_str("EVM approval signer does not match transaction sender") + } + TransactionError::HashMismatch => { + formatter.write_str("EVM transaction hash does not match approval") + } + TransactionError::SenderRecovery(error) => { + write!(formatter, "EVM transaction sender recovery error: {error}") + } + TransactionError::InconsistentEnvelope => { + formatter.write_str("EVM transaction fields do not match signed envelope") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TransactionError {} + +/// An unsigned Ethereum transaction payload plus one Ethereum-style Casper approval. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +pub struct Transaction { + timestamp: Timestamp, + ttl: TimeDiff, + hash: TransactionHash, + from: Address, + kind: TransactionKind, + 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, + approvals: BTreeSet, +} + +#[cfg(any(feature = "std", test))] +#[derive(Serialize)] +struct TransactionSerHelper<'a> { + timestamp: Timestamp, + ttl: TimeDiff, + hash: TransactionHash, + from: Address, + kind: TransactionKind, + to: Option
, + nonce: u64, + gas_limit: u64, + gas_price: Option, + max_fee_per_gas: u128, + max_priority_fee_per_gas: Option, + value: U256, + input: &'a Vec, + chain_id: Option, + authorization_list: &'a Vec, + approvals: &'a BTreeSet, +} + +#[cfg(any(feature = "std", test))] +#[derive(Deserialize)] +struct TransactionDeserHelper { + timestamp: Timestamp, + ttl: TimeDiff, + hash: TransactionHash, + from: Address, + kind: TransactionKind, + to: Option
, + nonce: u64, + gas_limit: u64, + gas_price: Option, + max_fee_per_gas: u128, + max_priority_fee_per_gas: Option, + value: U256, + input: Vec, + chain_id: Option, + authorization_list: Vec, + approvals: BTreeSet, +} + +#[cfg(any(feature = "std", test))] +impl Serialize for Transaction { + fn serialize(&self, serializer: S) -> Result { + TransactionSerHelper { + timestamp: self.timestamp, + ttl: self.ttl, + hash: self.hash, + from: self.from, + kind: self.kind, + to: self.to, + nonce: self.nonce, + gas_limit: self.gas_limit, + gas_price: self.gas_price, + max_fee_per_gas: self.max_fee_per_gas, + max_priority_fee_per_gas: self.max_priority_fee_per_gas, + value: self.value, + input: &self.input, + chain_id: self.chain_id, + authorization_list: &self.authorization_list, + approvals: &self.approvals, + } + .serialize(serializer) + } +} + +#[cfg(any(feature = "std", test))] +impl<'de> Deserialize<'de> for Transaction { + fn deserialize>(deserializer: D) -> Result { + let helper = TransactionDeserHelper::deserialize(deserializer)?; + let transaction = Transaction { + timestamp: helper.timestamp, + ttl: helper.ttl, + hash: helper.hash, + from: helper.from, + kind: helper.kind, + to: helper.to, + nonce: helper.nonce, + gas_limit: helper.gas_limit, + gas_price: helper.gas_price, + max_fee_per_gas: helper.max_fee_per_gas, + max_priority_fee_per_gas: helper.max_priority_fee_per_gas, + value: helper.value, + input: helper.input, + chain_id: helper.chain_id, + authorization_list: helper.authorization_list, + approvals: helper.approvals, + }; + transaction.verify().map_err(de::Error::custom)?; + Ok(transaction) + } +} + +impl Transaction { + /// Constructs an unsigned EVM call transaction for speculative execution. + /// + /// This is intended for read-only `eth_call` style execution through the + /// node's speculative execution path, so it intentionally carries no + /// approvals and should not be accepted as a network transaction. + /// The chain ID and gas price are still part of the marker payload so the + /// node can enforce EVM configuration compliance before execution. + #[allow(clippy::too_many_arguments)] + pub fn new_unsigned_call( + timestamp: Timestamp, + ttl: TimeDiff, + chain_id: u64, + from: Address, + to: Option
, + value: U256, + input: Vec, + gas_limit: u64, + gas_price: u128, + ) -> Self { + let mut transaction = Transaction { + timestamp, + ttl, + hash: TransactionHash::default(), + from, + kind: TransactionKind::Legacy, + to, + nonce: 0, + gas_limit, + gas_price: Some(gas_price), + max_fee_per_gas: 0, + max_priority_fee_per_gas: None, + value, + input, + chain_id: Some(chain_id), + authorization_list: Vec::new(), + approvals: BTreeSet::new(), + }; + transaction.hash = transaction.unsigned_call_hash(); + transaction + } + + fn unsigned_call_hash(&self) -> TransactionHash { + let mut bytes = Vec::new(); + bytes.extend_from_slice(b"casper-evm-call"); + self.timestamp + .write_bytes(&mut bytes) + .expect("timestamp should serialize"); + self.ttl + .write_bytes(&mut bytes) + .expect("ttl should serialize"); + self.from + .write_bytes(&mut bytes) + .expect("from address should serialize"); + self.to + .write_bytes(&mut bytes) + .expect("to address should serialize"); + self.value + .write_bytes(&mut bytes) + .expect("value should serialize"); + Bytes::from(self.input.clone()) + .write_bytes(&mut bytes) + .expect("input should serialize"); + self.gas_limit + .write_bytes(&mut bytes) + .expect("gas limit should serialize"); + self.gas_price + .write_bytes(&mut bytes) + .expect("gas price should serialize"); + self.chain_id + .write_bytes(&mut bytes) + .expect("chain ID should serialize"); + TransactionHash::new(Digest::hash(bytes)) + } + + /// Returns `true` if this is an unsigned read-only call transaction. + pub fn is_unsigned_call(&self) -> bool { + self.approvals.is_empty() + && self.hash == self.unsigned_call_hash() + && self.kind == TransactionKind::Legacy + && self.nonce == 0 + && self.gas_price.is_some() + && self.max_fee_per_gas == 0 + && self.max_priority_fee_per_gas.is_none() + && self.chain_id.is_some() + && self.authorization_list.is_empty() + } + + /// Decodes a signed Ethereum RLP transaction into an unsigned payload plus approval. + pub fn from_signed_rlp( + raw_signed_rlp: Vec, + timestamp: Timestamp, + ttl: TimeDiff, + ) -> Result { + if matches!(raw_signed_rlp.first(), Some(&EIP4844_TRANSACTION_TYPE_ID)) { + // EIP-4844 is proto-danksharding/blob transaction support. It + // adds blob-carrying transactions with fields like + // `max_fee_per_blob_gas` and `blob_versioned_hashes`, plus blob + // gas accounting, blob base fee validation, KZG + // commitments/proofs, and separate network representations for + // blob sidecars. Our current transaction type and executor block + // context only model normal EVM call/create execution, not blob + // sidecars, blob fee markets, or block/header blob accounting. + return Err(TransactionError::UnsupportedTransactionType( + raw_signed_rlp[0], + )); + } + + let mut encoded = raw_signed_rlp.as_slice(); + let envelope = TxEnvelope::decode_2718(&mut encoded) + .map_err(|error| TransactionError::Decode(format!("{error:?}")))?; + if !encoded.is_empty() { + return Err(TransactionError::Decode( + "trailing bytes after transaction envelope".to_string(), + )); + } + if envelope + .access_list() + .is_some_and(|access_list| !access_list.is_empty()) + { + return Err(TransactionError::UnsupportedAccessList); + } + + let kind = if envelope.is_legacy() { + TransactionKind::Legacy + } else if envelope.is_eip2930() { + TransactionKind::Eip2930 + } else if envelope.is_eip1559() { + TransactionKind::Eip1559 + } else if envelope.is_eip7702() { + TransactionKind::Eip7702 + } else { + return Err(TransactionError::UnsupportedTransactionType( + envelope.tx_type() as u8, + )); + }; + 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 = approval_from_alloy_signature(envelope.signature(), &signature_hash)?; + let mut approvals = BTreeSet::new(); + approvals.insert(approval); + + let from = envelope + .recover_signer() + .map_err(|error| TransactionError::SenderRecovery(format!("{error:?}")))?; + let authorization_list = match envelope.as_eip7702() { + Some(transaction) => { + if transaction.tx().authorization_list.is_empty() { + // Keep raw decode errors precise before constructing a + // transaction that `verify` would reject anyway. + return Err(TransactionError::EmptyAuthorizationList); + } + transaction + .tx() + .authorization_list + .iter() + .map(SetCodeAuthorization::from_alloy) + .collect() + } + None => Vec::new(), + }; + Ok(Transaction { + timestamp, + ttl, + 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, + approvals, + }) + } + + /// Reconstructs the signed Ethereum envelope and validates sender/hash consistency. + pub fn verify(&self) -> Result<(), TransactionError> { + let signed = self.signed_envelope()?; + if b256_to_transaction_hash(*signed.tx_hash()) != self.hash { + return Err(TransactionError::HashMismatch); + } + let recovered = signed + .recover_signer() + .map_err(|error| TransactionError::SenderRecovery(format!("{error:?}")))?; + if alloy_address_to_address(recovered) != self.from { + return Err(TransactionError::SenderMismatch); + } + Ok(()) + } + + /// Signs the unsigned Ethereum payload with one secp256k1 approval. + /// + /// This recomputes the recovered EVM sender and Ethereum signed + /// transaction hash from the new signature. + pub fn sign(&mut self, secret_key: &SecretKey) { + self.try_sign(secret_key) + .expect("EVM transactions must be signed with a valid secp256k1 key") + } + + /// Attempts to sign the unsigned Ethereum payload with one secp256k1 approval. + pub fn try_sign(&mut self, secret_key: &SecretKey) -> Result<(), TransactionError> { + let SecretKey::Secp256k1(signing_key) = secret_key else { + return Err(TransactionError::NonSecp256k1Approval); + }; + let unsigned = self.unsigned_transaction()?; + let signature_hash = unsigned.signature_hash(); + let (signature, recovery_id) = signing_key + .sign_prehash_recoverable(signature_hash.as_slice()) + .map_err(|_| TransactionError::InvalidApprovalSignature)?; + let mut signature_bytes = [0u8; Signature::SECP256K1_LENGTH]; + signature_bytes.copy_from_slice(signature.to_bytes().as_slice()); + let signature = Signature::secp256k1(signature_bytes) + .map_err(|_| TransactionError::InvalidApprovalSignature)?; + let signer = PublicKey::from(secret_key); + let mut approvals = BTreeSet::new(); + approvals.insert(Approval::new(signer, signature)); + + let recovered_key = + recover_verifying_key(&signature_hash, &signature_bytes, recovery_id.is_y_odd())?; + let alloy_signature = + AlloySignature::from_bytes_and_parity(&signature_bytes, recovery_id.is_y_odd()); + let signed = unsigned.into_envelope(alloy_signature); + + self.approvals = approvals; + self.from = evm_address_from_verifying_key(&recovered_key); + self.hash = b256_to_transaction_hash(*signed.tx_hash()); + self.verify() + } + + /// Returns the raw signed Ethereum RLP bytes reconstructed from the approval. + pub fn signed_rlp(&self) -> Result, TransactionError> { + Ok(self.signed_envelope()?.encoded_2718()) + } + + /// Returns the raw signed Ethereum RLP bytes reconstructed from the approval. + pub fn raw_signed_rlp(&self) -> Result, TransactionError> { + self.signed_rlp() + } + + /// Returns the Ethereum signing hash of the unsigned payload. + pub fn signature_hash(&self) -> Result { + Ok(b256_to_hash(self.unsigned_transaction()?.signature_hash())) + } + + /// Returns the bytes Ethereum signs for this unsigned payload. + pub fn signing_payload(&self) -> Result, TransactionError> { + Ok(self.unsigned_transaction()?.encoded_for_signing()) + } + + /// Returns the approvals attached to this transaction. + pub fn approvals(&self) -> &BTreeSet { + &self.approvals + } + + /// Returns the single public key that signed this EVM transaction. + pub fn signer(&self) -> Result<&PublicKey, TransactionError> { + Ok(self.single_approval()?.signer()) + } + + /// Returns this transaction with a replacement approval set. + /// + /// The stored Ethereum transaction hash is intentionally left unchanged; + /// [`Transaction::verify`] rejects replacement approvals that do not + /// reconstruct the same signed Ethereum transaction. + pub fn with_approvals(mut self, approvals: BTreeSet) -> Self { + self.approvals = approvals; + self + } + + /// Returns the Casper envelope timestamp. + pub fn timestamp(&self) -> Timestamp { + self.timestamp + } + + /// Returns the Casper envelope time to live. + pub fn ttl(&self) -> TimeDiff { + self.ttl + } + + /// Returns the Ethereum transaction hash. + pub fn hash(&self) -> TransactionHash { + self.hash + } + + /// Returns the recovered Ethereum sender address. + pub fn from(&self) -> Address { + self.from + } + + /// Returns the transaction envelope kind. + pub fn kind(&self) -> TransactionKind { + self.kind + } + + /// Returns the recipient address, or `None` for contract creation. + pub fn to(&self) -> Option
{ + self.to + } + + /// Returns the account nonce. + pub fn nonce(&self) -> u64 { + self.nonce + } + + /// Returns the transaction gas limit. + pub fn gas_limit(&self) -> u64 { + self.gas_limit + } + + /// Returns the legacy gas price, if available. + pub fn gas_price(&self) -> Option { + self.gas_price + } + + /// Returns the maximum fee per gas. + /// + /// 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 { + TransactionKind::Legacy | TransactionKind::Eip2930 => { + self.gas_price.unwrap_or(self.max_fee_per_gas) + } + TransactionKind::Eip1559 | TransactionKind::Eip7702 => { + let max_priority_fee_per_gas = self.max_priority_fee_per_gas.unwrap_or(0); + let priority_fee = self.max_fee_per_gas.saturating_sub(u128::from(base_fee)); + if priority_fee > max_priority_fee_per_gas { + 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(TransactionError::SenderMismatch); + } + Ok(unsigned.into_envelope(signature)) + } + + fn validate_authorization_list(&self) -> Result<(), TransactionError> { + match self.kind { + TransactionKind::Eip7702 => { + if self.authorization_list.is_empty() { + return Err(TransactionError::EmptyAuthorizationList); + } + if self.to.is_none() { + return Err(TransactionError::MissingSetCodeTarget); + } + } + TransactionKind::Legacy | TransactionKind::Eip2930 | TransactionKind::Eip1559 => { + if !self.authorization_list.is_empty() { + return Err(TransactionError::UnexpectedAuthorizationList); + } + } + } + Ok(()) + } + + fn unsigned_transaction(&self) -> Result { + self.validate_authorization_list()?; + let to = match self.to { + Some(address) => AlloyTxKind::Call(to_alloy_address(address)), + None => AlloyTxKind::Create, + }; + let value = casper_u256_to_alloy(self.value); + let input = AlloyBytes::from(self.input.clone()); + match self.kind { + TransactionKind::Legacy => Ok(TypedTransaction::Legacy(TxLegacy { + chain_id: self.chain_id, + nonce: self.nonce, + gas_price: self.gas_price.ok_or(TransactionError::MissingGasPrice)?, + gas_limit: self.gas_limit, + to, + value, + input, + })), + TransactionKind::Eip2930 => Ok(TypedTransaction::Eip2930(TxEip2930 { + chain_id: self.chain_id.ok_or(TransactionError::MissingChainId)?, + nonce: self.nonce, + gas_price: self.gas_price.ok_or(TransactionError::MissingGasPrice)?, + gas_limit: self.gas_limit, + to, + value, + access_list: AccessList::default(), + input, + })), + TransactionKind::Eip1559 => Ok(TypedTransaction::Eip1559(TxEip1559 { + chain_id: self.chain_id.ok_or(TransactionError::MissingChainId)?, + nonce: self.nonce, + gas_limit: self.gas_limit, + max_fee_per_gas: self.max_fee_per_gas, + max_priority_fee_per_gas: self.max_priority_fee_per_gas.unwrap_or(0), + to, + value, + access_list: AccessList::default(), + input, + })), + TransactionKind::Eip7702 => { + let address = self.to.expect("EIP-7702 target validated above"); + Ok(TypedTransaction::Eip7702(TxEip7702 { + chain_id: self.chain_id.ok_or(TransactionError::MissingChainId)?, + nonce: self.nonce, + gas_limit: self.gas_limit, + max_fee_per_gas: self.max_fee_per_gas, + max_priority_fee_per_gas: self.max_priority_fee_per_gas.unwrap_or(0), + to: to_alloy_address(address), + value, + access_list: AccessList::default(), + authorization_list: self + .authorization_list + .iter() + .map(SetCodeAuthorization::to_alloy) + .collect(), + input, + })) + } + } + } + + fn approval_signature( + &self, + signature_hash: &B256, + ) -> Result<(AlloySignature, Address), TransactionError> { + let approval = self.single_approval()?; + let raw_signature = secp256k1_signature_bytes(approval)?; + let expected_signer = approval.signer(); + for y_parity in [false, true] { + let alloy_signature = AlloySignature::from_bytes_and_parity(&raw_signature, y_parity); + let recovered_key = recover_verifying_key(signature_hash, &raw_signature, y_parity)?; + let recovered_public_key = public_key_from_verifying_key(&recovered_key)?; + if &recovered_public_key == expected_signer { + return Ok(( + alloy_signature, + evm_address_from_verifying_key(&recovered_key), + )); + } + } + Err(TransactionError::InvalidApprovalSignature) + } + + fn single_approval(&self) -> Result<&Approval, TransactionError> { + let mut approvals = self.approvals.iter(); + let approval = approvals.next().ok_or(TransactionError::MissingApproval)?; + if approvals.next().is_some() { + return Err(TransactionError::MultipleApprovals); + } + Ok(approval) + } +} + +impl Display for Transaction { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!( + formatter, + "EVM transaction {} from {}", + self.hash.to_hex_string(), + self.from + ) + } +} + +impl ToBytes for Transaction { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + self.timestamp.serialized_length() + + self.ttl.serialized_length() + + self.hash.serialized_length() + + self.from.serialized_length() + + self.kind.serialized_length() + + self.to.serialized_length() + + self.nonce.serialized_length() + + self.gas_limit.serialized_length() + + self.gas_price.serialized_length() + + self.max_fee_per_gas.serialized_length() + + self.max_priority_fee_per_gas.serialized_length() + + self.value.serialized_length() + + Bytes::from(self.input.clone()).serialized_length() + + self.chain_id.serialized_length() + + self.authorization_list.serialized_length() + + self.approvals.serialized_length() + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.timestamp.write_bytes(writer)?; + self.ttl.write_bytes(writer)?; + self.hash.write_bytes(writer)?; + self.from.write_bytes(writer)?; + self.kind.write_bytes(writer)?; + self.to.write_bytes(writer)?; + self.nonce.write_bytes(writer)?; + self.gas_limit.write_bytes(writer)?; + self.gas_price.write_bytes(writer)?; + self.max_fee_per_gas.write_bytes(writer)?; + self.max_priority_fee_per_gas.write_bytes(writer)?; + self.value.write_bytes(writer)?; + Bytes::from(self.input.clone()).write_bytes(writer)?; + self.chain_id.write_bytes(writer)?; + self.authorization_list.write_bytes(writer)?; + self.approvals.write_bytes(writer) + } +} + +impl FromBytes for Transaction { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (timestamp, remainder) = Timestamp::from_bytes(bytes)?; + let (ttl, remainder) = TimeDiff::from_bytes(remainder)?; + let (hash, remainder) = TransactionHash::from_bytes(remainder)?; + let (from, remainder) = Address::from_bytes(remainder)?; + let (kind, remainder) = TransactionKind::from_bytes(remainder)?; + let (to, remainder) = Option::
::from_bytes(remainder)?; + let (nonce, remainder) = u64::from_bytes(remainder)?; + let (gas_limit, remainder) = u64::from_bytes(remainder)?; + let (gas_price, remainder) = Option::::from_bytes(remainder)?; + let (max_fee_per_gas, remainder) = u128::from_bytes(remainder)?; + let (max_priority_fee_per_gas, remainder) = Option::::from_bytes(remainder)?; + let (value, remainder) = U256::from_bytes(remainder)?; + let (input, remainder) = Bytes::from_bytes(remainder)?; + let (chain_id, remainder) = Option::::from_bytes(remainder)?; + let (authorization_list, remainder) = Vec::::from_bytes(remainder)?; + let (approvals, remainder) = BTreeSet::::from_bytes(remainder)?; + let transaction = Transaction { + timestamp, + ttl, + hash, + from, + kind, + to, + nonce, + gas_limit, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + value, + input: input.into(), + chain_id, + authorization_list, + approvals, + }; + if !transaction.is_unsigned_call() { + transaction + .verify() + .map_err(|_| bytesrepr::Error::Formatting)?; + } + Ok((transaction, remainder)) + } +} + +fn approval_from_alloy_signature( + signature: &AlloySignature, + signature_hash: &B256, +) -> Result { + let raw_signature = signature.as_rsy(); + let mut signature_bytes = [0u8; Signature::SECP256K1_LENGTH]; + signature_bytes.copy_from_slice(&raw_signature[..Signature::SECP256K1_LENGTH]); + let recovered_key = recover_verifying_key(signature_hash, &signature_bytes, signature.v())?; + let signer = public_key_from_verifying_key(&recovered_key)?; + let signature = Signature::secp256k1(signature_bytes) + .map_err(|_| TransactionError::InvalidApprovalSignature)?; + Ok(Approval::new(signer, signature)) +} + +fn secp256k1_signature_bytes(approval: &Approval) -> Result<[u8; 64], TransactionError> { + if !matches!(approval.signer(), PublicKey::Secp256k1(_)) + || !matches!(approval.signature(), Signature::Secp256k1(_)) + { + return Err(TransactionError::NonSecp256k1Approval); + } + let signature_bytes = Vec::::from(approval.signature()); + signature_bytes + .try_into() + .map_err(|_| TransactionError::InvalidApprovalSignature) +} + +fn recover_verifying_key( + signature_hash: &B256, + signature_bytes: &[u8; 64], + y_parity: bool, +) -> Result { + let signature = K256Signature::try_from(signature_bytes.as_slice()) + .map_err(|_| TransactionError::InvalidApprovalSignature)?; + let recovery_id = RecoveryId::new(y_parity, false); + VerifyingKey::recover_from_prehash(signature_hash.as_slice(), &signature, recovery_id) + .map_err(|_| TransactionError::InvalidApprovalSignature) +} + +fn public_key_from_verifying_key(key: &VerifyingKey) -> Result { + PublicKey::secp256k1_from_bytes(key.to_encoded_point(true).as_ref()) + .map_err(|_| TransactionError::InvalidApprovalSignature) +} + +fn evm_address_from_verifying_key(key: &VerifyingKey) -> Address { + let encoded = key.to_encoded_point(false); + let bytes = encoded.as_bytes(); + let digest = keccak256(&bytes[1..]); + let mut address = [0u8; super::ADDRESS_LENGTH]; + address.copy_from_slice(&digest.as_slice()[HASH_LENGTH - super::ADDRESS_LENGTH..]); + Address::new(address) +} + +fn to_alloy_address(address: Address) -> AlloyAddress { + AlloyAddress::from(address.value()) +} + +fn alloy_address_to_address(address: AlloyAddress) -> Address { + let mut bytes = [0u8; super::ADDRESS_LENGTH]; + bytes.copy_from_slice(address.as_slice()); + Address::new(bytes) +} + +fn b256_to_hash(hash: B256) -> Hash { + Hash::new(hash.0) +} + +fn b256_to_transaction_hash(hash: B256) -> TransactionHash { + TransactionHash::from_raw(hash.0) +} + +fn alloy_u256_to_casper(value: AlloyU256) -> U256 { + U256::from_big_endian(&value.to_be_bytes::<32>()) +} + +fn casper_u256_to_alloy(value: U256) -> AlloyU256 { + let mut bytes = [0u8; 32]; + value.to_big_endian(&mut bytes); + AlloyU256::from_be_slice(&bytes) +} + +#[cfg(test)] +mod tests { + use alloy_consensus::crypto::secp256k1; + + use super::*; + + const SIGNING_SECRET: [u8; 32] = [7; 32]; + const AUTHORIZATION_SECRET: [u8; 32] = [8; 32]; + + #[test] + fn eip7702_transaction_serde_roundtrips_authorization_list() { + let transaction = signed_eip7702_transaction(); + + let serialized = serde_json::to_string(&transaction).expect("transaction should serialize"); + assert!(serialized.contains("authorization_list")); + let deserialized: Transaction = + serde_json::from_str(&serialized).expect("transaction should deserialize"); + + assert_eq!(deserialized, transaction); + assert_eq!( + deserialized.authorization_list(), + transaction.authorization_list() + ); + } + + #[test] + fn non_eip7702_transaction_serde_rejects_authorization_list() { + let mut transaction = signed_legacy_transaction(); + transaction + .authorization_list + .push(set_code_authorization()); + let serialized = serde_json::to_string(&transaction).expect("transaction should serialize"); + + let error = + serde_json::from_str::(&serialized).expect_err("transaction should fail"); + + assert!(error + .to_string() + .contains("unexpected EVM set-code authorization list")); + } + + #[test] + fn unsigned_call_transaction_bytesrepr_roundtrips_without_approvals() { + let transaction = Transaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(300), + 7, + Address::new([1; crate::evm::ADDRESS_LENGTH]), + Some(Address::new([2; crate::evm::ADDRESS_LENGTH])), + U256::from(3), + vec![0xde, 0xad], + 1_000, + 1, + ); + + assert!(transaction.approvals().is_empty()); + assert!(transaction.is_unsigned_call()); + assert!(matches!( + transaction.verify(), + Err(TransactionError::MissingApproval) + )); + bytesrepr::test_serialization_roundtrip(&transaction); + } + + #[test] + fn non_marker_unsigned_transaction_bytesrepr_is_rejected() { + let mut transaction = Transaction::new_unsigned_call( + Timestamp::zero(), + TimeDiff::from_seconds(300), + 7, + Address::new([1; crate::evm::ADDRESS_LENGTH]), + Some(Address::new([2; crate::evm::ADDRESS_LENGTH])), + U256::from(3), + vec![0xde, 0xad], + 1_000, + 1, + ); + transaction.gas_price = Some(2); + + assert!(transaction.approvals().is_empty()); + assert!(!transaction.is_unsigned_call()); + assert!(Transaction::from_bytes( + &transaction + .to_bytes() + .expect("transaction should serialize") + ) + .is_err()); + } + + fn signed_legacy_transaction() -> Transaction { + let tx = TxLegacy { + chain_id: Some(7), + nonce: 3, + gas_price: 1, + gas_limit: 21_000, + to: AlloyTxKind::Call(AlloyAddress::from([4; 20])), + value: AlloyU256::ZERO, + input: AlloyBytes::default(), + }; + let transaction_signature = + secp256k1::sign_message(B256::from(SIGNING_SECRET), tx.signature_hash()) + .expect("transaction signing should succeed"); + let envelope: TxEnvelope = tx.into_signed(transaction_signature).into(); + + Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") + } + + fn signed_eip7702_transaction() -> Transaction { + let authorization = AlloyAuthorization { + chain_id: AlloyU256::from(7), + address: AlloyAddress::from([9; 20]), + nonce: 4, + }; + let authorization_signature = secp256k1::sign_message( + B256::from(AUTHORIZATION_SECRET), + authorization.signature_hash(), + ) + .expect("authorization signing should succeed"); + let tx = TxEip7702 { + chain_id: 7, + nonce: 3, + gas_limit: 70_000, + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 0, + to: AlloyAddress::from([4; 20]), + value: AlloyU256::from(987u64), + access_list: AccessList::default(), + authorization_list: vec![authorization.into_signed(authorization_signature)], + input: AlloyBytes::from(vec![0xde, 0xad]), + }; + let transaction_signature = + secp256k1::sign_message(B256::from(SIGNING_SECRET), tx.signature_hash()) + .expect("transaction signing should succeed"); + let envelope: TxEnvelope = tx.into_signed(transaction_signature).into(); + + Transaction::from_signed_rlp( + envelope.encoded_2718(), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ) + .expect("transaction should decode") + } + + fn set_code_authorization() -> SetCodeAuthorization { + let authorization = AlloyAuthorization { + chain_id: AlloyU256::from(7), + address: AlloyAddress::from([9; 20]), + nonce: 4, + }; + let signature = secp256k1::sign_message( + B256::from(AUTHORIZATION_SECRET), + authorization.signature_hash(), + ) + .expect("authorization signing should succeed"); + SetCodeAuthorization::from_alloy(&authorization.into_signed(signature)) + } +} diff --git a/types/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..f7a9d77157 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::{ @@ -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| evm::EvmAddr::Account(evm::Address::new(bytes))), + u8_slice_32().prop_map(|bytes| evm::EvmAddr::ByteCode(evm::Hash::new(bytes))), + (prop::array::uniform20(any::()), u256_arb()).prop_map(|(address, slot)| { + evm::EvmAddr::Storage(evm::StorageAddr::new(evm::Address::new(address), slot)) + }), + prop::array::uniform20(any::()) + .prop_map(|bytes| evm::EvmAddr::Nonce(evm::Address::new(bytes))), + prop::array::uniform20(any::()) + .prop_map(|bytes| evm::EvmAddr::CodeHash(evm::Address::new(bytes))), + ] +} + 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..0a3fbfca9b 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; @@ -194,9 +195,9 @@ pub use timestamp::{TimeDiff, Timestamp}; #[cfg(any(feature = "std", test))] pub use transaction::{calculate_lane_id_for_deploy, calculate_transaction_lane, GasLimited}; pub use transaction::{ - AddressableEntityIdentifier, Approval, ApprovalsHash, Deploy, DeployDecodeFromJsonError, - DeployError, DeployExcessiveSizeError, DeployHash, DeployHeader, DeployId, - ExecutableDeployItem, ExecutableDeployItemIdentifier, ExecutionInfo, InitiatorAddr, + AddressableEntityIdentifier, Approval, ApprovalsHash, Deploy, DeployCategory, + DeployDecodeFromJsonError, DeployError, DeployExcessiveSizeError, DeployHash, DeployHeader, + DeployId, ExecutableDeployItem, ExecutableDeployItemIdentifier, ExecutionInfo, InitiatorAddr, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, NamedArg, PackageIdentifier, PricingMode, PricingModeError, RuntimeArgs, Transaction, TransactionArgs, TransactionEntryPoint, TransactionHash, TransactionId, TransactionInvocationTarget, diff --git a/types/src/package.rs b/types/src/package.rs index c6286eeff5..c5617552a2 100644 --- a/types/src/package.rs +++ b/types/src/package.rs @@ -562,13 +562,14 @@ impl From<&PublicKey> for PackageHash { } /// A enum to determine the lock status of the package. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] pub enum PackageStatus { /// The package is locked and cannot be versioned. Locked, /// The package is unlocked and can be versioned. + #[default] Unlocked, } @@ -583,12 +584,6 @@ impl PackageStatus { } } -impl Default for PackageStatus { - fn default() -> Self { - Self::Unlocked - } -} - impl ToBytes for PackageStatus { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut result = bytesrepr::allocate_buffer(self)?; diff --git a/types/src/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..c9db53cbfa 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -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, 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(evm::Transaction), } impl Transaction { @@ -160,11 +164,17 @@ impl Transaction { Transaction::V1(v1) } + /// EVM variant ctor. + pub fn from_evm(evm: evm::Transaction) -> 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.approvals().clone(), } } @@ -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) => ApprovalsHash::compute(txn.approvals())?, }; 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,39 @@ impl Transaction { }); TransactionId::new(TransactionHash::V1(txn_hash), approvals_hash) } + Transaction::Evm(txn) => { + let approvals_hash = + ApprovalsHash::compute(txn.approvals()).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 Casper initiator address, if this transaction has one. + pub fn initiator_addr(&self) -> Option { + match self { + Transaction::Deploy(deploy) => Some(InitiatorAddr::PublicKey(deploy.account().clone())), + Transaction::V1(txn) => Some(txn.initiator_addr().clone()), + Transaction::Evm(_) => None, + } + } + + /// 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 address of the initiator of the transaction. - pub fn initiator_addr(&self) -> InitiatorAddr { + /// Returns the native EVM transaction hash for an EVM transaction. + pub fn evm_hash(&self) -> Option { match self { - Transaction::Deploy(deploy) => InitiatorAddr::PublicKey(deploy.account().clone()), - Transaction::V1(txn) => txn.initiator_addr().clone(), + Transaction::Evm(txn) => Some(txn.hash()), + _ => None, } } @@ -287,6 +334,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.expired(current_instant), Transaction::V1(txn) => txn.expired(current_instant), + Transaction::Evm(txn) => txn.expired(current_instant), } } @@ -295,6 +343,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => deploy.header().expires(), Transaction::V1(txn) => txn.payload().expires(), + Transaction::Evm(txn) => txn.expires(), } } @@ -311,6 +360,11 @@ impl Transaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + Transaction::Evm(txn) => txn + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), } } @@ -325,6 +379,7 @@ impl Transaction { Transaction::V1(transaction_v1) => { Transaction::V1(transaction_v1.with_approvals(approvals)) } + Transaction::Evm(txn) => Transaction::Evm(txn), } } @@ -333,6 +388,15 @@ impl Transaction { match self { Transaction::Deploy(_) => None, Transaction::V1(v1) => Some(v1), + Transaction::Evm(_) => None, + } + } + + /// Get the wrapped EVM transaction. + pub fn as_evm(&self) -> Option<&evm::Transaction> { + match self { + Transaction::Evm(evm) => Some(evm), + _ => None, } } @@ -349,6 +413,11 @@ impl Transaction { .iter() .map(|approval| approval.signer().to_account_hash()) .collect(), + Transaction::Evm(txn) => txn + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), } } @@ -357,6 +426,7 @@ impl Transaction { match self { Transaction::Deploy(_) => true, Transaction::V1(_) => false, + Transaction::Evm(_) => false, } } @@ -412,6 +482,7 @@ impl Transaction { Err(err) => Err(err), } } + Transaction::Evm(txn) => Ok(Gas::new(txn.gas_limit())), } } @@ -442,6 +513,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 +581,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(evm::Transaction), } #[cfg(any(feature = "std", test))] @@ -519,9 +601,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 +612,7 @@ impl TryFrom for Transaction { )) }) } + TransactionJson::Evm(evm) => Ok(Transaction::Evm(evm)), } } } @@ -539,8 +622,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 +632,7 @@ impl TryFrom for TransactionJson { error )) }), + Transaction::Evm(evm) => Ok(TransactionJson::Evm(evm)), } } } @@ -583,6 +668,12 @@ impl From for Transaction { } } +impl From for Transaction { + fn from(txn: evm::Transaction) -> Self { + Self::Evm(txn) + } +} + impl ToBytes for Transaction { fn to_bytes(&self) -> Result, bytesrepr::Error> { let mut buffer = bytesrepr::allocate_buffer(self)?; @@ -595,6 +686,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 +700,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 +720,10 @@ impl FromBytes for Transaction { let (txn, remainder) = TransactionV1::from_bytes(remainder)?; Ok((Transaction::V1(txn), remainder)) } + EVM_TAG => { + let (txn, remainder) = evm::Transaction::from_bytes(remainder)?; + Ok((Transaction::Evm(txn), remainder)) + } _ => Err(bytesrepr::Error::Formatting), } } @@ -634,6 +734,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..4adb4b8709 100644 --- a/types/src/transaction/error.rs +++ b/types/src/transaction/error.rs @@ -1,4 +1,4 @@ -use crate::InvalidDeploy; +use crate::{evm, InvalidDeploy}; use core::fmt::{Display, Formatter}; #[cfg(feature = "datasize")] use datasize::DataSize; @@ -20,6 +20,8 @@ pub enum InvalidTransaction { Deploy(InvalidDeploy), /// V1 transactions. V1(InvalidTransactionV1), + /// EVM transactions. + Evm(evm::TransactionError), } impl From for InvalidTransaction { @@ -34,12 +36,19 @@ impl From for InvalidTransaction { } } +impl From for InvalidTransaction { + fn from(value: evm::TransactionError) -> Self { + Self::Evm(value) + } +} + #[cfg(feature = "std")] impl StdError for InvalidTransaction { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { InvalidTransaction::Deploy(deploy) => deploy.source(), InvalidTransaction::V1(v1) => v1.source(), + InvalidTransaction::Evm(evm) => Some(evm), } } } @@ -49,6 +58,7 @@ impl Display for InvalidTransaction { match self { InvalidTransaction::Deploy(inner) => Display::fmt(inner, f), InvalidTransaction::V1(inner) => Display::fmt(inner, f), + InvalidTransaction::Evm(inner) => Display::fmt(inner, f), } } } diff --git a/types/src/transaction/initiator_addr.rs b/types/src/transaction/initiator_addr.rs index b9e6c40046..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..ac30ce1ca0 100644 --- a/types/src/transaction/transaction_hash.rs +++ b/types/src/transaction/transaction_hash.rs @@ -14,11 +14,12 @@ use super::{DeployHash, TransactionV1Hash}; use crate::testing::TestRng; use crate::{ bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - Digest, + evm, Digest, }; const DEPLOY_TAG: u8 = 0; const V1_TAG: u8 = 1; +const EVM_TAG: u8 = 2; const TAG_LENGTH: u8 = 1; /// A versioned wrapper for a transaction hash or deploy hash. @@ -32,6 +33,8 @@ pub enum TransactionHash { /// A version 1 transaction hash. #[serde(rename = "Version1")] V1(TransactionV1Hash), + /// An EVM transaction hash. + Evm(evm::TransactionHash), } impl TransactionHash { @@ -42,6 +45,7 @@ impl TransactionHash { match self { TransactionHash::Deploy(deploy_hash) => *deploy_hash.inner(), TransactionHash::V1(transaction_hash) => *transaction_hash.inner(), + TransactionHash::Evm(transaction_hash) => *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(evm::TransactionHash::random(rng)), _ => panic!(), } } @@ -91,6 +96,18 @@ impl From<&TransactionV1Hash> for TransactionHash { } } +impl From for TransactionHash { + fn from(hash: evm::TransactionHash) -> Self { + Self::Evm(hash) + } +} + +impl From<&evm::TransactionHash> for TransactionHash { + fn from(hash: &evm::TransactionHash) -> Self { + Self::from(*hash) + } +} + impl Default for TransactionHash { fn default() -> Self { TransactionHash::V1(TransactionV1Hash::default()) @@ -102,6 +119,7 @@ impl Display for TransactionHash { match self { TransactionHash::Deploy(hash) => Display::fmt(hash, formatter), TransactionHash::V1(hash) => Display::fmt(hash, formatter), + TransactionHash::Evm(hash) => Display::fmt(hash, formatter), } } } @@ -111,6 +129,7 @@ impl AsRef<[u8]> for TransactionHash { match self { TransactionHash::Deploy(hash) => hash.as_ref(), TransactionHash::V1(hash) => hash.as_ref(), + TransactionHash::Evm(hash) => hash.as_ref(), } } } @@ -127,6 +146,7 @@ impl ToBytes for TransactionHash { + match self { TransactionHash::Deploy(hash) => hash.serialized_length(), TransactionHash::V1(hash) => hash.serialized_length(), + TransactionHash::Evm(hash) => hash.serialized_length(), } } @@ -140,6 +160,10 @@ impl ToBytes for TransactionHash { V1_TAG.write_bytes(writer)?; hash.write_bytes(writer) } + TransactionHash::Evm(hash) => { + EVM_TAG.write_bytes(writer)?; + hash.write_bytes(writer) + } } } } @@ -156,6 +180,10 @@ impl FromBytes for TransactionHash { let (hash, remainder) = TransactionV1Hash::from_bytes(remainder)?; Ok((TransactionHash::V1(hash), remainder)) } + EVM_TAG => { + let (hash, remainder) = evm::TransactionHash::from_bytes(remainder)?; + Ok((TransactionHash::Evm(hash), remainder)) + } _ => Err(bytesrepr::Error::Formatting), } } diff --git a/types/src/transaction/transaction_v1/arg_handling.rs b/types/src/transaction/transaction_v1/arg_handling.rs index b1c003ef90..87b5f15cce 100644 --- a/types/src/transaction/transaction_v1/arg_handling.rs +++ b/types/src/transaction/transaction_v1/arg_handling.rs @@ -97,6 +97,7 @@ pub(crate) fn new_transfer_args, T: Into>( TransferTarget::AccountHash(account_hash) => { args.insert(TRANSFER_ARG_TARGET, account_hash)? } + TransferTarget::EvmAddress(address) => args.insert(TRANSFER_ARG_TARGET, address)?, TransferTarget::URef(uref) => args.insert(TRANSFER_ARG_TARGET, uref)?, } TRANSFER_ARG_AMOUNT.insert(&mut args, amount.into())?; @@ -182,3 +183,30 @@ pub(crate) fn new_redelegate_args>( REDELEGATE_ARG_NEW_VALIDATOR.insert(&mut args, new_validator)?; Ok(args) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{evm, CLType}; + + #[test] + fn new_transfer_args_accepts_evm_address_target() { + let address = evm::Address::new([0x44; evm::ADDRESS_LENGTH]); + let args = new_transfer_args(U512::from(1), None, address, None) + .expect("EVM address transfer args should serialize"); + let target = args + .get(TRANSFER_ARG_TARGET) + .expect("target argument should exist"); + + assert_eq!( + target.cl_type(), + &CLType::ByteArray(evm::ADDRESS_LENGTH as u32) + ); + assert_eq!( + target + .to_t::() + .expect("EVM address should deserialize"), + address + ); + } +} diff --git a/types/src/transaction/transfer_target.rs b/types/src/transaction/transfer_target.rs index e1500a5384..d306a11619 100644 --- a/types/src/transaction/transfer_target.rs +++ b/types/src/transaction/transfer_target.rs @@ -3,7 +3,7 @@ use rand::Rng; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; -use crate::{account::AccountHash, PublicKey, URef}; +use crate::{account::AccountHash, evm, PublicKey, URef}; /// The various types which can be used as the `target` runtime argument of a native transfer. #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] @@ -12,6 +12,8 @@ pub enum TransferTarget { PublicKey(PublicKey), /// An account hash. AccountHash(AccountHash), + /// An EVM address. + EvmAddress(evm::Address), /// A URef. URef(URef), } @@ -41,6 +43,12 @@ impl From for TransferTarget { } } +impl From for TransferTarget { + fn from(address: evm::Address) -> Self { + Self::EvmAddress(address) + } +} + impl From for TransferTarget { fn from(uref: URef) -> Self { Self::URef(uref) 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..eac8ead70f --- /dev/null +++ b/types/tests/evm_transaction.rs @@ -0,0 +1,510 @@ +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::{ + self, Address, Hash, Transaction, TransactionError, TransactionKind, + EIP4844_TRANSACTION_TYPE_ID, + }, + Approval, ApprovalsHash, Digest, PublicKey, SecretKey, TimeDiff, Timestamp, + Transaction as CasperTransaction, TransactionHash, U256, +}; + +const SIGNING_SECRET: [u8; 32] = [7; 32]; +const AUTHORIZATION_SECRET: [u8; 32] = [8; 32]; + +#[test] +fn decodes_legacy_signed_rlp() { + let signed_transaction = signed_legacy_transaction(); + let transaction = decode(signed_transaction.raw_rlp.clone()); + + assert_eq!(transaction.kind(), TransactionKind::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_eq!(transaction.approvals().len(), 1); + 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(), TransactionKind::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"); +} + +#[test] +fn decodes_eip1559_signed_rlp() { + let signed_transaction = signed_eip1559_transaction(); + let transaction = decode(signed_transaction.raw_rlp); + + assert_eq!(transaction.kind(), TransactionKind::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"); +} + +#[test] +fn decodes_eip7702_signed_rlp() { + let signed_transaction = signed_eip7702_transaction(); + let transaction = decode(signed_transaction.raw_rlp.clone()); + + assert_eq!(transaction.kind(), TransactionKind::Eip7702); + assert_eq!(transaction.from(), signed_transaction.sender); + assert_eq!(transaction.to(), Some(address(4))); + assert_eq!(transaction.nonce(), 3); + assert_eq!(transaction.gas_limit(), 70_000); + assert_eq!(transaction.max_fee_per_gas(), 2_000_000_000); + assert_eq!(transaction.max_priority_fee_per_gas(), Some(0)); + assert_eq!(transaction.value(), U256::from(987u64)); + assert_eq!(transaction.input(), &[0xde, 0xad]); + assert_eq!(transaction.chain_id(), Some(7)); + assert_eq!(transaction.authorization_list().len(), 1); + + let expected = &signed_transaction.authorization_list[0]; + let actual = &transaction.authorization_list()[0]; + assert_eq!(actual.chain_id, alloy_u256_to_casper(*expected.chain_id())); + assert_eq!( + actual.address, + alloy_address_to_address(*expected.address()) + ); + assert_eq!(actual.nonce, expected.nonce()); + assert_eq!(actual.y_parity, expected.y_parity()); + assert_eq!(actual.r, alloy_u256_to_casper(expected.r())); + assert_eq!(actual.s, alloy_u256_to_casper(expected.s())); + + transaction + .verify() + .expect("EIP-7702 transaction should verify"); + transaction + .signature_hash() + .expect("EIP-7702 signing hash should be available"); + assert_eq!( + transaction.signed_rlp().unwrap(), + signed_transaction.raw_rlp + ); + bytesrepr_roundtrip(&transaction); +} + +#[test] +fn unsupported_typed_transactions_are_clear_errors() { + let timestamp = Timestamp::zero(); + let ttl = TimeDiff::from_seconds(60); + + assert_eq!( + Transaction::from_signed_rlp(vec![EIP4844_TRANSACTION_TYPE_ID], timestamp, ttl), + Err(TransactionError::UnsupportedTransactionType( + EIP4844_TRANSACTION_TYPE_ID + )) + ); +} + +#[test] +fn non_empty_access_lists_are_rejected() { + let timestamp = Timestamp::zero(); + let ttl = TimeDiff::from_seconds(60); + + assert_eq!( + Transaction::from_signed_rlp(signed_eip2930_with_access_list(), timestamp, ttl), + Err(TransactionError::UnsupportedAccessList) + ); + assert_eq!( + Transaction::from_signed_rlp(signed_eip7702_with_access_list(), timestamp, ttl), + Err(TransactionError::UnsupportedAccessList) + ); +} + +#[test] +fn empty_eip7702_authorization_lists_are_rejected() { + assert_eq!( + Transaction::from_signed_rlp( + signed_eip7702_with_authorization_list(Vec::new(), AccessList::default()), + Timestamp::zero(), + TimeDiff::from_seconds(60), + ), + Err(TransactionError::EmptyAuthorizationList) + ); +} + +#[test] +fn approval_backed_transaction_identity_uses_evm_approval() { + let evm_transaction = decode(signed_eip1559_transaction().raw_rlp); + let transaction = CasperTransaction::from(evm_transaction.clone()); + let approvals_hash = ApprovalsHash::compute(evm_transaction.approvals()).unwrap(); + + assert_eq!(transaction.approvals(), evm_transaction.approvals().clone()); + assert_eq!( + transaction.compute_approvals_hash().unwrap(), + approvals_hash + ); + assert_eq!(transaction.compute_id().approvals_hash(), approvals_hash); + assert_eq!( + transaction.compute_id().transaction_hash(), + TransactionHash::from(evm_transaction.hash()) + ); +} + +#[test] +fn evm_approvals_are_not_replaced_by_finalized_approvals() { + let evm_transaction = decode(signed_eip1559_transaction().raw_rlp); + let transaction = CasperTransaction::from(evm_transaction.clone()); + let secret_key = SecretKey::ed25519_from_bytes([42; SecretKey::ED25519_LENGTH]).unwrap(); + let replacement_approval = + Approval::create(&TransactionHash::from(evm_transaction.hash()), &secret_key); + + assert_eq!( + transaction + .with_approvals(BTreeSet::from([replacement_approval])) + .approvals(), + evm_transaction.approvals().clone() + ); +} + +#[test] +fn evm_transaction_sign_replaces_approval_and_recomputes_identity() { + let mut transaction = CasperTransaction::from(decode(signed_legacy_transaction().raw_rlp)); + let old_hash = transaction.hash(); + let new_secret_key = secp_secret_key([1; SecretKey::SECP256K1_LENGTH]); + let expected_signer = PublicKey::from(&new_secret_key); + + transaction.sign(&new_secret_key); + + let CasperTransaction::Evm(evm_transaction) = transaction else { + panic!("expected EVM transaction"); + }; + assert_eq!(evm_transaction.approvals().len(), 1); + assert_eq!( + evm_transaction.approvals().iter().next().unwrap().signer(), + &expected_signer + ); + assert_ne!(TransactionHash::from(evm_transaction.hash()), old_hash); + evm_transaction + .verify() + .expect("signed transaction should verify"); + + let decoded = decode(evm_transaction.signed_rlp().unwrap()); + assert_eq!(decoded.hash(), evm_transaction.hash()); + assert_eq!(decoded.from(), evm_transaction.from()); + assert_eq!(decoded.approvals(), evm_transaction.approvals()); +} + +#[test] +fn evm_eip7702_transaction_sign_preserves_authorizations() { + let mut transaction = CasperTransaction::from(decode(signed_eip7702_transaction().raw_rlp)); + let CasperTransaction::Evm(evm_transaction) = &transaction else { + panic!("expected EVM transaction"); + }; + let authorization_list = evm_transaction.authorization_list().to_vec(); + let new_secret_key = secp_secret_key([1; SecretKey::SECP256K1_LENGTH]); + + transaction.sign(&new_secret_key); + + let CasperTransaction::Evm(evm_transaction) = transaction else { + panic!("expected EVM transaction"); + }; + assert_eq!(evm_transaction.authorization_list(), authorization_list); + evm_transaction + .verify() + .expect("signed EIP-7702 transaction should verify"); + + let decoded = decode(evm_transaction.signed_rlp().unwrap()); + assert_eq!(decoded.kind(), TransactionKind::Eip7702); + assert_eq!(decoded.authorization_list(), authorization_list); + assert_eq!(decoded.from(), evm_transaction.from()); +} + +#[test] +#[should_panic(expected = "EVM transactions must be signed with a valid secp256k1 key")] +fn evm_transaction_sign_rejects_non_secp256k1_keys() { + let mut transaction = CasperTransaction::from(decode(signed_legacy_transaction().raw_rlp)); + transaction.sign(&ed_secret_key([42; SecretKey::ED25519_LENGTH])); +} + +#[test] +fn evm_approval_verification_rejects_bad_approval_sets() { + let transaction = decode(signed_legacy_transaction().raw_rlp); + assert_eq!( + transaction.clone().with_approvals(BTreeSet::new()).verify(), + Err(TransactionError::MissingApproval) + ); + + let mut multiple_approvals = transaction.approvals().clone(); + multiple_approvals.insert(Approval::create( + &TransactionHash::from(transaction.hash()), + &secp_secret_key([1; SecretKey::SECP256K1_LENGTH]), + )); + assert_eq!( + transaction + .clone() + .with_approvals(multiple_approvals) + .verify(), + Err(TransactionError::MultipleApprovals) + ); + + let non_secp_approval = Approval::create( + &TransactionHash::from(transaction.hash()), + &ed_secret_key([42; SecretKey::ED25519_LENGTH]), + ); + assert_eq!( + transaction + .clone() + .with_approvals(BTreeSet::from([non_secp_approval])) + .verify(), + Err(TransactionError::NonSecp256k1Approval) + ); +} + +#[test] +fn evm_hashes_round_trip_raw_digest_bytes() { + let raw = [0x42; 32]; + let hash = Hash::new(raw); + assert_eq!(hash.value(), raw); + assert_eq!(hash.as_bytes(), &raw); + bytesrepr_roundtrip(&hash); + let serialized = serde_json::to_string(&hash).unwrap(); + let deserialized = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(deserialized, hash); + + let digest = Digest::from_raw(raw); + let transaction_hash = evm::TransactionHash::new(digest); + assert_eq!(transaction_hash.inner(), &digest); + assert_eq!(transaction_hash.value(), raw); + assert_eq!(Digest::from(transaction_hash), digest); + bytesrepr_roundtrip(&transaction_hash); + let serialized = serde_json::to_string(&transaction_hash).unwrap(); + let deserialized = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(deserialized, transaction_hash); +} + +struct SignedTransaction { + raw_rlp: Vec, + sender: Address, + authorization_list: Vec, +} + +fn decode(bytes: Vec) -> Transaction { + Transaction::from_signed_rlp(bytes, Timestamp::zero(), TimeDiff::from_seconds(60)) + .expect("transaction should decode") +} + +fn bytesrepr_roundtrip(value: &T) +where + T: ToBytes + FromBytes + PartialEq + std::fmt::Debug, +{ + let bytes = value.to_bytes().expect("value should serialize"); + let (decoded, remainder) = T::from_bytes(&bytes).expect("value should deserialize"); + assert!(remainder.is_empty()); + assert_eq!(&decoded, value); +} + +fn signed_legacy_transaction() -> SignedTransaction { + let tx = TxLegacy { + chain_id: Some(7), + nonce: 0, + gas_price: 1_000_000_000, + gas_limit: 21_000, + to: TxKind::Call(alloy_address(1)), + value: AlloyU256::from(123u64), + input: Vec::new().into(), + }; + let signature = sign_transaction(&tx); + signed_transaction(tx.into_signed(signature).into()) +} + +fn signed_eip2930_transaction() -> SignedTransaction { + let tx = TxEip2930 { + chain_id: 7, + nonce: 1, + gas_price: 1_000_000_000, + gas_limit: 50_000, + to: TxKind::Call(alloy_address(2)), + value: AlloyU256::from(456u64), + input: vec![0x12, 0x34].into(), + access_list: AccessList::default(), + }; + let signature = sign_transaction(&tx); + signed_transaction(tx.into_signed(signature).into()) +} + +fn signed_eip1559_transaction() -> SignedTransaction { + let tx = TxEip1559 { + chain_id: 7, + nonce: 2, + gas_limit: 60_000, + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 100_000_000, + to: TxKind::Call(alloy_address(3)), + value: AlloyU256::from(789u64), + access_list: AccessList::default(), + input: vec![0xab, 0xcd].into(), + }; + let signature = sign_transaction(&tx); + signed_transaction(tx.into_signed(signature).into()) +} + +fn signed_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"), + ); + SignedTransaction { + raw_rlp: envelope.encoded_2718(), + sender, + 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),