From ccd3b2ca9c65523868ce32b2324a432410f7b2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Buko=C5=A1ek?= Date: Thu, 8 Aug 2024 13:17:00 +0200 Subject: [PATCH 1/3] wip --- Cargo.lock | 722 +++++++++- Cargo.toml | 1 + runtime-sdk/modules/evm-new/Cargo.toml | 48 + runtime-sdk/modules/evm-new/src/db.rs | 106 ++ .../modules/evm-new/src/derive_caller.rs | 21 + runtime-sdk/modules/evm-new/src/lib.rs | 558 ++++++++ runtime-sdk/modules/evm-new/src/mock.rs | 285 ++++ runtime-sdk/modules/evm-new/src/raw_tx.rs | 444 ++++++ .../modules/evm-new/src/signed_call.rs | 336 +++++ runtime-sdk/modules/evm-new/src/state.rs | 122 ++ runtime-sdk/modules/evm-new/src/test.rs | 1203 +++++++++++++++++ runtime-sdk/modules/evm-new/src/types.rs | 222 +++ 12 files changed, 4038 insertions(+), 30 deletions(-) create mode 100644 runtime-sdk/modules/evm-new/Cargo.toml create mode 100644 runtime-sdk/modules/evm-new/src/db.rs create mode 100644 runtime-sdk/modules/evm-new/src/derive_caller.rs create mode 100644 runtime-sdk/modules/evm-new/src/lib.rs create mode 100644 runtime-sdk/modules/evm-new/src/mock.rs create mode 100644 runtime-sdk/modules/evm-new/src/raw_tx.rs create mode 100644 runtime-sdk/modules/evm-new/src/signed_call.rs create mode 100644 runtime-sdk/modules/evm-new/src/state.rs create mode 100644 runtime-sdk/modules/evm-new/src/test.rs create mode 100644 runtime-sdk/modules/evm-new/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index da4d246c8e..3e81521c17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -82,6 +94,92 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "alloy-eip2930" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41" +dependencies = [ + "alloy-primitives", + "alloy-rlp", +] + +[[package]] +name = "alloy-eip7702" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea59dc42102bc9a1905dc57901edc6dd48b9f38115df86c7d252acba70d71d04" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "k256", +] + +[[package]] +name = "alloy-eips" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f923dd5fca5f67a43d81ed3ebad0880bd41f6dd0ada930030353ac356c54cd0f" +dependencies = [ + "alloy-eip2930", + "alloy-eip7702", + "alloy-primitives", + "alloy-rlp", + "c-kzg", + "derive_more 1.0.0", + "once_cell", + "serde", +] + +[[package]] +name = "alloy-primitives" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71738eb20c42c5fb149571e76536a0f309d142f3957c28791662b96baf77a3d" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if 1.0.0", + "const-hex", + "derive_more 1.0.0", + "foldhash", + "hex-literal", + "indexmap 2.6.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand", + "ruint", + "rustc-hash 2.0.0", + "serde", + "sha3", + "tiny-keccak 2.0.2", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26154390b1d205a4a7ac7352aa2eb4f81f391399d4e2f546fb81a2f8bb383f62" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0f2d905ebd295e7effec65e5f6868d153936130ae718352771de3e7d03c75c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -111,9 +209,9 @@ checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anyhow" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arbitrary" @@ -130,6 +228,130 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[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 0.4.6", + "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 0.4.6", + "num-traits", + "paste", + "rustc_version 0.4.1", + "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-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint 0.4.6", + "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 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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 0.4.6", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -241,6 +463,16 @@ dependencies = [ "syn 2.0.82", ] +[[package]] +name = "aurora-engine-modexp" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aef7712851e524f35fbbb74fa6599c5cd8692056a1c36f9ca0d2001b670e7e5" +dependencies = [ + "hex", + "num", +] + [[package]] name = "auto_impl" version = "1.2.0" @@ -329,7 +561,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.82", "which", @@ -352,12 +584,21 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.82", "which", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-vec" version = "0.5.1" @@ -435,6 +676,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blst" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + [[package]] name = "brotli-decompressor" version = "4.0.1" @@ -465,13 +718,28 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" dependencies = [ "serde", ] +[[package]] +name = "c-kzg" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0307f72feab3300336fb803a57134159f6e20139af1357f36c54cb90d8e8928" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + [[package]] name = "cast" version = "0.3.0" @@ -645,6 +913,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "const-hex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0121754e84117e65f9d90648ee6aa4882a6e63110307ab73967a4c5e7e69e586" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "hex", + "proptest", + "serde", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -839,7 +1120,7 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "rustc_version", + "rustc_version 0.4.1", "subtle", "zeroize", ] @@ -1003,6 +1284,17 @@ dependencies = [ "powerfmt", ] +[[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_arbitrary" version = "1.3.2" @@ -1025,6 +1317,27 @@ dependencies = [ "syn 2.0.82", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", + "unicode-xid", +] + [[package]] name = "diff" version = "0.1.13" @@ -1069,6 +1382,12 @@ dependencies = [ "syn 2.0.82", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1158,6 +1477,17 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "environmental" version = "1.1.4" @@ -1324,6 +1654,23 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + [[package]] name = "ff" version = "0.13.0" @@ -1608,6 +1955,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.0" @@ -1646,6 +2003,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hkdf" version = "0.12.4" @@ -1682,7 +2045,7 @@ dependencies = [ "arbitrary", "lazy_static", "memmap2", - "rustc_version", + "rustc_version 0.4.1", ] [[package]] @@ -1931,6 +2294,16 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keccak-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2281,6 +2654,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "oasis-cbor" version = "0.5.1" @@ -2568,6 +2951,34 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "oasis-runtime-sdk-evm-new" +version = "0.6.0" +dependencies = [ + "anyhow", + "base64", + "blake3", + "criterion", + "ethabi", + "ethereum 0.15.0", + "fixed-hash", + "hex", + "honggfuzz", + "k256", + "oasis-cbor", + "oasis-runtime-sdk", + "once_cell", + "primitive-types", + "rand", + "revm", + "rlp", + "serde", + "serde_json", + "sha3", + "thiserror", + "uint", +] + [[package]] name = "oasis-runtime-sdk-macros" version = "0.3.0" @@ -2751,6 +3162,17 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + [[package]] name = "pin-project" version = "1.1.6" @@ -2935,13 +3357,33 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec 0.6.3", + "bitflags 2.6.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "prost" version = "0.13.3" @@ -2965,6 +3407,12 @@ dependencies = [ "syn 2.0.82", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.37" @@ -3077,6 +3525,66 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "revm" +version = "14.0.0" +source = "git+https://github.com/bluealloy/revm?tag=v42#3085f04ac6b144e7ac721abce9cbbf538ff6b7fe" +dependencies = [ + "auto_impl", + "cfg-if 1.0.0", + "dyn-clone", + "revm-interpreter", + "revm-precompile", + "serde", + "serde_json", +] + +[[package]] +name = "revm-interpreter" +version = "10.0.0" +source = "git+https://github.com/bluealloy/revm?tag=v42#3085f04ac6b144e7ac721abce9cbbf538ff6b7fe" +dependencies = [ + "revm-primitives", + "serde", +] + +[[package]] +name = "revm-precompile" +version = "11.0.0" +source = "git+https://github.com/bluealloy/revm?tag=v42#3085f04ac6b144e7ac721abce9cbbf538ff6b7fe" +dependencies = [ + "aurora-engine-modexp", + "blst", + "c-kzg", + "cfg-if 1.0.0", + "k256", + "once_cell", + "revm-primitives", + "ripemd", + "secp256k1", + "sha2 0.10.8", + "substrate-bn", +] + +[[package]] +name = "revm-primitives" +version = "9.0.0" +source = "git+https://github.com/bluealloy/revm?tag=v42#3085f04ac6b144e7ac721abce9cbbf538ff6b7fe" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "auto_impl", + "bitflags 2.6.0", + "bitvec", + "c-kzg", + "cfg-if 1.0.0", + "dyn-clone", + "enumn", + "hashbrown 0.14.5", + "hex", + "serde", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -3177,6 +3685,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ruint" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "bytes", + "fastrlp", + "num-bigint 0.4.6", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand", + "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" @@ -3189,19 +3727,34 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[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.23", ] [[package]] @@ -3295,6 +3848,18 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.18" @@ -3312,22 +3877,22 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.11.3" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca070c12893629e2cc820a9761bedf6ce1dcddc9852984d1dc734b8bd9bd024" +checksum = "22760a375f81a31817aeaf6f5081e9ccb7ffd7f2da1809a6e3fc82b6656f10d5" dependencies = [ "bitvec", "cfg-if 1.0.0", - "derive_more", + "derive_more 1.0.0", "parity-scale-codec", "scale-info-derive", ] [[package]] name = "scale-info-derive" -version = "2.11.3" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d35494501194174bda522a32605929eefc9ecf7e0a326c26db1fdd85881eb62" +checksum = "abc61ebe25a5c410c0e245028fc9934bf8fa4817199ef5a24a68092edfd34614" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", @@ -3374,6 +3939,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "rand", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "secret-sharing" version = "0.1.0" @@ -3390,17 +3974,35 @@ dependencies = [ "thiserror", ] +[[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.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] @@ -3426,9 +4028,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", @@ -3441,6 +4043,7 @@ version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ + "indexmap 2.6.0", "itoa", "memchr", "ryu", @@ -3522,6 +4125,16 @@ dependencies = [ "keccak", ] +[[package]] +name = "sha3-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +dependencies = [ + "cc", + "cfg-if 1.0.0", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3625,7 +4238,7 @@ dependencies = [ "chacha20poly1305", "curve25519-dalek", "rand_core", - "rustc_version", + "rustc_version 0.4.1", "sha2 0.10.8", "subtle", ] @@ -3775,6 +4388,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tendermint" version = "0.39.1" @@ -3825,7 +4451,7 @@ checksum = "8595e8c4b202c41588930be1fdf13d7c9c39c621704e3ee6592e6f8064f50926" dependencies = [ "contracts", "crossbeam-channel", - "derive_more", + "derive_more 0.99.18", "flex-error", "futures", "regex", @@ -3847,7 +4473,7 @@ version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8c85693e8f7d65c19898292ae7ec90b565a73c5b4df15564987e0f0e895a451" dependencies = [ - "derive_more", + "derive_more 0.99.18", "flex-error", "serde", "tendermint", @@ -3882,7 +4508,7 @@ dependencies = [ "peg", "pin-project", "rand", - "semver", + "semver 1.0.23", "serde", "serde_bytes", "serde_json", @@ -3978,24 +4604,33 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", "syn 2.0.82", ] +[[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.36" @@ -4072,9 +4707,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -4196,6 +4831,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[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" @@ -4208,6 +4849,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.17" @@ -4307,6 +4954,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.5" @@ -4323,6 +4976,15 @@ dependencies = [ "nix", ] +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index e47bc5e76a..21495c5a63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ # Runtime SDK Modules. "runtime-sdk/modules/contracts", "runtime-sdk/modules/evm", + "runtime-sdk/modules/evm-new", # Smart Contract SDK. "contract-sdk", diff --git a/runtime-sdk/modules/evm-new/Cargo.toml b/runtime-sdk/modules/evm-new/Cargo.toml new file mode 100644 index 0000000000..4f6d056576 --- /dev/null +++ b/runtime-sdk/modules/evm-new/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "oasis-runtime-sdk-evm-new" +description = "New EVM module for the Oasis Runtime SDK." +version = "0.6.0" +authors = ["Oasis Protocol Foundation "] +edition = "2021" +license = "Apache-2.0" + +[dependencies] +cbor = { version = "0.5.1", package = "oasis-cbor" } +oasis-runtime-sdk = { path = "../.." } + +# Third party. +anyhow = "1.0" +base64 = "0.22.1" +blake3 = { version = "~1.5.1", features = ["traits-preview"] } +thiserror = "1.0" +hex = "0.4.2" +k256 = "0.13.1" +sha3 = { version = "0.10", default-features = false } +once_cell = "1.8.0" + +# Ethereum. +ethabi = { version = "18.0.0", default-features = false, features = ["std"] } +ethereum = "0.15" +revm = { git = "https://github.com/bluealloy/revm", tag = "v42" } +fixed-hash = "0.8.0" +primitive-types = { version = "0.12", default-features = false, features = ["rlp", "num-traits"] } +rlp = "0.5.2" +uint = "0.9.1" + +# Fuzzing. +honggfuzz = "0.5.56" +serde = { version = "1.0.203", features = ["derive"], optional = true } +serde_json = { version = "1.0.116", features = ["raw_value"], optional = true } + +[dev-dependencies] +criterion = "0.5.1" +oasis-runtime-sdk = { path = "../..", features = ["test"] } +rand = "0.8.5" +serde = { version = "1.0.203", features = ["derive"] } +serde_json = { version = "1.0.116", features = ["raw_value"] } +ethabi = { version = "18.0.0", default-features = false, features = ["std", "full-serde"] } + +[features] +default = [] +test = ["serde", "serde_json"] + diff --git a/runtime-sdk/modules/evm-new/src/db.rs b/runtime-sdk/modules/evm-new/src/db.rs new file mode 100644 index 0000000000..de9efb5bfc --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/db.rs @@ -0,0 +1,106 @@ +use revm::{ + primitives::{keccak256, AccountInfo, Address, Bytecode, Log, B256, KECCAK_EMPTY, U256}, + Database, +}; +use std::{convert::Infallible, vec::Vec}; + +use std::marker::PhantomData; + +use oasis_runtime_sdk::{ + context::Context, + core::common::crypto::hash::Hash, + modules::{ + accounts::API as _, + core::{self, API as _}, + }, + state::CurrentState, + subcall, + types::token, + Runtime, +}; + +use crate::{state, types, Config}; + +pub struct OasisDB<'ctx, C: Context, Cfg: Config> { + ctx: &'ctx C, + _cfg: PhantomData, +} + +impl<'ctx, C: Context, Cfg: Config> OasisDB<'ctx, C, Cfg> { + pub fn new(ctx: &'ctx C) -> Self { + Self { + ctx, + _cfg: PhantomData, + } + } +} + +impl<'ctx, C: Context, Cfg: Config> Database for OasisDB<'ctx, C, Cfg> { + type Error = Infallible; + + /// Get basic account information. + fn basic(&mut self, address: Address) -> Result, Self::Error> { + // Derive SDK account address from the Ethereum address. + let sdk_address = Cfg::map_address(address); + + // Fetch balance and nonce from SDK accounts. Note that these can never fail. + let balance = + ::Accounts::get_balance(sdk_address, Cfg::TOKEN_DENOMINATION) + .unwrap(); + let mut nonce = ::Accounts::get_nonce(sdk_address).unwrap(); + + // Fetch code for this address from storage. + let code = CurrentState::with_store(|store| { + let codes = state::codes(store); + + if let Some(code) = codes.get::<_, Vec>(address) { + Some(Bytecode::new_raw(code.into())) + } else { + None + } + }); + + // Calculate hash of code if it exists. + let code_hash = match code { + None => KECCAK_EMPTY, + Some(ref bc) => bc.hash_slow(), + }; + + Ok(Some(AccountInfo { + nonce: nonce.into(), + balance: U256::from(balance), + code, + code_hash, + })) + } + + /// Get account code by its hash (unimplemented). + fn code_by_hash(&mut self, _code_hash: B256) -> Result { + // XXX: return an error here instead. + Ok(Bytecode::new()) + } + + /// Get storage value of address at index. + fn storage(&mut self, address: Address, index: U256) -> Result { + let address: types::H160 = address.into_array().into(); + let index: types::H256 = index.to_be_bytes().into(); // XXX: is BE ok? + + let res: types::H256 = state::with_storage::(self.ctx, &address, |store| { + store.get(index).unwrap_or_default() + }); + Ok(U256::from_be_bytes(res.into())) // XXX: is BE ok? + } + + /// Get block hash by block number. + fn block_hash(&mut self, number: u64) -> Result { + CurrentState::with_store(|store| { + let block_hashes = state::block_hashes(store); + + if let Some(hash) = block_hashes.get::<_, Hash>(&number.to_be_bytes()) { + Ok(B256::from_slice(hash.as_ref())) + } else { + Ok(B256::default()) + } + }) + } +} diff --git a/runtime-sdk/modules/evm-new/src/derive_caller.rs b/runtime-sdk/modules/evm-new/src/derive_caller.rs new file mode 100644 index 0000000000..014c9d0c3d --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/derive_caller.rs @@ -0,0 +1,21 @@ +use oasis_runtime_sdk::types::{ + address::SignatureAddressSpec, + transaction::{AddressSpec, AuthInfo, CallerAddress}, +}; + +use crate::{types::H160, Error}; + +pub fn from_sigspec(spec: &SignatureAddressSpec) -> Result { + match spec { + SignatureAddressSpec::Secp256k1Eth(pk) => Ok(H160::from_slice(&pk.to_eth_address())), + _ => Err(Error::InvalidSignerType), + } +} + +pub fn from_tx_auth_info(ai: &AuthInfo) -> Result { + match &ai.signer_info[0].address_spec { + AddressSpec::Signature(spec) => from_sigspec(spec), + AddressSpec::Internal(CallerAddress::EthAddress(address)) => Ok(address.into()), + _ => Err(Error::InvalidSignerType), + } +} diff --git a/runtime-sdk/modules/evm-new/src/lib.rs b/runtime-sdk/modules/evm-new/src/lib.rs new file mode 100644 index 0000000000..2ead70547a --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/lib.rs @@ -0,0 +1,558 @@ +//! EVM module. +#![feature(array_chunks)] +#![feature(test)] + +pub mod db; +pub mod derive_caller; +pub mod raw_tx; +mod signed_call; +pub mod state; +pub mod types; + +use revm::{ + primitives::{ExecutionResult, Output, TxKind}, + Evm, +}; + +use oasis_runtime_sdk::{ + callformat, + context::Context, + handler, migration, + module::{self, Module as _}, + modules::{ + self, + accounts::API as _, + core::{Error as CoreError, API as _}, + }, + runtime::Runtime, + sdk_derive, + state::{CurrentState, Mode, Options, TransactionResult, TransactionWithMeta}, + types::{ + address::{self, Address}, + token, transaction, + transaction::Transaction, + }, +}; +use thiserror::Error; +use types::{H160, H256, U256}; + +/// Unique module name. +const MODULE_NAME: &str = "evm-new"; + +#[cfg(any(test, feature = "test"))] +pub mod mock; +#[cfg(test)] +mod test; + +/// Module configuration. +pub trait Config: 'static { + /// The chain ID to supply when a contract requests it. Ethereum-format transactions must use + /// this chain ID. + const CHAIN_ID: u64; + + /// Token denomination used as the native EVM token. + const TOKEN_DENOMINATION: token::Denomination; + + /// Whether to use confidential storage by default, and transaction data encryption. + const CONFIDENTIAL: bool = false; + + /// Whether to refund unused transaction fee. + const REFUND_UNUSED_FEE: bool = true; + + /// Maximum result size in bytes. + const MAX_RESULT_SIZE: usize = 1024; + + /// Maps an Ethereum address into an SDK account address. + fn map_address(address: revm::primitives::Address) -> Address { + Address::new( + address::ADDRESS_V0_SECP256K1ETH_CONTEXT, + address::ADDRESS_V0_VERSION, + address.as_ref(), + ) + } +} + +pub struct Module { + _cfg: std::marker::PhantomData, +} + +/// Errors emitted by the EVM module. +#[derive(Error, Debug, oasis_runtime_sdk::Error)] +pub enum Error { + #[error("invalid argument")] + #[sdk_error(code = 1)] + InvalidArgument, + + #[error("execution failed: {0}")] + #[sdk_error(code = 2)] + ExecutionFailed(String), + + #[error("invalid signer type")] + #[sdk_error(code = 3)] + InvalidSignerType, + + #[error("fee overflow")] + #[sdk_error(code = 4)] + FeeOverflow, + + #[error("gas limit too low: {0} required")] + #[sdk_error(code = 5)] + GasLimitTooLow(u64), + + #[error("insufficient balance")] + #[sdk_error(code = 6)] + InsufficientBalance, + + #[error("forbidden by policy")] + #[sdk_error(code = 7)] + Forbidden, + + #[error("reverted: {0}")] + #[sdk_error(code = 8)] + Reverted(String), + + #[error("forbidden by policy: this node only allows simulating calls that use up to {0} gas")] + #[sdk_error(code = 9)] + SimulationTooExpensive(u64), + + #[error("invalid signed simulate call query: {0}")] + #[sdk_error(code = 10)] + InvalidSignedSimulateCall(&'static str), + + #[error("core: {0}")] + #[sdk_error(transparent)] + Core(#[from] CoreError), +} + +/// Gas costs. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct GasCosts {} + +/// Parameters for the EVM module. +#[derive(Clone, Default, Debug, cbor::Encode, cbor::Decode)] +pub struct Parameters { + /// Gas costs. + pub gas_costs: GasCosts, +} + +impl module::Parameters for Parameters { + type Error = (); + + fn validate_basic(&self) -> Result<(), Self::Error> { + Ok(()) + } +} + +/// Genesis state for the EVM module. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct Genesis { + pub parameters: Parameters, +} + +/// Local configuration that can be provided by the node operator. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct LocalConfig { + /// Maximum gas limit that can be passed to the `evm.SimulateCall` query. Queries + /// with a higher gas limit will be rejected. A special value of `0` indicates + /// no limit. Default: 0. + #[cbor(optional)] + pub query_simulate_call_max_gas: u64, +} + +/// Events emitted by the EVM module. +#[derive(Debug, cbor::Encode, oasis_runtime_sdk::Event)] +#[cbor(untagged)] +pub enum Event { + #[sdk_event(code = 1)] + Log { + address: H160, + topics: Vec, + data: Vec, + }, +} + +/// Interface that can be called from other modules. +pub trait API { + /// Perform an Ethereum CREATE transaction. + /// Returns 160-bit address of created contract. + fn create(ctx: &C, value: U256, init_code: Vec) -> Result, Error>; + + /// Perform an Ethereum CALL transaction. + fn call( + ctx: &C, + address: H160, + value: U256, + data: Vec, + ) -> Result, Error>; + + /// Peek into EVM storage. + /// Returns 256-bit value stored at given contract address and index (slot) + /// in the storage. + fn get_storage(ctx: &C, address: H160, index: H256) -> Result, Error>; + + /// Peek into EVM code storage. + /// Returns EVM bytecode of contract at given address. + fn get_code(ctx: &C, address: H160) -> Result, Error>; + + /// Get EVM account balance. + fn get_balance(ctx: &C, address: H160) -> Result; + + /// Simulate an Ethereum CALL. + /// + /// If the EVM is confidential, it may accept _signed queries_, which are + /// formatted as either a [`sdk::types::transaction::Call`] or + /// [`types::SignedCallDataPack`] encoded and packed into the `data` field + /// of [`types::SimulateCallQuery`]. + fn simulate_call(ctx: &C, call: types::SimulateCallQuery) + -> Result, Error>; +} + +impl API for Module { + fn create(ctx: &C, value: U256, init_code: Vec) -> Result, Error> { + let caller = Self::derive_caller()?; + + if !ctx.should_execute_contracts() { + // Only fast checks are allowed. + return Ok(vec![]); + } + + let (tx_call_format, tx_index, is_simulation) = CurrentState::with_env(|env| { + (env.tx_call_format(), env.tx_index(), env.is_simulation()) + }); + + // Create output (the contract address) does not need to be encrypted because it's + // trivially computable by anyone who can observe the create tx and receipt status. + // Therefore, we don't need the `tx_metadata` or to encode the result. + let (init_code, _tx_metadata) = + Self::decode_call_data(ctx, init_code, tx_call_format, tx_index, true)? + .expect("processing always proceeds"); + + // If in simulation, this must be EstimateGas query. + // Use estimate mode if not doing binary search for exact gas costs. + let estimate_gas = + is_simulation && ::Core::estimate_gas_search_max_iters(ctx) == 0; + + Self::evm_create(ctx, caller, value, init_code, estimate_gas) + } + + fn call( + ctx: &C, + address: H160, + value: U256, + data: Vec, + ) -> Result, Error> { + let caller = Self::derive_caller()?; + + if !ctx.should_execute_contracts() { + // Only fast checks are allowed. + return Ok(vec![]); + } + + let (tx_call_format, tx_index, is_simulation) = CurrentState::with_env(|env| { + (env.tx_call_format(), env.tx_index(), env.is_simulation()) + }); + + let (data, tx_metadata) = + Self::decode_call_data(ctx, data, tx_call_format, tx_index, true)? + .expect("processing always proceeds"); + + // If in simulation, this must be EstimateGas query. + // Use estimate mode if not doing binary search for exact gas costs. + let estimate_gas = + is_simulation && ::Core::estimate_gas_search_max_iters(ctx) == 0; + + let evm_result = Self::evm_call(ctx, caller, address, value, data, estimate_gas); + Self::encode_evm_result(ctx, evm_result, tx_metadata) + } + + fn get_storage(_ctx: &C, address: H160, index: H256) -> Result, Error> { + state::with_public_storage(&address, |store| { + let result: H256 = store.get(index).unwrap_or_default(); + Ok(result.as_bytes().to_vec()) + }) + } + + fn get_code(_ctx: &C, address: H160) -> Result, Error> { + CurrentState::with_store(|store| { + let codes = state::codes(store); + Ok(codes.get(address).unwrap_or_default()) + }) + } + + fn get_balance(_ctx: &C, address: H160) -> Result { + let address = Cfg::map_address(address.0.into()); + Ok( + ::Accounts::get_balance(address, Cfg::TOKEN_DENOMINATION) + .unwrap_or_default(), + ) + } + + fn simulate_call( + ctx: &C, + call: types::SimulateCallQuery, + ) -> Result, Error> { + todo!() + } +} + +impl Module { + fn evm_create( + ctx: &C, + caller: H160, + value: U256, + init_code: Vec, + estimate_gas: bool, + ) -> Result, Error> { + let mut db = db::OasisDB::<'_, C, Cfg>::new(ctx); + + let mut evm = Evm::builder() + .with_db(db) + .modify_tx_env(|tx| { + tx.transact_to = TxKind::Create; + tx.caller = caller.0.into(); + tx.value = revm::primitives::U256::from_be_bytes(value.into()); // XXX: is BE ok? + tx.data = init_code.into(); + }) + .build(); + + let tx = evm.transact().unwrap(); // XXX: transact_commit? + err checking + + let ExecutionResult::Success { + output: Output::Create(_, Some(address)), + .. + } = tx.result + else { + return Err(todo!()); + }; + + todo!() + } + + fn evm_call( + ctx: &C, + caller: H160, + address: H160, + value: U256, + data: Vec, + estimate_gas: bool, + ) -> Result, Error> { + todo!() + } + + fn derive_caller() -> Result { + CurrentState::with_env(|env| derive_caller::from_tx_auth_info(env.tx_auth_info())) + } + + /// Returns the decrypted call data or `None` if this transaction is simulated in + /// a context that may not include a key manager (i.e. SimulateCall but not EstimateGas). + fn decode_call_data( + ctx: &C, + data: Vec, + format: transaction::CallFormat, // The tx call format. + tx_index: usize, + assume_km_reachable: bool, + ) -> Result, callformat::Metadata)>, Error> { + if !Cfg::CONFIDENTIAL || format != transaction::CallFormat::Plain { + // Either the runtime is non-confidential and all txs are plaintext, or the tx + // is sent using a confidential call format and the tx has already been decrypted. + return Ok(Some((data, callformat::Metadata::Empty))); + } + match cbor::from_slice(&data) { + Ok(call) => Self::decode_call(ctx, call, tx_index, assume_km_reachable), + Err(_) => Ok(Some((data, callformat::Metadata::Empty))), // It's not encrypted. + } + } + + /// Returns the decrypted call data or `None` if this transaction is simulated in + /// a context that may not include a key manager (i.e. SimulateCall but not EstimateGas). + fn decode_call( + ctx: &C, + call: transaction::Call, + tx_index: usize, + assume_km_reachable: bool, + ) -> Result, callformat::Metadata)>, Error> { + match callformat::decode_call_ex(ctx, call, tx_index, assume_km_reachable)? { + Some(( + transaction::Call { + body: cbor::Value::ByteString(data), + .. + }, + metadata, + )) => Ok(Some((data, metadata))), + Some((_, _)) => { + Err(CoreError::InvalidCallFormat(anyhow::anyhow!("invalid inner data")).into()) + } + None => Ok(None), + } + } + + fn decode_simulate_call_query( + ctx: &C, + call: types::SimulateCallQuery, + ) -> Result<(types::SimulateCallQuery, callformat::Metadata), Error> { + if !Cfg::CONFIDENTIAL { + return Ok((call, callformat::Metadata::Empty)); + } + + if let Ok(types::SignedCallDataPack { + data, + leash, + signature, + }) = cbor::from_slice(&call.data) + { + let (data, tx_metadata) = + Self::decode_call(ctx, data, 0, true)?.expect("processing always proceeds"); + return Ok(( + signed_call::verify::<_, Cfg>( + ctx, + types::SimulateCallQuery { data, ..call }, + leash, + signature, + )?, + tx_metadata, + )); + } + + // The call is not signed, but it must be encoded as an oasis-sdk call. + let tx_call_format = transaction::CallFormat::Plain; // Queries cannot be encrypted. + let (data, tx_metadata) = Self::decode_call_data(ctx, call.data, tx_call_format, 0, true)? + .expect("processing always proceeds"); + Ok(( + types::SimulateCallQuery { + caller: Default::default(), // The sender cannot be spoofed. + data, + ..call + }, + tx_metadata, + )) + } + + fn encode_evm_result( + ctx: &C, + evm_result: Result, Error>, + tx_metadata: callformat::Metadata, // Potentially parsed from an inner enveloped tx. + ) -> Result, Error> { + if matches!(tx_metadata, callformat::Metadata::Empty) { + // Either the runtime is non-confidential and all responses are plaintext, + // or the tx was sent using a confidential call format and dispatcher will + // encrypt the call in the normal way. + return evm_result; + } + // Always propagate errors in plaintext. + let call_result = module::CallResult::Ok(evm_result?.into()); + Ok(cbor::to_vec(callformat::encode_result_ex( + ctx, + call_result, + tx_metadata, + true, + ))) + } +} + +#[sdk_derive(Module)] +impl Module { + const NAME: &'static str = MODULE_NAME; + const VERSION: u32 = 3; + type Error = Error; + type Event = Event; + type Parameters = Parameters; + type Genesis = Genesis; + + #[migration(init)] + fn init(genesis: Genesis) { + // Set genesis parameters. + Self::set_params(genesis.parameters); + } + + #[migration(from = 2)] + fn migrate_v2_to_v3() { + // TODO? + } + + #[handler(call = "evm.Create")] + fn tx_create(ctx: &C, body: types::Create) -> Result, Error> { + Self::create(ctx, body.value, body.init_code) + } + + #[handler(call = "evm.Call")] + fn tx_call(ctx: &C, body: types::Call) -> Result, Error> { + Self::call(ctx, body.address, body.value, body.data) + } + + #[handler(query = "evm.Storage")] + fn query_storage(ctx: &C, body: types::StorageQuery) -> Result, Error> { + Self::get_storage(ctx, body.address, body.index) + } + + #[handler(query = "evm.Code")] + fn query_code(ctx: &C, body: types::CodeQuery) -> Result, Error> { + Self::get_code(ctx, body.address) + } + + #[handler(query = "evm.Balance")] + fn query_balance(ctx: &C, body: types::BalanceQuery) -> Result { + Self::get_balance(ctx, body.address) + } + + #[handler(query = "evm.SimulateCall", expensive, allow_private_km)] + fn query_simulate_call( + ctx: &C, + body: types::SimulateCallQuery, + ) -> Result, Error> { + let cfg: LocalConfig = ctx.local_config(MODULE_NAME).unwrap_or_default(); + if cfg.query_simulate_call_max_gas > 0 && body.gas_limit > cfg.query_simulate_call_max_gas { + return Err(Error::SimulationTooExpensive( + cfg.query_simulate_call_max_gas, + )); + } + Self::simulate_call(ctx, body) + } +} + +impl module::TransactionHandler for Module { + fn decode_tx( + ctx: &C, + scheme: &str, + body: &[u8], + ) -> Result, CoreError> { + match scheme { + "evm.ethereum.v0" => { + let min_gas_price = + ::Core::min_gas_price(&Cfg::TOKEN_DENOMINATION) + .unwrap_or_default(); + + Ok(Some( + raw_tx::decode( + body, + Some(Cfg::CHAIN_ID), + min_gas_price, + &Cfg::TOKEN_DENOMINATION, + ) + .map_err(CoreError::MalformedTransaction)?, + )) + } + _ => Ok(None), + } + } +} + +impl module::BlockHandler for Module { + fn end_block(ctx: &C) { + CurrentState::with_store(|store| { + // Update the list of historic block hashes. + let block_number = ctx.runtime_header().round; + let block_hash = ctx.runtime_header().encoded_hash(); + let mut block_hashes = state::block_hashes(store); + + let current_number = block_number; + block_hashes.insert(block_number.to_be_bytes(), block_hash); + + if current_number > state::BLOCK_HASH_WINDOW_SIZE { + let start_number = current_number - state::BLOCK_HASH_WINDOW_SIZE; + block_hashes.remove(start_number.to_be_bytes()); + } + }); + } +} + +impl module::InvariantHandler for Module {} diff --git a/runtime-sdk/modules/evm-new/src/mock.rs b/runtime-sdk/modules/evm-new/src/mock.rs new file mode 100644 index 0000000000..a882490475 --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/mock.rs @@ -0,0 +1,285 @@ +//! Mock functionality for use during testing. +use base64::prelude::*; +use uint::hex::FromHex; + +use oasis_runtime_sdk::{ + callformat, + core::common::crypto::mrae::deoxysii, + dispatcher, + error::RuntimeError, + module, + testing::mock::{CallOptions, Signer}, + types::{address::SignatureAddressSpec, transaction}, + Context, +}; + +use crate::{ + derive_caller, + types::{self, H160}, +}; + +/// A mock EVM signer for use during tests. +pub struct EvmSigner(Signer); + +impl EvmSigner { + /// Create a new mock signer using the given nonce and signature spec. + pub fn new(nonce: u64, sigspec: SignatureAddressSpec) -> Self { + Self(Signer::new(nonce, sigspec)) + } + + /// Dispatch a call to the given EVM contract method. + pub fn call_evm( + &mut self, + ctx: &C, + address: H160, + name: &str, + param_types: &[ethabi::ParamType], + params: &[ethabi::Token], + ) -> dispatcher::DispatchResult + where + C: Context, + { + self.call_evm_opts(ctx, address, name, param_types, params, Default::default()) + } + + /// Dispatch a call to the given EVM contract method with the given options. + pub fn call_evm_opts( + &mut self, + ctx: &C, + address: H160, + name: &str, + param_types: &[ethabi::ParamType], + params: &[ethabi::Token], + opts: CallOptions, + ) -> dispatcher::DispatchResult + where + C: Context, + { + let data = [ + ethabi::short_signature(name, param_types).to_vec(), + ethabi::encode(params), + ] + .concat(); + + self.call_opts( + ctx, + "evm.Call", + types::Call { + address, + value: 0.into(), + data, + }, + opts, + ) + } + + /// Ethereum address for this signer. + pub fn address(&self) -> H160 { + derive_caller::from_sigspec(self.sigspec()).expect("caller should be evm-compatible") + } + + /// Dispatch a query to the given EVM contract method. + pub fn query_evm_call( + &self, + ctx: &C, + address: H160, + name: &str, + param_types: &[ethabi::ParamType], + params: &[ethabi::Token], + ) -> Result, RuntimeError> + where + C: Context, + { + self.query_evm_call_opts(ctx, address, name, param_types, params, Default::default()) + } + + /// Dispatch a query to the given EVM contract method. + pub fn query_evm_call_opts( + &self, + ctx: &C, + address: H160, + name: &str, + param_types: &[ethabi::ParamType], + params: &[ethabi::Token], + opts: QueryOptions, + ) -> Result, RuntimeError> + where + C: Context, + { + let data = [ + ethabi::short_signature(name, param_types).to_vec(), + ethabi::encode(params), + ] + .concat(); + + self.query_evm_opts(ctx, Some(address), data, opts) + } + + /// Dispatch a query to simulate EVM contract creation. + pub fn query_evm_create(&self, ctx: &C, init_code: Vec) -> Result, RuntimeError> + where + C: Context, + { + self.query_evm_opts(ctx, None, init_code, Default::default()) + } + + /// Dispatch a query to simulate EVM contract creation. + pub fn query_evm_create_opts( + &self, + ctx: &C, + init_code: Vec, + opts: QueryOptions, + ) -> Result, RuntimeError> + where + C: Context, + { + self.query_evm_opts(ctx, None, init_code, opts) + } + + /// Dispatch a query to the EVM. + pub fn query_evm_opts( + &self, + ctx: &C, + address: Option, + mut data: Vec, + opts: QueryOptions, + ) -> Result, RuntimeError> + where + C: Context, + { + // Handle optional encryption. + let client_keypair = deoxysii::generate_key_pair(); + if opts.encrypt { + data = cbor::to_vec( + callformat::encode_call( + ctx, + transaction::Call { + format: transaction::CallFormat::EncryptedX25519DeoxysII, + method: "".into(), + body: cbor::Value::from(data), + ..Default::default() + }, + &client_keypair, + ) + .unwrap(), + ); + } + + let mut result: Vec = self.query( + ctx, + "evm.SimulateCall", + types::SimulateCallQuery { + gas_price: 0.into(), + gas_limit: opts.gas_limit, + caller: opts.caller.unwrap_or_else(|| self.address()), + address, + value: 0.into(), + data, + }, + )?; + + // Handle optional decryption. + if opts.encrypt { + let call_result: transaction::CallResult = + cbor::from_slice(&result).expect("result from EVM should be properly encoded"); + let call_result = callformat::decode_result( + ctx, + transaction::CallFormat::EncryptedX25519DeoxysII, + call_result, + &client_keypair, + ) + .expect("callformat decoding should succeed"); + + result = match call_result { + module::CallResult::Ok(v) => { + cbor::from_value(v).expect("result from EVM should be correct") + } + module::CallResult::Failed { + module, + code, + message, + } => return Err(RuntimeError::new(&module, code, &message)), + module::CallResult::Aborted(e) => panic!("aborted with error: {e}"), + }; + } + + Ok(result) + } +} + +impl std::ops::Deref for EvmSigner { + type Target = Signer; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for EvmSigner { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Options for making queries. +pub struct QueryOptions { + /// Whether the call should be encrypted. + pub encrypt: bool, + /// Gas limit. + pub gas_limit: u64, + /// Use specified caller instead of signer. + pub caller: Option, +} + +impl Default for QueryOptions { + fn default() -> Self { + Self { + encrypt: false, + gas_limit: 10_000_000, + caller: None, + } + } +} + +/// Load contract bytecode from a hex-encoded string. +pub fn load_contract_bytecode(raw: &str) -> Vec { + Vec::from_hex(raw.split_whitespace().collect::()) + .expect("compiled contract should be a valid hex string") +} + +/// Decode a basic revert reason. +pub fn decode_reverted(msg: &str) -> Option { + decode_reverted_abi( + msg, + ethabi::AbiError { + name: "Error".to_string(), + inputs: vec![ethabi::Param { + name: "message".to_string(), + kind: ethabi::ParamType::String, + internal_type: None, + }], + }, + )? + .pop() + .unwrap() + .into_string() +} + +/// Decode a revert reason accoording to the given API. +pub fn decode_reverted_abi(msg: &str, abi: ethabi::AbiError) -> Option> { + let raw = decode_reverted_raw(msg)?; + + // Strip (and validate) error signature. + let signature = abi.signature(); + let raw = raw.strip_prefix(&signature.as_bytes()[..4])?; + + Some(abi.decode(raw).unwrap()) +} + +/// Decode a base64-encoded revert reason. +pub fn decode_reverted_raw(msg: &str) -> Option> { + // Trim the optional reverted prefix. + let msg = msg.trim_start_matches("reverted: "); + + BASE64_STANDARD.decode(msg).ok() +} diff --git a/runtime-sdk/modules/evm-new/src/raw_tx.rs b/runtime-sdk/modules/evm-new/src/raw_tx.rs new file mode 100644 index 0000000000..763863413d --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/raw_tx.rs @@ -0,0 +1,444 @@ +use std::convert::TryInto; + +use anyhow::{anyhow, Context as _}; +use ethereum::{self, EnvelopedDecodable}; +use k256::elliptic_curve::scalar::IsHigh; + +use oasis_runtime_sdk::{ + crypto::signature, + types::{address, token, transaction}, +}; + +use crate::types; + +pub fn recover_low( + sig: &k256::ecdsa::Signature, + sig_recid: k256::ecdsa::RecoveryId, + sig_hash: &primitive_types::H256, +) -> Result { + if sig.s().is_high().into() { + return Err(anyhow!("signature s high")); + } + k256::ecdsa::VerifyingKey::recover_from_prehash( + sig_hash.as_fixed_bytes().as_ref(), + sig, + sig_recid, + ) + .with_context(|| "recover verify key from digest") +} + +pub fn decode( + body: &[u8], + expected_chain_id: Option, + min_gas_price: u128, + denom: &token::Denomination, +) -> Result { + let ( + chain_id, + sig, + sig_recid, + sig_hash, + eth_action, + eth_value, + eth_input, + eth_nonce, + eth_gas_price, + eth_gas_limit, + ) = match ethereum::TransactionV2::decode(body) + .map_err(|_| anyhow!("decoding transaction rlp"))? + { + ethereum::TransactionV2::Legacy(eth_tx) => { + let sig = k256::ecdsa::Signature::from_scalars( + eth_tx.signature.r().to_fixed_bytes(), + eth_tx.signature.s().to_fixed_bytes(), + ) + .with_context(|| "signature from_scalars")?; + let sig_recid = k256::ecdsa::RecoveryId::from_byte(eth_tx.signature.standard_v()) + .ok_or(anyhow!("bad recovery id"))?; + let message = ethereum::LegacyTransactionMessage::from(eth_tx); + + ( + message.chain_id.or(expected_chain_id), + sig, + sig_recid, + message.hash(), + message.action, + message.value, + message.input, + message.nonce, + message.gas_price, + message.gas_limit, + ) + } + ethereum::TransactionV2::EIP2930(eth_tx) => { + let sig = k256::ecdsa::Signature::from_scalars( + eth_tx.r.to_fixed_bytes(), + eth_tx.s.to_fixed_bytes(), + ) + .with_context(|| "signature from_scalars")?; + let sig_recid = k256::ecdsa::RecoveryId::new(eth_tx.odd_y_parity, false); + let message = ethereum::EIP2930TransactionMessage::from(eth_tx); + + ( + Some(message.chain_id), + sig, + sig_recid, + message.hash(), + message.action, + message.value, + message.input, + message.nonce, + message.gas_price, + message.gas_limit, + ) + } + ethereum::TransactionV2::EIP1559(eth_tx) => { + let sig = k256::ecdsa::Signature::from_scalars( + eth_tx.r.to_fixed_bytes(), + eth_tx.s.to_fixed_bytes(), + ) + .with_context(|| "signature from_scalars")?; + let sig_recid = k256::ecdsa::RecoveryId::new(eth_tx.odd_y_parity, false); + let message = ethereum::EIP1559TransactionMessage::from(eth_tx); + + if message.max_fee_per_gas < message.max_priority_fee_per_gas { + return Err(anyhow!("invalid gas price")); + } + let base_fee_per_gas = min_gas_price.into(); + if message.max_fee_per_gas < base_fee_per_gas { + return Err(anyhow!("gas price too low")); + } + + let priority_fee_per_gas = std::cmp::min( + message.max_priority_fee_per_gas, + message.max_fee_per_gas.saturating_sub(base_fee_per_gas), + ); + let effective_gas_price = priority_fee_per_gas.saturating_add(base_fee_per_gas); + + ( + Some(message.chain_id), + sig, + sig_recid, + message.hash(), + message.action, + message.value, + message.input, + message.nonce, + effective_gas_price, + message.gas_limit, + ) + } + }; + if chain_id != expected_chain_id { + return Err(anyhow!( + "chain ID {:?}, expected {:?}", + chain_id, + expected_chain_id + )); + } + let (method, body) = match eth_action { + ethereum::TransactionAction::Call(eth_address) => ( + "evm.Call", + cbor::to_value(types::Call { + address: eth_address.into(), + value: eth_value.into(), + data: eth_input, + }), + ), + ethereum::TransactionAction::Create => ( + "evm.Create", + cbor::to_value(types::Create { + value: eth_value.into(), + init_code: eth_input, + }), + ), + }; + let key = recover_low(&sig, sig_recid, &sig_hash)?; + let nonce: u64 = eth_nonce + .try_into() + .map_err(|e| anyhow!("converting nonce: {}", e))?; + let gas_price: u128 = eth_gas_price + .try_into() + .map_err(|e| anyhow!("converting gas price: {}", e))?; + let gas_limit: u64 = eth_gas_limit + .try_into() + .map_err(|e| anyhow!("converting gas limit: {}", e))?; + let resolved_fee_amount = gas_price + .checked_mul(gas_limit as u128) + .ok_or_else(|| anyhow!("computing total fee amount"))?; + + Ok(transaction::Transaction { + version: transaction::LATEST_TRANSACTION_VERSION, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: method.to_owned(), + body, + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo { + address_spec: transaction::AddressSpec::Signature( + address::SignatureAddressSpec::Secp256k1Eth( + signature::secp256k1::PublicKey::from_bytes( + k256::EncodedPoint::from(&key).as_bytes(), + ) + .with_context(|| "sdk secp256k1 public key from bytes")?, + ), + ), + nonce, + }], + fee: transaction::Fee { + amount: token::BaseUnits::new(resolved_fee_amount, denom.clone()), + gas: gas_limit, + consensus_messages: 0, // Dynamic number of consensus messages, limited by gas. + proxy: None, + }, + ..Default::default() + }, + }) +} + +#[cfg(test)] +mod test { + use std::str::FromStr as _; + + use hex::FromHex as _; + + use oasis_runtime_sdk::types::token; + + use crate::{derive_caller, types}; + + use super::decode; + + #[allow(clippy::too_many_arguments)] + fn decode_expect_call( + raw: &str, + expected_chain_id: Option, + expected_to: &str, + expected_value: u128, + expected_data: &str, + expected_gas_limit: u64, + expected_gas_price: u128, + expected_from: &str, + expected_nonce: u64, + min_gas_price: u128, + ) { + let tx = decode( + &Vec::from_hex(raw).unwrap(), + expected_chain_id, + min_gas_price, + &token::Denomination::NATIVE, + ) + .unwrap(); + println!("{:?}", &tx); + assert_eq!(tx.call.method, "evm.Call"); + let body: types::Call = cbor::from_value(tx.call.body).unwrap(); + assert_eq!(body.address, types::H160::from_str(expected_to).unwrap()); + assert_eq!(body.value, types::U256::from(expected_value)); + assert_eq!(body.data, Vec::from_hex(expected_data).unwrap()); + assert_eq!(tx.auth_info.signer_info.len(), 1); + assert_eq!( + derive_caller::from_tx_auth_info(&tx.auth_info).unwrap(), + types::H160::from_str(expected_from).unwrap(), + ); + assert_eq!(tx.auth_info.signer_info[0].nonce, expected_nonce); + assert_eq!( + tx.auth_info.fee.amount.0, + expected_gas_limit as u128 * expected_gas_price, + ); + assert_eq!(tx.auth_info.fee.amount.1, token::Denomination::NATIVE); + assert_eq!(tx.auth_info.fee.gas, expected_gas_limit); + } + + #[allow(clippy::too_many_arguments)] + fn decode_expect_create( + raw: &str, + expected_chain_id: Option, + expected_value: u128, + expected_init_code: &str, + expected_gas_limit: u64, + expected_gas_price: u128, + expected_from: &str, + expected_nonce: u64, + min_gas_price: u128, + ) { + let tx = decode( + &Vec::from_hex(raw).unwrap(), + expected_chain_id, + min_gas_price, + &token::Denomination::NATIVE, + ) + .unwrap(); + println!("{:?}", &tx); + assert_eq!(tx.call.method, "evm.Create"); + let body: types::Create = cbor::from_value(tx.call.body).unwrap(); + assert_eq!(body.value, types::U256::from(expected_value)); + assert_eq!(body.init_code, Vec::from_hex(expected_init_code).unwrap()); + assert_eq!(tx.auth_info.signer_info.len(), 1); + assert_eq!( + derive_caller::from_tx_auth_info(&tx.auth_info).unwrap(), + types::H160::from_str(expected_from).unwrap(), + ); + assert_eq!(tx.auth_info.signer_info[0].nonce, expected_nonce); + assert_eq!( + tx.auth_info.fee.amount.0, + expected_gas_limit as u128 * expected_gas_price, + ); + assert_eq!(tx.auth_info.fee.amount.1, token::Denomination::NATIVE); + assert_eq!(tx.auth_info.fee.gas, expected_gas_limit); + } + + fn decode_expect_invalid(raw: &str, expected_chain_id: Option) { + let e = decode( + &Vec::from_hex(raw).unwrap(), + expected_chain_id, + 0, + &token::Denomination::NATIVE, + ) + .unwrap_err(); + eprintln!("Decoding error (expected): {:?}", e); + } + + fn decode_expect_from_mismatch( + raw: &str, + expected_chain_id: Option, + unexpected_from: &str, + ) { + match decode( + &Vec::from_hex(raw).unwrap(), + expected_chain_id, + 0, + &token::Denomination::NATIVE, + ) { + Ok(tx) => { + assert_ne!( + derive_caller::from_tx_auth_info(&tx.auth_info).unwrap(), + types::H160::from_str(unexpected_from).unwrap(), + ); + } + Err(e) => { + // Returning Err is fine too. + eprintln!("Decoding error (expected): {:?}", e); + } + } + } + + #[test] + fn test_decode_basic() { + // https://github.com/ethereum/tests/blob/v10.0/BasicTests/txtest.json + let legacy_tx = "f86b8085e8d4a510008227109413978aee95f38490e9769c39b2773ed763d9cd5f872386f26fc10000801ba0eab47c1a49bf2fe5d40e01d313900e19ca485867d462fe06e139e3a536c6d4f4a014a569d327dcda4b29f74f93c0e9729d2f49ad726e703f9cd90dbb0fbf6649f1"; + decode_expect_call( + legacy_tx, + None, + "13978aee95f38490e9769c39b2773ed763d9cd5f", + 10_000_000_000_000_000, + "", + 10_000, + 1_000_000_000_000, + // "cow" test account + "cd2a3d9f938e13cd947ec05abc7fe734df8dd826", + 0, + 1_000, + ); + decode_expect_call( + legacy_tx, + Some(1), // Legacy pre-EIP-155 transaction should work with any chain ID. + "13978aee95f38490e9769c39b2773ed763d9cd5f", + 10_000_000_000_000_000, + "", + 10_000, + 1_000_000_000_000, + // "cow" test account + "cd2a3d9f938e13cd947ec05abc7fe734df8dd826", + 0, + 1_000, + ); + decode_expect_create( + // We're using a transaction normalized from the original (below) to have low `s`. + // f87f8085e8d4a510008227108080af6025515b525b600a37f260003556601b596020356000355760015b525b54602052f260255860005b525b54602052f21ba05afed0244d0da90b67cf8979b0f246432a5112c0d31e8d5eedd2bc17b171c694a0bb1035c834677c2e1185b8dc90ca6d1fa585ab3d7ef23707e1a497a98e752d1b + "f87f8085e8d4a510008227108080af6025515b525b600a37f260003556601b596020356000355760015b525b54602052f260255860005b525b54602052f21ca05afed0244d0da90b67cf8979b0f246432a5112c0d31e8d5eedd2bc17b171c694a044efca37cb9883d1ee7a47236f3592df152931a930566933de2dc6e341c11426", + None, + 0, + "6025515b525b600a37f260003556601b596020356000355760015b525b54602052f260255860005b525b54602052f2", + 10_000, + 1_000_000_000_000, + // "horse" test account + "13978aee95f38490e9769c39b2773ed763d9cd5f", + 0, + 1_000, + ); + } + + #[test] + fn test_decode_chain_id() { + // Test with mismatching expect_chain_id to exercise our check. + decode_expect_invalid( + // Taken from test_decode_types with chain ID of 1. + "01f86301028203e882c35094cccccccccccccccccccccccccccccccccccccccc8080c080a0260f95e555a1282ef49912ff849b2007f023c44529dc8fb7ecca7693cccb64caa06252cf8af2a49f4cb76fd7172feaece05124edec02db242886b36963a30c2606", + Some(5), + ); + } + + #[test] + fn test_decode_types() { + // https://github.com/ethereum/tests/blob/v10.0/BlockchainTests/ValidBlocks/bcEIP1559/transType.json + + // Legacy. + decode_expect_call( + "f861018203e882c35094cccccccccccccccccccccccccccccccccccccccc80801ca021539ef96c70ab75350c594afb494458e211c8c722a7a0ffb7025c03b87ad584a01d5395fe48edb306f614f0cd682b8c2537537f5fd3e3275243c42e9deff8e93d", + None, + "cccccccccccccccccccccccccccccccccccccccc", + 0, + "", + 50_000, + 1_000, + "d02d72e067e77158444ef2020ff2d325f929b363", + 1, + 1_000, + ); + + // Legacy. + decode_expect_call( + "01f86301028203e882c35094cccccccccccccccccccccccccccccccccccccccc8080c080a0260f95e555a1282ef49912ff849b2007f023c44529dc8fb7ecca7693cccb64caa06252cf8af2a49f4cb76fd7172feaece05124edec02db242886b36963a30c2606", + Some(1), + "cccccccccccccccccccccccccccccccccccccccc", + 0, + "", + 50_000, + 1_000, + "d02d72e067e77158444ef2020ff2d325f929b363", + 2, + 1_000, + ); + + // EIP-1559 + // maxFeePerGas = 1000 + // maxPriorityFeePerGas = 100 + decode_expect_call( + "02f8640103648203e882c35094cccccccccccccccccccccccccccccccccccccccc8080c001a08480e6848952a15ae06192b8051d213d689bdccdf8f14cf69f61725e44e5e80aa057c2af627175a2ac812dab661146dfc7b9886e885c257ad9c9175c3fcec2202e", + Some(1), + "cccccccccccccccccccccccccccccccccccccccc", + 0, + "", + 50_000, + 500, // min(100, 1000 - 400) + 400 + "d02d72e067e77158444ef2020ff2d325f929b363", + 3, + 400, + ); + } + + #[test] + fn test_decode_verify() { + // Altered signature, out of bounds r = n. + decode_expect_invalid("f86b8085e8d4a510008227109413978aee95f38490e9769c39b2773ed763d9cd5f872386f26fc10000801ba0fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141a014a569d327dcda4b29f74f93c0e9729d2f49ad726e703f9cd90dbb0fbf6649f1", None); + // Altered signature, high s. + decode_expect_invalid("f86b8085e8d4a510008227109413978aee95f38490e9769c39b2773ed763d9cd5f872386f26fc10000801ca0eab47c1a49bf2fe5d40e01d313900e19ca485867d462fe06e139e3a536c6d4f4a0eb5a962cd82325b4d608b06c3f168d618b652f7440d8609ee6c4a37d10cff750", None); + // Altered signature, s decreased by one. + decode_expect_from_mismatch( + "f86b8085e8d4a510008227109413978aee95f38490e9769c39b2773ed763d9cd5f872386f26fc10000801ba0eab47c1a49bf2fe5d40e01d313900e19ca485867d462fe06e139e3a536c6d4f4a014a569d327dcda4b29f74f93c0e9729d2f49ad726e703f9cd90dbb0fbf6649f0", + None, + "cd2a3d9f938e13cd947ec05abc7fe734df8dd826", + ); + } +} diff --git a/runtime-sdk/modules/evm-new/src/signed_call.rs b/runtime-sdk/modules/evm-new/src/signed_call.rs new file mode 100644 index 0000000000..ed1d05c61d --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/signed_call.rs @@ -0,0 +1,336 @@ +use std::convert::TryFrom as _; + +use ethabi::Token; +use once_cell::sync::OnceCell; +use sha3::{Digest as _, Keccak256}; + +use oasis_runtime_sdk::{ + context::Context, core::common::crypto::hash::Hash, modules::accounts::API as _, + state::CurrentState, +}; + +use crate::{ + state, + types::{Leash, SimulateCallQuery}, + Config, Error, Runtime, +}; + +/// Verifies the signature on signed query and whether it is appropriately leashed. +/// +/// See [`crate::types::SignedSimulateCallEnvelope`] for details on the signature format. +pub(crate) fn verify( + ctx: &C, + query: SimulateCallQuery, + leash: Leash, + mut signature: [u8; 65], +) -> Result { + // First, verify the signature since it's cheap compared to accessing state to verify the leash. + if signature[64] >= 27 { + // Some wallets generate a high recovery id, which isn't tolerated by the ecdsa crate. + signature[64] -= 27 + } + let sig = k256::ecdsa::Signature::try_from(&signature[..64]) + .map_err(|_| Error::InvalidSignedSimulateCall("invalid signature"))?; + let sig_recid = k256::ecdsa::RecoveryId::from_byte(signature[64]) + .ok_or(Error::InvalidSignedSimulateCall("invalid signature"))?; + let signed_message = hash_call_toplevel::(&query, &leash); + let signer_pk = crate::raw_tx::recover_low(&sig, sig_recid, &signed_message.into()) + .map_err(|_| Error::InvalidSignedSimulateCall("signature recovery failed"))?; + let signer_addr_digest = Keccak256::digest(&signer_pk.to_encoded_point(false).as_bytes()[1..]); + if &signer_addr_digest[12..] != query.caller.as_ref() { + return Err(Error::InvalidSignedSimulateCall("signer != caller")); + } + + // Next, verify the leash. + let current_block = ctx.runtime_header().round; + let sdk_address = Cfg::map_address(query.caller.0.into()); + let nonce = ::Accounts::get_nonce(sdk_address).unwrap(); + if nonce > leash.nonce { + return Err(Error::InvalidSignedSimulateCall("stale nonce")); + } + + let base_block_hash = CurrentState::with_store(|store| { + let block_hashes = state::block_hashes(store); + match block_hashes.get::<_, Hash>(&leash.block_number.to_be_bytes()) { + Some(hash) => Ok(hash), + None => Err(Error::InvalidSignedSimulateCall("base block not found")), + } + })?; + if base_block_hash.as_ref() != leash.block_hash.as_ref() { + return Err(Error::InvalidSignedSimulateCall("unexpected base block")); + } + + #[allow(clippy::unnecessary_lazy_evaluations)] + let block_delta = current_block + .checked_sub(leash.block_number) + .unwrap_or_else(|| leash.block_number - current_block); + if block_delta > leash.block_range { + return Err(Error::InvalidSignedSimulateCall( + "current block out of range", + )); + } + + Ok(query) +} + +macro_rules! leash_type_str { + () => { + concat!( + "Leash", + "(", + "uint64 nonce", + ",uint64 blockNumber", + ",bytes32 blockHash", + ",uint64 blockRange", + ")", + ) + }; +} + +fn hash_call_toplevel(query: &SimulateCallQuery, leash: &Leash) -> [u8; 32] { + let call_struct_hash = hash_call(query, leash); + let domain_separator = hash_domain::(); + let mut encoded_call = [0u8; 66]; + encoded_call[0..2].copy_from_slice(b"\x19\x01"); + encoded_call[2..34].copy_from_slice(domain_separator); + encoded_call[34..].copy_from_slice(&call_struct_hash); + Keccak256::digest(encoded_call).into() +} + +fn hash_call(query: &SimulateCallQuery, leash: &Leash) -> [u8; 32] { + const CALL_TYPE_STR: &str = concat!( + "Call", + "(", + "address from", + ",address to", + ",uint64 gasLimit", + ",uint256 gasPrice", + ",uint256 value", + ",bytes data", + ",Leash leash", + ")", + leash_type_str!() + ); + hash_encoded(&[ + encode_bytes(CALL_TYPE_STR), + Token::Address(query.caller.0.into()), + Token::Address(query.address.unwrap_or_default().0.into()), + Token::Uint(query.gas_limit.into()), + Token::Uint(ethabi::ethereum_types::U256(query.gas_price.0)), + Token::Uint(ethabi::ethereum_types::U256(query.value.0)), + encode_bytes(&query.data), + Token::Uint(hash_leash(leash).into()), + ]) +} + +fn hash_leash(leash: &Leash) -> [u8; 32] { + hash_encoded(&[ + encode_bytes(leash_type_str!()), + Token::Uint(leash.nonce.into()), + Token::Uint(leash.block_number.into()), + Token::Uint(leash.block_hash.0.into()), + Token::Uint(leash.block_range.into()), + ]) +} + +fn hash_domain() -> &'static [u8; 32] { + static DOMAIN_SEPARATOR: OnceCell<[u8; 32]> = OnceCell::new(); // Not `Lazy` because of generic. + DOMAIN_SEPARATOR.get_or_init(|| { + const DOMAIN_TYPE_STR: &str = "EIP712Domain(string name,string version,uint256 chainId)"; + hash_encoded(&[ + encode_bytes(DOMAIN_TYPE_STR), + encode_bytes("oasis-runtime-sdk/evm: signed query"), + encode_bytes("1.0.0"), + Token::Uint(Cfg::CHAIN_ID.into()), + ]) + }) +} + +fn encode_bytes(s: impl AsRef<[u8]>) -> Token { + Token::FixedBytes(Keccak256::digest(s.as_ref()).to_vec()) +} + +fn hash_encoded(tokens: &[Token]) -> [u8; 32] { + Keccak256::digest(ethabi::encode(tokens)).into() +} + +#[cfg(test)] +mod test { + use super::*; + + use oasis_runtime_sdk::{modules::accounts, testing::mock}; + + use crate::{ + test::{ConfidentialEVMConfig as C10lCfg, EVMConfig as Cfg}, + types::{SignedCallDataPack, SimulateCallQuery, H160}, + Module as EVMModule, + }; + + type Accounts = accounts::Module; + + /// This was generated using the `@oasislabs/sapphire-paratime` JS lib. + const SIGNED_CALL_DATA_PACK: &str = +"a36464617461a164626f64794401020304656c65617368a4656e6f6e63651903e76a626c6f636b5f686173685820c92b675c7013e33aa88feaae520eb0ede155e7cacb3c4587e0923cba9953f8bb6b626c6f636b5f72616e6765036c626c6f636b5f6e756d626572182a697369676e6174757265584148bca100e84d13a80b131c62b9b87caf07e4da6542a9e1ea16d8042ba08cc1e31f10ae924d8c137882204e9217423194014ce04fa2130c14f27b148858733c7b1c"; + + fn make_signed_call() -> (SimulateCallQuery, SignedCallDataPack) { + let data_pack: SignedCallDataPack = + cbor::from_slice(&hex::decode(SIGNED_CALL_DATA_PACK).unwrap()).unwrap(); + ( + SimulateCallQuery { + gas_price: 123u64.into(), + gas_limit: 10, + caller: "0x11e244400Cf165ade687077984F09c3A037b868F" + .parse() + .unwrap(), + address: Some( + "0xb5ed90452AAC09f294a0BE877CBf2Dc4D55e096f" + .parse() + .unwrap(), + ), + value: 42u64.into(), + data: cbor::from_value(data_pack.data.body.clone()).unwrap(), + }, + data_pack, + ) + } + + fn setup_nonce(caller: &H160, leash: &Leash) { + let sdk_address = C10lCfg::map_address((*caller).0.into()); + Accounts::set_nonce(sdk_address, leash.nonce); + } + + fn setup_stale_nonce(caller: &H160, leash: &Leash) { + let sdk_address = C10lCfg::map_address((*caller).0.into()); + Accounts::set_nonce(sdk_address, leash.nonce + 1); + } + + fn setup_block(leash: &Leash) { + CurrentState::with_store(|store| { + let mut block_hashes = state::block_hashes(store); + block_hashes.insert::<_, Hash>( + &leash.block_number.to_be_bytes(), + leash.block_hash.as_ref().into(), + ); + }); + } + + #[test] + fn test_verify_ok() { + let (query, data_pack) = make_signed_call(); + + let mut mock = mock::Mock::default(); + mock.runtime_header.round = data_pack.leash.block_number; + let mut ctx = mock.create_ctx(); + + setup_nonce(&query.caller, &data_pack.leash); + setup_block(&data_pack.leash); + + verify::<_, C10lCfg>(&mut ctx, query, data_pack.leash, data_pack.signature).unwrap(); + } + + #[test] + fn test_verify_bad_signature() { + let (query, mut data_pack) = make_signed_call(); + + let mut mock = mock::Mock::default(); + mock.runtime_header.round = data_pack.leash.block_number; + let mut ctx = mock.create_ctx(); + + setup_nonce(&query.caller, &data_pack.leash); + setup_block(&data_pack.leash); + + data_pack.signature[0] ^= 1; + assert!(matches!( + verify::<_, C10lCfg>(&mut ctx, query, data_pack.leash, data_pack.signature) + .unwrap_err(), + Error::InvalidSignedSimulateCall("signer != caller") + )); + } + + #[test] + fn test_verify_bad_nonce() { + let (query, data_pack) = make_signed_call(); + + let mut mock = mock::Mock::default(); + mock.runtime_header.round = data_pack.leash.block_number; + let mut ctx = mock.create_ctx(); + + setup_stale_nonce(&query.caller, &data_pack.leash); + setup_block(&data_pack.leash); + + assert!(matches!( + verify::<_, C10lCfg>(&mut ctx, query, data_pack.leash, data_pack.signature) + .unwrap_err(), + Error::InvalidSignedSimulateCall("stale nonce") + )); + } + + #[test] + fn test_verify_bad_base_block() { + let (query, data_pack) = make_signed_call(); + + let mut mock = mock::Mock::default(); + mock.runtime_header.round = data_pack.leash.block_number; + let mut ctx = mock.create_ctx(); + + setup_nonce(&query.caller, &data_pack.leash); + + assert!(matches!( + verify::<_, C10lCfg>(&mut ctx, query, data_pack.leash, data_pack.signature) + .unwrap_err(), + Error::InvalidSignedSimulateCall("base block not found") + )); + } + + #[test] + fn test_verify_bad_range() { + let (query, data_pack) = make_signed_call(); + + let mut mock = mock::Mock::default(); + let mut ctx = mock.create_ctx(); + + setup_nonce(&query.caller, &data_pack.leash); + setup_block(&data_pack.leash); + + assert!(matches!( + verify::<_, C10lCfg>(&mut ctx, query, data_pack.leash, data_pack.signature) + .unwrap_err(), + Error::InvalidSignedSimulateCall("current block out of range") + )); + } + + #[test] + fn test_decode_simulate_call_query() { + let (unsigned_body, data_pack) = make_signed_call(); + let signed_body = SimulateCallQuery { + data: cbor::to_vec(data_pack.clone()), + ..unsigned_body + }; + + let mut mock = mock::Mock::default(); + let mut ctx = mock.create_ctx(); + + let mut c10l_mock = mock::Mock::default(); + c10l_mock.runtime_header.round = data_pack.leash.block_number; + let mut c10l_ctx = c10l_mock.create_ctx(); + + setup_nonce(&signed_body.caller, &data_pack.leash); + setup_block(&data_pack.leash); + + let mut non_c10l_decode = |body: &SimulateCallQuery| { + EVMModule::::decode_simulate_call_query(&mut ctx, body.clone()) + }; + let mut c10l_decode = |body: &SimulateCallQuery| { + EVMModule::::decode_simulate_call_query(&mut c10l_ctx, body.clone()) + }; + + assert!(EVMModule::::decode_simulate_call_query( + &mut mock::Mock::default().create_ctx(), + signed_body.clone() + ) + .is_err()); // Check that errors are propagated (in this case leash invalidity). + + assert_eq!(c10l_decode(&signed_body).unwrap().0, unsigned_body); + assert_eq!(non_c10l_decode(&unsigned_body).unwrap().0, unsigned_body); + } +} diff --git a/runtime-sdk/modules/evm-new/src/state.rs b/runtime-sdk/modules/evm-new/src/state.rs new file mode 100644 index 0000000000..71d35fe4be --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/state.rs @@ -0,0 +1,122 @@ +use oasis_runtime_sdk::{ + context::Context, + state::CurrentState, + storage::{ConfidentialStore, HashedStore, PrefixStore, Store, TypedStore}, +}; + +use crate::{types::H160, Config}; + +/// Prefix for Ethereum account code in our storage (maps H160 -> Vec). +pub const CODES: &[u8] = &[0x01]; +/// Prefix for Ethereum account storage in our storage (maps H160||H256 -> H256). +pub const STORAGES: &[u8] = &[0x02]; +/// Prefix for Ethereum block hashes (only for last BLOCK_HASH_WINDOW_SIZE blocks +/// excluding current) storage in our storage (maps Round -> H256). +pub const BLOCK_HASHES: &[u8] = &[0x03]; +/// Prefix for Ethereum account storage in our confidential storage (maps H160||H256 -> H256). +pub const CONFIDENTIAL_STORAGES: &[u8] = &[0x04]; + +/// Confidential store key pair ID domain separation context base. +pub const CONFIDENTIAL_STORE_KEY_PAIR_ID_CONTEXT_BASE: &[u8] = b"oasis-runtime-sdk/evm: state"; +const CONTEXT_KEY_CONFIDENTIAL_STORE_INSTANCE_COUNT: &str = "evm.ConfidentialStoreCounter"; + +/// The number of hash blocks that can be obtained from the current blockchain. +pub const BLOCK_HASH_WINDOW_SIZE: u64 = 256; + +/// Run closure with the store of the provided contract address. Based on configuration this will +/// be either confidential or public storage. +pub fn with_storage(ctx: &C, address: &H160, f: F) -> R +where + Cfg: Config, + C: Context, + F: FnOnce(&mut TypedStore<&mut dyn Store>) -> R, +{ + if Cfg::CONFIDENTIAL { + with_confidential_storage(ctx, address, f) + } else { + with_public_storage(address, f) + } +} + +/// Run closure with the public store of the provided contract address. +pub fn with_public_storage(address: &H160, f: F) -> R +where + F: FnOnce(&mut TypedStore<&mut dyn Store>) -> R, +{ + CurrentState::with_store(|store| { + let mut store = + HashedStore::<_, blake3::Hasher>::new(contract_storage(store, STORAGES, address)); + let mut store = TypedStore::new(&mut store as &mut dyn Store); + f(&mut store) + }) +} + +/// Run closure with the confidential store of the provided contract address. +pub fn with_confidential_storage<'a, C, F, R>(ctx: &'a C, address: &'a H160, f: F) -> R +where + C: Context, + F: FnOnce(&mut TypedStore<&mut dyn Store>) -> R, +{ + let kmgr_client = ctx + .key_manager() + .expect("key manager must be available to use confidentiality"); + let key_id = oasis_runtime_sdk::keymanager::get_key_pair_id([ + CONFIDENTIAL_STORE_KEY_PAIR_ID_CONTEXT_BASE, + address.as_ref(), + ]); + let keypair = kmgr_client + .get_or_create_keys(key_id) + .expect("unable to retrieve confidential storage keys"); + let confidential_key = keypair.state_key; + + // These values are used to derive the confidential store nonce: + let round = ctx.runtime_header().round; + let instance_count: usize = CurrentState::with(|state| { + // One state is used per tx batch, so the instance count will monotonically increase. + let cnt = *state + .block_value(CONTEXT_KEY_CONFIDENTIAL_STORE_INSTANCE_COUNT) + .or_default(); + state + .block_value(CONTEXT_KEY_CONFIDENTIAL_STORE_INSTANCE_COUNT) + .set(cnt + 1); + cnt + }); + + CurrentState::with(|state| { + let mode = state.env().mode() as u8; + let contract_storages = contract_storage(state.store(), CONFIDENTIAL_STORAGES, address); + let mut confidential_storages = ConfidentialStore::new_with_key( + contract_storages, + confidential_key.0, + &[ + round.to_le_bytes().as_slice(), + instance_count.to_le_bytes().as_slice(), + &[mode], + ], + ); + let mut store = TypedStore::new(&mut confidential_storages as &mut dyn Store); + f(&mut store) + }) +} + +fn contract_storage<'a, S: Store + 'a>( + state: S, + prefix: &'a [u8], + address: &'a H160, +) -> PrefixStore { + let store = PrefixStore::new(state, &crate::MODULE_NAME); + let storages = PrefixStore::new(store, prefix); + PrefixStore::new(storages, address) +} + +/// Get a typed store for codes of all contracts. +pub fn codes<'a, S: Store + 'a>(state: S) -> TypedStore { + let store = PrefixStore::new(state, &crate::MODULE_NAME); + TypedStore::new(PrefixStore::new(store, &CODES)) +} + +/// Get a typed store for historic block hashes. +pub fn block_hashes<'a, S: Store + 'a>(state: S) -> TypedStore { + let store = PrefixStore::new(state, &crate::MODULE_NAME); + TypedStore::new(PrefixStore::new(store, &BLOCK_HASHES)) +} diff --git a/runtime-sdk/modules/evm-new/src/test.rs b/runtime-sdk/modules/evm-new/src/test.rs new file mode 100644 index 0000000000..7a777f9729 --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/test.rs @@ -0,0 +1,1203 @@ +//! Tests for the EVM module. +use std::collections::BTreeMap; + +use ethabi::{ParamType, Token}; +use sha3::Digest as _; +use uint::hex::FromHex; + +use oasis_runtime_sdk::{ + callformat, + crypto::{self, signature::secp256k1}, + error::Error as _, + module::{self, InvariantHandler as _, TransactionHandler as _}, + modules::{ + accounts::{self, Module as Accounts, ADDRESS_FEE_ACCUMULATOR, API as _}, + core::{self, Module as Core}, + }, + state::{self, CurrentState, Mode, Options, TransactionResult}, + testing::{keys, mock, mock::CallOptions}, + types::{ + address::{Address, SignatureAddressSpec}, + token::{self, Denomination}, + transaction, + transaction::Fee, + }, + Runtime, Version, +}; + +use crate::{ + derive_caller, + mock::{decode_reverted, decode_reverted_raw, load_contract_bytecode, EvmSigner, QueryOptions}, + types::{self, H160}, + Config, Genesis, Module as EVMModule, +}; + +/// Test contract code. +static TEST_CONTRACT_CODE_HEX: &str = + include_str!("../../../../tests/e2e/evm/contracts/evm_erc20_test_compiled.hex"); +static FAUCET_CONTRACT_CODE_HEX: &str = + include_str!("../../../../tests/e2e/evm/contracts/faucet/faucet.hex"); + +pub(crate) struct EVMConfig; + +impl Config for EVMConfig { + // type AdditionalPrecompileSet = (); // XXX + + const CHAIN_ID: u64 = 0xa515; + + const TOKEN_DENOMINATION: Denomination = Denomination::NATIVE; +} + +pub(crate) struct ConfidentialEVMConfig; + +impl Config for ConfidentialEVMConfig { + // type AdditionalPrecompileSet = (); // XXX + + const CHAIN_ID: u64 = 0x5afe; + + const TOKEN_DENOMINATION: Denomination = Denomination::NATIVE; + + const CONFIDENTIAL: bool = true; +} + +fn load_erc20() -> Vec { + Vec::from_hex( + TEST_CONTRACT_CODE_HEX + .split_whitespace() + .collect::(), + ) + .expect("compiled ERC20 contract should be a valid hex string") +} + +fn check_derivation(seed: &str, priv_hex: &str, addr_hex: &str) { + let priv_bytes = sha3::Keccak256::digest(seed.as_bytes()); + assert_eq!( + priv_bytes.as_slice(), + Vec::from_hex(priv_hex).unwrap().as_slice() + ); + let priv_key = k256::ecdsa::SigningKey::from_bytes(&priv_bytes).unwrap(); + let pub_key = priv_key.verifying_key(); + let sdk_pub_key = + secp256k1::PublicKey::from_bytes(pub_key.to_encoded_point(true).as_bytes()).unwrap(); + let addr = + derive_caller::from_sigspec(&SignatureAddressSpec::Secp256k1Eth(sdk_pub_key)).unwrap(); + assert_eq!(addr.as_bytes(), Vec::from_hex(addr_hex).unwrap().as_slice()); +} + +#[test] +fn test_evm_caller_addr_derivation() { + // https://github.com/ethereum/tests/blob/v10.0/BasicTests/keyaddrtest.json + check_derivation( + "cow", + "c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", + "cd2a3d9f938e13cd947ec05abc7fe734df8dd826", + ); + check_derivation( + "horse", + "c87f65ff3f271bf5dc8643484f66b200109caffe4bf98c4cb393dc35740b28c0", + "13978aee95f38490e9769c39b2773ed763d9cd5f", + ); + + let expected = + H160::from_slice(&Vec::::from_hex("dce075e1c39b1ae0b75d554558b6451a226ffe00").unwrap()); + let derived = derive_caller::from_sigspec(&keys::dave::sigspec()).unwrap(); + assert_eq!(derived, expected); +} + +fn do_test_evm_calls(force_plain: bool) { + let mut mock = mock::Mock::default(); + let ctx = mock.create_ctx_for_runtime::(true); + let client_keypair = + oasis_runtime_sdk::core::common::crypto::mrae::deoxysii::generate_key_pair(); + + macro_rules! encode_data { + ($data:expr) => { + if C::CONFIDENTIAL && !force_plain { + cbor::to_vec( + callformat::encode_call( + &ctx, + transaction::Call { + format: transaction::CallFormat::EncryptedX25519DeoxysII, + method: "".into(), + body: cbor::Value::from($data), + ..Default::default() + }, + &client_keypair, + ) + .unwrap(), + ) + } else { + $data + } + }; + } + + macro_rules! decode_result { + ($tx_ctx:ident, $result:expr$(,)?) => { + match $result { + Ok(evm_result) => { + if C::CONFIDENTIAL && !force_plain { + let call_result: transaction::CallResult = + cbor::from_slice(&evm_result).unwrap(); + callformat::decode_result( + &$tx_ctx, + transaction::CallFormat::EncryptedX25519DeoxysII, + call_result, + &client_keypair, + ) + .expect("bad decode") + } else { + module::CallResult::Ok(cbor::Value::from(evm_result)) + } + } + Err(e) => e.into_call_result(), + } + }; + } + + Core::::init(core::Genesis { + parameters: core::Parameters { + max_batch_gas: 10_000_000, + ..Default::default() + }, + }); + + Accounts::init(accounts::Genesis { + balances: { + let mut b = BTreeMap::new(); + // Dave. + b.insert(keys::dave::address(), { + let mut d = BTreeMap::new(); + d.insert(Denomination::NATIVE, 1_000_000); + d + }); + b + }, + total_supplies: { + let mut ts = BTreeMap::new(); + ts.insert(Denomination::NATIVE, 1_000_000); + ts + }, + ..Default::default() + }); + + EVMModule::::init(Genesis { + parameters: Default::default(), + }); + + let erc20 = load_erc20(); + + // Test the Create transaction. + let create_tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: "evm.Create".to_owned(), + body: cbor::to_value(types::Create { + value: 0.into(), + init_code: encode_data!(erc20), + }), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + keys::dave::sigspec(), + 0, + )], + fee: transaction::Fee { + gas: 1_000_000, + ..Default::default() + }, + ..Default::default() + }, + }; + // Run authentication handler to simulate nonce increments. + Accounts::authenticate_tx(&ctx, &create_tx).unwrap(); + + let call = create_tx.call.clone(); + let erc20_addr = + CurrentState::with_transaction_opts(Options::new().with_tx(create_tx.into()), || { + let addr = H160::from_slice( + &EVMModule::::tx_create(&ctx, cbor::from_value(call.body).unwrap()).unwrap(), + ); + EVMModule::::check_invariants(&ctx).expect("invariants should hold"); + + TransactionResult::Commit(addr) + }); + + // Test the Call transaction. + let name_method: Vec = Vec::from_hex("06fdde03".to_owned() + &"0".repeat(64 - 8)).unwrap(); + let call_name_tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: "evm.Call".to_owned(), + body: cbor::to_value(types::Call { + address: erc20_addr, + value: 0.into(), + data: encode_data!(name_method), + }), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + keys::dave::sigspec(), + 1, + )], + fee: transaction::Fee { + gas: 25_000, + ..Default::default() + }, + ..Default::default() + }, + }; + // Run authentication handler to simulate nonce increments. + Accounts::authenticate_tx(&ctx, &call_name_tx).unwrap(); + + let call = call_name_tx.call.clone(); + let erc20_name = + CurrentState::with_transaction_opts(Options::new().with_tx(call_name_tx.into()), || { + let name: Vec = cbor::from_value( + decode_result!( + ctx, + EVMModule::::tx_call(&ctx, cbor::from_value(call.body).unwrap()) + ) + .unwrap(), + ) + .unwrap(); + + EVMModule::::check_invariants(&ctx).expect("invariants should hold"); + + TransactionResult::Commit(name) + }); + assert_eq!(erc20_name.len(), 96); + assert_eq!(erc20_name[63], 0x04); // Name is 4 bytes long. + assert_eq!(erc20_name[64..68], vec![0x54, 0x65, 0x73, 0x74]); // "Test". +} + +#[test] +fn test_evm_calls() { + do_test_evm_calls::(false); +} + +#[test] +fn test_c10l_evm_calls_enc() { + let _guard = crypto::signature::context::test_using_chain_context(); + crypto::signature::context::set_chain_context(Default::default(), "test"); + do_test_evm_calls::(false); +} + +#[test] +fn test_c10l_evm_calls_plain() { + let _guard = crypto::signature::context::test_using_chain_context(); + crypto::signature::context::set_chain_context(Default::default(), "test"); + do_test_evm_calls::(true /* force_plain */); +} + +#[test] +fn test_c10l_evm_balance_transfer() { + let _guard = crypto::signature::context::test_using_chain_context(); + crypto::signature::context::set_chain_context(Default::default(), "test"); + let mut mock = mock::Mock::default(); + let ctx = mock.create_ctx(); + + Core::::init(core::Genesis { + parameters: core::Parameters { + max_batch_gas: 10_000_000, + ..Default::default() + }, + }); + + Accounts::init(accounts::Genesis { + balances: BTreeMap::from([( + keys::dave::address(), + BTreeMap::from([(Denomination::NATIVE, 1_000_000)]), + )]), + total_supplies: BTreeMap::from([(Denomination::NATIVE, 1_000_000)]), + ..Default::default() + }); + + EVMModule::::init(Genesis { + parameters: Default::default(), + }); + + let recipient = ethabi::Address::repeat_byte(42); + let transfer_tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: "evm.Call".to_owned(), + body: cbor::to_value(types::Call { + address: recipient.into(), + value: 12345u64.into(), + data: vec![], + }), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + keys::dave::sigspec(), + 0, + )], + fee: transaction::Fee { + gas: 1_000_000, + ..Default::default() + }, + ..Default::default() + }, + }; + // Run authentication handler to simulate nonce increments. + Accounts::authenticate_tx(&ctx, &transfer_tx).unwrap(); + + let call = transfer_tx.call.clone(); + CurrentState::with_transaction_opts(Options::new().with_tx(transfer_tx.into()), || { + EVMModule::::tx_call(&ctx, cbor::from_value(call.body).unwrap()) + .unwrap(); + EVMModule::::check_invariants(&ctx).expect("invariants should hold"); + }); + + let recipient_balance = EVMModule::::query_balance( + &ctx, + types::BalanceQuery { + address: recipient.into(), + }, + ) + .unwrap(); + assert_eq!(recipient_balance, 12345u64.into()); +} + +#[test] +fn test_c10l_enc_call_identity_decoded() { + // Calls sent using the Oasis encrypted envelope format (not inner-enveloped) + // should not be decoded: + let mut mock = mock::Mock::default(); + let ctx = mock.create_ctx_for_runtime::>(true); + let data = vec![1, 2, 3, 4, 5]; + let (decoded_data, metadata) = EVMModule::::decode_call_data( + &ctx, + data.clone(), + transaction::CallFormat::EncryptedX25519DeoxysII, + 0, + true, + ) + .expect("decode failed") + .expect("km is unreachable"); + assert_eq!(data, decoded_data); + assert!(matches!(metadata, callformat::Metadata::Empty)); +} + +struct CoreConfig; + +impl core::Config for CoreConfig {} + +/// EVM test runtime. +struct EVMRuntime(C); + +impl Runtime for EVMRuntime { + const VERSION: Version = Version::new(0, 0, 0); + + type Core = Core; + type Accounts = Accounts; + + type Modules = (Core, Accounts, EVMModule); + + fn genesis_state() -> ::Genesis { + ( + core::Genesis { + parameters: core::Parameters { + max_batch_gas: 10_000_000, + min_gas_price: BTreeMap::from([(token::Denomination::NATIVE, 0)]), + ..Default::default() + }, + }, + accounts::Genesis { + balances: { + let mut b = BTreeMap::new(); + // Dave. + b.insert(keys::dave::address(), { + let mut d = BTreeMap::new(); + d.insert(Denomination::NATIVE, 1_000_000); + d + }); + b + }, + total_supplies: { + let mut ts = BTreeMap::new(); + ts.insert(Denomination::NATIVE, 1_000_000); + ts + }, + ..Default::default() + }, + Genesis { + parameters: Default::default(), + }, + ) + } +} + +fn do_test_evm_runtime() { + let mut mock = mock::Mock::default(); + let ctx = mock.create_ctx_for_runtime::>(true); + let client_keypair = + oasis_runtime_sdk::core::common::crypto::mrae::deoxysii::generate_key_pair(); + + // This is a macro to avoid mucking with borrow scopes. + macro_rules! encode_data { + ($data:expr) => { + if C::CONFIDENTIAL { + cbor::to_vec( + callformat::encode_call( + &ctx, + transaction::Call { + format: transaction::CallFormat::EncryptedX25519DeoxysII, + method: "".into(), + body: cbor::Value::from($data), + ..Default::default() + }, + &client_keypair, + ) + .unwrap(), + ) + } else { + $data + } + }; + } + + macro_rules! decode_result { + ($tx_ctx:ident, $result:expr$(,)?) => { + match $result { + Ok(evm_result) => { + if C::CONFIDENTIAL { + let call_result: transaction::CallResult = + cbor::from_slice(&evm_result).unwrap(); + callformat::decode_result( + &$tx_ctx, + transaction::CallFormat::EncryptedX25519DeoxysII, + call_result, + &client_keypair, + ) + .expect("bad decode") + } else { + module::CallResult::Ok(cbor::Value::from(evm_result)) + } + } + Err(e) => e.into_call_result(), + } + }; + } + + EVMRuntime::::migrate(&ctx); + + let erc20 = load_erc20(); + + // Test the Create transaction. + let create_tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: "evm.Create".to_owned(), + body: cbor::to_value(types::Create { + value: 0.into(), + init_code: encode_data!(erc20.clone()), + }), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + keys::dave::sigspec(), + 0, + )], + fee: transaction::Fee { + gas: 1_000_000, + ..Default::default() + }, + ..Default::default() + }, + }; + // Run authentication handler to simulate nonce increments. + as Runtime>::Modules::authenticate_tx(&ctx, &create_tx).unwrap(); + + let call = create_tx.call.clone(); + let erc20_addr = + CurrentState::with_transaction_opts(Options::new().with_tx(create_tx.into()), || { + let addr = H160::from_slice( + &EVMModule::::tx_create(&ctx, cbor::from_value(call.body).unwrap()).unwrap(), + ); + EVMModule::::check_invariants(&ctx).expect("invariants should hold"); + + TransactionResult::Commit(addr) + }); + + // Make sure the derived address matches the expected value. If this fails it likely indicates + // a problem with nonce increment semantics between the SDK and EVM. + assert_eq!( + erc20_addr, + "0x3e6a6598a229b84e1411005d55003d88e3b11067" + .parse() + .unwrap(), + "derived address should be correct" + ); + + // Submitting an invalid create transaction should fail. + let out_of_gas_create = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: "evm.Create".to_owned(), + body: cbor::to_value(types::Create { + value: 0.into(), + init_code: encode_data!(erc20), + }), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + keys::dave::sigspec(), + 1, + )], + fee: transaction::Fee { + gas: 10, // Not enough gas. + ..Default::default() + }, + ..Default::default() + }, + }; + // Run authentication handler to simulate nonce increments. + as Runtime>::Modules::authenticate_tx(&ctx, &out_of_gas_create).unwrap(); + + let call = out_of_gas_create.call.clone(); + CurrentState::with_transaction_opts( + Options::new().with_tx(out_of_gas_create.clone().into()), + || { + assert!(!decode_result!( + ctx, + EVMModule::::tx_create(&ctx, cbor::from_value(call.body).unwrap()) + ) + .is_success()); + }, + ); + + // CheckTx should not fail. + let call = out_of_gas_create.call.clone(); + CurrentState::with_transaction_opts( + Options::new() + .with_mode(state::Mode::Check) + .with_tx(out_of_gas_create.clone().into()), + || { + let rsp = EVMModule::::tx_create(&ctx, cbor::from_value(call.body).unwrap()) + .expect("call should succeed with empty result"); + + assert_eq!( + rsp, + Vec::::new(), + "check tx should return an empty response" + ); + }, + ); + + // Test the Call transaction. + let name_method: Vec = Vec::from_hex("06fdde03".to_owned() + &"0".repeat(64 - 8)).unwrap(); + let call_name_tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: "evm.Call".to_owned(), + body: cbor::to_value(types::Call { + address: erc20_addr, + value: 0.into(), + data: encode_data!(name_method), + }), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + keys::dave::sigspec(), + 2, + )], + fee: transaction::Fee { + gas: 25_000, + ..Default::default() + }, + ..Default::default() + }, + }; + // Run authentication handler to simulate nonce increments. + as Runtime>::Modules::authenticate_tx(&ctx, &call_name_tx).unwrap(); + + // Test transaction call in simulate mode. + let call = call_name_tx.call.clone(); + CurrentState::with_transaction_opts( + Options::new() + .with_mode(Mode::Simulate) + .with_tx(call_name_tx.clone().into()), + || { + let erc20_name: Vec = cbor::from_value( + decode_result!( + ctx, + EVMModule::::tx_call(&ctx, cbor::from_value(call.body).unwrap()) + ) + .unwrap(), + ) + .unwrap(); + + EVMModule::::check_invariants(&ctx).expect("invariants should hold"); + + assert_eq!(erc20_name.len(), 96); + assert_eq!(erc20_name[63], 0x04); // Name is 4 bytes long. + assert_eq!(erc20_name[64..68], vec![0x54, 0x65, 0x73, 0x74]); // "Test". + }, + ); + + let call = call_name_tx.call.clone(); + let erc20_name = + CurrentState::with_transaction_opts(Options::new().with_tx(call_name_tx.into()), || { + let name: Vec = cbor::from_value( + decode_result!( + ctx, + EVMModule::::tx_call(&ctx, cbor::from_value(call.body).unwrap()) + ) + .unwrap(), + ) + .unwrap(); + + EVMModule::::check_invariants(&ctx).expect("invariants should hold"); + + TransactionResult::Commit(name) + }); + assert_eq!(erc20_name.len(), 96); + assert_eq!(erc20_name[63], 0x04); // Name is 4 bytes long. + assert_eq!(erc20_name[64..68], vec![0x54, 0x65, 0x73, 0x74]); // "Test". + + // Test the Call transaction with more complicated parameters + // (transfer 0x1000 coins to 0xc001d00d). + let transfer_method: Vec = Vec::from_hex( + "a9059cbb".to_owned() + + &"0".repeat(64 - 4) + + &"1000".to_owned() + + &"0".repeat(64 - 8) + + &"c001d00d".to_owned(), + ) + .unwrap(); + let call_transfer_tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: "evm.Call".to_owned(), + body: cbor::to_value(types::Call { + address: erc20_addr, + value: 0.into(), + data: encode_data!(transfer_method.clone()), + }), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + keys::dave::sigspec(), + 3, + )], + fee: transaction::Fee { + gas: 64_000, + ..Default::default() + }, + ..Default::default() + }, + }; + // Run authentication handler to simulate nonce increments. + as Runtime>::Modules::authenticate_tx(&ctx, &call_transfer_tx).unwrap(); + + let call = call_transfer_tx.call.clone(); + let transfer_ret = CurrentState::with_transaction_opts( + Options::new().with_tx(call_transfer_tx.into()), + || { + let ret: Vec = cbor::from_value( + decode_result!( + ctx, + EVMModule::::tx_call(&ctx, cbor::from_value(call.body).unwrap()) + ) + .unwrap(), + ) + .unwrap(); + + EVMModule::::check_invariants(&ctx).expect("invariants should hold"); + + TransactionResult::Commit(ret) + }, + ); + assert_eq!( + transfer_ret, + Vec::::from_hex("0".repeat(64 - 1) + &"1".to_owned()).unwrap() + ); // OK. + + // Submitting an invalid call transaction should fail. + let out_of_gas_tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: "evm.Call".to_owned(), + body: cbor::to_value(types::Call { + address: erc20_addr, + value: 0.into(), + data: encode_data!(transfer_method), + }), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + keys::dave::sigspec(), + 4, + )], + fee: transaction::Fee { + gas: 10, // Not enough gas. + ..Default::default() + }, + ..Default::default() + }, + }; + as Runtime>::Modules::authenticate_tx(&ctx, &out_of_gas_tx).unwrap(); + + let call = out_of_gas_tx.call.clone(); + CurrentState::with_transaction_opts( + Options::new().with_tx(out_of_gas_tx.clone().into()), + || { + assert!(!decode_result!( + ctx, + EVMModule::::tx_call(&ctx, cbor::from_value(call.body).unwrap()) + ) + .is_success()); + }, + ); + + // CheckTx should not fail. + let call = out_of_gas_tx.call.clone(); + CurrentState::with_transaction_opts( + Options::new() + .with_mode(state::Mode::Check) + .with_tx(out_of_gas_tx.clone().into()), + || { + let rsp = EVMModule::::tx_call(&ctx, cbor::from_value(call.body).unwrap()) + .expect("call should succeed with empty result"); + + assert_eq!( + rsp, + Vec::::new(), + "check tx should return an empty response" + ); + }, + ); +} + +#[test] +fn test_evm_runtime() { + do_test_evm_runtime::(); +} + +#[test] +fn test_c10l_evm_runtime() { + let _guard = crypto::signature::context::test_using_chain_context(); + crypto::signature::context::set_chain_context(Default::default(), "test"); + do_test_evm_runtime::(); +} + +#[test] +fn test_c10l_queries() { + let mut mock = mock::Mock::default(); + let ctx = mock.create_ctx_for_runtime::>(true); + let mut signer = EvmSigner::new(0, keys::dave::sigspec()); + + EVMRuntime::::migrate(&ctx); + + static QUERY_CONTRACT_CODE_HEX: &str = + include_str!("../../../../tests/e2e/evm/contracts/query/query.hex"); + + // Simulate contract creation. + let result = signer + .query_evm_create(&ctx, load_contract_bytecode(QUERY_CONTRACT_CODE_HEX)) + .expect("query should succeed"); + let contract_address1 = H160::from_slice(&result); + + let result = signer + .query_evm_create_opts( + &ctx, + load_contract_bytecode(QUERY_CONTRACT_CODE_HEX), + QueryOptions { + encrypt: true, + ..Default::default() + }, + ) + .expect("query should succeed"); + let contract_address2 = H160::from_slice(&result); + + assert_eq!(contract_address1, contract_address2); + + // Create contract. + let dispatch_result = signer.call( + &ctx, + "evm.Create", + types::Create { + value: 0.into(), + init_code: load_contract_bytecode(QUERY_CONTRACT_CODE_HEX), + }, + ); + let result = dispatch_result.result.unwrap(); + let result: Vec = cbor::from_value(result).unwrap(); + let contract_address = H160::from_slice(&result); + + // Call the `test` method on the contract via a query. + let result = signer + .query_evm_call(&ctx, contract_address, "test", &[], &[]) + .expect("query should succeed"); + + let mut result = + ethabi::decode(&[ParamType::Address], &result).expect("output should be correct"); + + let test = result.pop().unwrap().into_address().unwrap(); + assert_eq!(test, Default::default(), "msg.signer should be zeroized"); + + // Test call with confidential envelope. + let result = signer + .query_evm_call_opts( + &ctx, + contract_address, + "test", + &[], + &[], + QueryOptions { + encrypt: true, + ..Default::default() + }, + ) + .expect("query should succeed"); + + let mut result = + ethabi::decode(&[ParamType::Address], &result).expect("output should be correct"); + + let test = result.pop().unwrap().into_address().unwrap(); + assert_eq!(test, Default::default(), "msg.signer should be zeroized"); +} + +#[test] +fn test_fee_refunds() { + let mut mock = mock::Mock::default(); + let ctx = mock.create_ctx_for_runtime::>(true); + let mut signer = EvmSigner::new(0, keys::dave::sigspec()); + + EVMRuntime::::migrate(&ctx); + + // Give Dave some tokens. + Accounts::mint( + keys::dave::address(), + &token::BaseUnits(1_000_000_000, Denomination::NATIVE), + ) + .unwrap(); + + // Create contract. + let dispatch_result = signer.call( + &ctx, + "evm.Create", + types::Create { + value: 0.into(), + init_code: load_contract_bytecode(TEST_CONTRACT_CODE_HEX), + }, + ); + let result = dispatch_result.result.unwrap(); + let result: Vec = cbor::from_value(result).unwrap(); + let contract_address = H160::from_slice(&result); + + // Call the `name` method on the contract. + let dispatch_result = signer.call_evm_opts( + &ctx, + contract_address, + "name", + &[], + &[], + CallOptions { + fee: Fee { + amount: token::BaseUnits::new(1_000_000, Denomination::NATIVE), + gas: 100_000, + ..Default::default() + }, + ..Default::default() + }, + ); + assert!(dispatch_result.result.is_success(), "call should succeed"); + + // Make sure two events were emitted and are properly formatted. + let tags = &dispatch_result.tags; + assert_eq!(tags.len(), 2, "two events should have been emitted"); + assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x01"); // accounts.Transfer (code = 1) event + assert_eq!(tags[1].key, b"core\x00\x00\x00\x01"); // core.GasUsed (code = 1) event + + #[derive(Debug, Default, cbor::Decode)] + struct TransferEvent { + from: Address, + to: Address, + amount: token::BaseUnits, + } + + let events: Vec = cbor::from_slice(&tags[0].value).unwrap(); + assert_eq!(events.len(), 1); // One event for fee payment. + let event = &events[0]; + assert_eq!(event.from, keys::dave::address()); + assert_eq!(event.to, *ADDRESS_FEE_ACCUMULATOR); + assert_eq!( + event.amount, + token::BaseUnits::new(242_700, Denomination::NATIVE) + ); + + #[derive(Debug, Default, cbor::Decode)] + struct GasUsedEvent { + amount: u64, + } + + let events: Vec = cbor::from_slice(&tags[1].value).unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].amount, 24_270); + + // Call the `transfer` method on the contract with invalid parameters so it reverts. + let dispatch_result = signer.call_evm_opts( + &ctx, + contract_address, + "transfer", + &[ParamType::Address, ParamType::Uint(256)], + &[ + Token::Address(contract_address.into()), + Token::Uint(u128::MAX.into()), // Too much so it reverts. + ], + CallOptions { + fee: Fee { + amount: token::BaseUnits::new(1_000_000, Denomination::NATIVE), + gas: 100_000, + ..Default::default() + }, + ..Default::default() + }, + ); + if let module::CallResult::Failed { + module, + code, + message, + } = dispatch_result.result + { + assert_eq!(module, "evm"); + assert_eq!(code, 8); + assert_eq!( + decode_reverted(&message).unwrap(), + "ERC20: transfer amount exceeds balance" + ); + } else { + panic!("call should revert"); + } + + // Make sure two events were emitted and are properly formatted. + let tags = &dispatch_result.tags; + assert_eq!(tags.len(), 2, "two events should have been emitted"); + assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x01"); // accounts.Transfer (code = 1) event + assert_eq!(tags[1].key, b"core\x00\x00\x00\x01"); // core.GasUsed (code = 1) event + + let events: Vec = cbor::from_slice(&tags[0].value).unwrap(); + assert_eq!(events.len(), 1); // One event for fee payment. + let event = &events[0]; + assert_eq!(event.from, keys::dave::address()); + assert_eq!(event.to, *ADDRESS_FEE_ACCUMULATOR); + assert_eq!( + event.amount, + token::BaseUnits::new(245_850, Denomination::NATIVE) // Note the refund. + ); + + let events: Vec = cbor::from_slice(&tags[1].value).unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].amount, 24_585); +} + +#[test] +fn test_transfer_event() { + let mut mock = mock::Mock::default(); + let mut ctx = mock.create_ctx_for_runtime::>(true); + let mut signer = EvmSigner::new(0, keys::dave::sigspec()); + + EVMRuntime::::migrate(&mut ctx); + + // Create contract. + let dispatch_result = signer.call( + &mut ctx, + "evm.Create", + types::Create { + value: 0.into(), + init_code: load_contract_bytecode(FAUCET_CONTRACT_CODE_HEX), + }, + ); + let result = dispatch_result.result.unwrap(); + let result: Vec = cbor::from_value(result).unwrap(); + let contract_address = H160::from_slice(&result); + let contract_address_native = EVMConfig::map_address(contract_address.0.into()); + + // Give the faucet some tokens. + Accounts::mint( + contract_address_native, + &token::BaseUnits(1_000_000_000_000, Denomination::NATIVE), + ) + .unwrap(); + + // Call the `withdraw` method on the contract; this initiates a native token transfer from within EVM. + let dispatch_result = signer.call_evm_opts( + &mut ctx, + contract_address, + "withdraw", + &[ParamType::Uint(256)], + &[Token::Uint(1_000_000_000.into())], + CallOptions { + fee: Fee { + amount: token::BaseUnits::new(1_000_000, Denomination::NATIVE), + gas: 100_000, + ..Default::default() + }, + ..Default::default() + }, + ); + assert!(dispatch_result.result.is_success(), "call should succeed"); + + // Make sure two events were emitted and are properly formatted. + let tags = &dispatch_result.tags; + assert_eq!(tags.len(), 2, "two events should have been emitted"); + assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x01"); // accounts.Transfer (code = 1) events + assert_eq!(tags[1].key, b"core\x00\x00\x00\x01"); // core.GasUsed (code = 1) event + + #[derive(Debug, Default, cbor::Decode)] + struct TransferEvent { + from: Address, + to: Address, + amount: token::BaseUnits, + } + + let events: Vec = cbor::from_slice(&tags[0].value).unwrap(); + assert_eq!(events.len(), 2); // One event for fee payment, one for the withdrawal. + let event = &events[0]; + assert_eq!(event.from, contract_address_native); + assert_eq!(event.to, keys::dave::address()); + assert_eq!( + event.amount, + token::BaseUnits::new(1_000_000_000, Denomination::NATIVE) + ); + let event = &events[1]; + assert_eq!(event.from, keys::dave::address()); + assert_eq!(event.to, *ADDRESS_FEE_ACCUMULATOR); + assert_eq!( + event.amount, + token::BaseUnits::new(283_430, Denomination::NATIVE) + ); + + #[derive(Debug, Default, cbor::Decode)] + struct GasUsedEvent { + amount: u64, + } + + let events: Vec = cbor::from_slice(&tags[1].value).unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].amount, 28_343); +} + +#[test] +fn test_return_value_limits() { + let mut mock = mock::Mock::default(); + let ctx = mock.create_ctx_for_runtime::>(true); + let mut signer = EvmSigner::new(0, keys::dave::sigspec()); + + EVMRuntime::::migrate(&ctx); + + // Give Dave some tokens. + Accounts::mint( + keys::dave::address(), + &token::BaseUnits(1_000_000_000, Denomination::NATIVE), + ) + .unwrap(); + + static RETVAL_CONTRACT_CODE_HEX: &str = + include_str!("../../../../tests/e2e/evm/contracts/retval/retval.hex"); + + // Create contract. + let dispatch_result = signer.call( + &ctx, + "evm.Create", + types::Create { + value: 0.into(), + init_code: load_contract_bytecode(RETVAL_CONTRACT_CODE_HEX), + }, + ); + let result = dispatch_result.result.unwrap(); + let result: Vec = cbor::from_value(result).unwrap(); + let contract_address = H160::from_slice(&result); + + // Call the `testSuccess` method on the contract. + let dispatch_result = signer.call_evm_opts( + &ctx, + contract_address, + "testSuccess", + &[], + &[], + CallOptions { + fee: Fee { + amount: token::BaseUnits::new(1_000_000, Denomination::NATIVE), + gas: 100_000, + ..Default::default() + }, + ..Default::default() + }, + ); + let result: Vec = cbor::from_value(dispatch_result.result.unwrap()).unwrap(); + assert_eq!(result.len(), 1024, "result should be correctly trimmed"); + // Actual payload is ABI-encoded so the raw result starts at offset 64. + assert_eq!(result[64], 0xFF, "result should be correct"); + assert_eq!(result[1023], 0x42, "result should be correct"); + + // Call the `testRevert` method on the contract. + let dispatch_result = signer.call_evm_opts( + &ctx, + contract_address, + "testRevert", + &[], + &[], + CallOptions { + fee: Fee { + amount: token::BaseUnits::new(1_000_000, Denomination::NATIVE), + gas: 100_000, + ..Default::default() + }, + ..Default::default() + }, + ); + if let module::CallResult::Failed { + module, + code, + message, + } = dispatch_result.result + { + assert_eq!(module, "evm"); + assert_eq!(code, 8); + let message = decode_reverted_raw(&message).unwrap(); + // Actual payload is ABI-encoded so the raw result starts at offset 68. + assert_eq!(message[68], 0xFF, "result should be correct"); + assert_eq!(message[1023], 0x42, "result should be correct"); + } else { + panic!("call should revert"); + } + + // Make sure that in query context, the return value is not trimmed. + let ctx = mock.create_ctx_for_runtime::>(true); + + let result = signer + .query_evm_call_opts( + &ctx, + contract_address, + "testSuccess", + &[], + &[], + Default::default(), + ) + .expect("query should succeed"); + + assert_eq!(result.len(), 1120, "result should not be trimmed"); + // Actual payload is ABI-encoded so the raw result starts at offset 64. + assert_eq!(result[64], 0xFF, "result should be correct"); + assert_eq!(result[1023], 0x42, "result should be correct"); +} diff --git a/runtime-sdk/modules/evm-new/src/types.rs b/runtime-sdk/modules/evm-new/src/types.rs new file mode 100644 index 0000000000..bd954c0544 --- /dev/null +++ b/runtime-sdk/modules/evm-new/src/types.rs @@ -0,0 +1,222 @@ +//! EVM module types. + +/// Transaction body for creating an EVM contract. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct Create { + pub value: U256, + pub init_code: Vec, +} + +/// Transaction body for calling an EVM contract. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct Call { + pub address: H160, + pub value: U256, + pub data: Vec, +} + +/// Transaction body for peeking into EVM storage. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct StorageQuery { + pub address: H160, + pub index: H256, +} + +/// Transaction body for peeking into EVM code storage. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct CodeQuery { + pub address: H160, +} + +/// Transaction body for fetching EVM account's balance. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct BalanceQuery { + pub address: H160, +} + +/// Transaction body for simulating an EVM call. +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct SimulateCallQuery { + pub gas_price: U256, + pub gas_limit: u64, + pub caller: H160, + #[cbor(optional)] + pub address: Option, + pub value: U256, + pub data: Vec, +} + +/// An envelope containing the encryption-enveloped data of a [`SimulateCallQuery`] +/// and a signature generated according to [EIP-712](https://eips.ethereum.org/EIPS/eip-712) +/// over the unmodified Eth call. +/// +/// EIP-712 is used so that the signed message can be easily verified by the user. +/// MetaMask, for instance, shows each field as itself, whereas a standard `eth_personalSign` +/// would show an opaque CBOR-encoded [`SimulateCallQuery`]. +/// +/// The EIP-712 type parameters for a signed query are: +/// ```ignore +/// { +/// domain: { +/// name: 'oasis-runtime-sdk/evm: signed query', +/// version: '1.0.0', +/// chainId, +/// }, +/// types: { +/// Call: [ +/// { name: 'from', type: 'address' }, +/// { name: 'to', type: 'address' }, +/// { name: 'value', type: 'uint256' }, +/// { name: 'gasPrice', type: 'uint256' }, +/// { name: 'gasLimit', type: 'uint64' }, +/// { name: 'data', type: 'bytes' }, +/// { name: 'leash', type: 'Leash' }, +/// ], +/// Leash: [ +/// { name: 'nonce', type: 'uint64' }, +/// { name: 'blockNumber', type: 'uint64' }, +/// { name: 'blockHash', type: 'uint256' }, +/// { name: 'blockRange', type: 'uint64' }, +/// ], +/// }, +/// } +/// ``` +#[derive(Clone, Debug, cbor::Encode, cbor::Decode)] +#[cbor(no_default)] +pub struct SignedCallDataPack { + pub data: oasis_runtime_sdk::types::transaction::Call, + pub leash: Leash, + pub signature: [u8; 65], +} + +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct Leash { + /// The maximum account nonce that will be tolerated. + pub nonce: u64, + /// The base block number. + pub block_number: u64, + /// The expeced hash at `block_number`. + pub block_hash: H256, + /// The range of the leash past `block_number`. + pub block_range: u64, +} + +// The rest of the file contains wrappers for primitive_types::{H160, H256, U256}, +// so that we can implement cbor::{Encode, Decode} for them, ugh. +// Remove this once oasis-cbor#8 is implemented. +// +// Thanks to Nick for providing the fancy macros below :) + +// This `mod` exists solely to place an `#[allow(...)]` around the generated code. +#[allow(clippy::assign_op_pattern, clippy::non_canonical_clone_impl)] +mod eth { + use std::convert::TryFrom; + + use thiserror::Error; + + #[derive(Error, Debug)] + pub enum NoError {} + + macro_rules! construct_fixed_hash { + ($name:ident($num_bytes:literal)) => { + fixed_hash::construct_fixed_hash! { + pub struct $name($num_bytes); + } + + impl cbor::Encode for $name { + fn into_cbor_value(self) -> cbor::Value { + cbor::Value::ByteString(self.as_bytes().to_vec()) + } + } + + impl cbor::Decode for $name { + fn try_default() -> Result { + Ok(Default::default()) + } + + fn try_from_cbor_value(value: cbor::Value) -> Result { + match value { + cbor::Value::ByteString(v) => { + if v.len() == $num_bytes { + Ok(Self::from_slice(&v)) + } else { + Err(cbor::DecodeError::UnexpectedIntegerSize) + } + } + _ => Err(cbor::DecodeError::UnexpectedType), + } + } + } + + impl TryFrom<&[u8]> for $name { + type Error = NoError; + + fn try_from(bytes: &[u8]) -> Result { + Ok(Self::from_slice(bytes)) + } + } + }; + } + + macro_rules! construct_uint { + ($name:ident($num_words:tt)) => { + uint::construct_uint! { + pub struct $name($num_words); + } + + impl cbor::Encode for $name { + fn into_cbor_value(self) -> cbor::Value { + let mut out = [0u8; $num_words * 8]; + self.to_big_endian(&mut out); + cbor::Value::ByteString(out.to_vec()) + } + } + + impl cbor::Decode for $name { + fn try_default() -> Result { + Ok(Default::default()) + } + + fn try_from_cbor_value(value: cbor::Value) -> Result { + match value { + cbor::Value::ByteString(v) => { + if v.len() <= $num_words * 8 { + Ok(Self::from_big_endian(&v)) + } else { + Err(cbor::DecodeError::UnexpectedIntegerSize) + } + } + _ => Err(cbor::DecodeError::UnexpectedType), + } + } + } + }; + } + + construct_fixed_hash!(H160(20)); + construct_fixed_hash!(H256(32)); + construct_uint!(U256(4)); + + macro_rules! impl_upstream_conversions { + ($($ty:ident),* $(,)?) => { + $( + impl From<$ty> for primitive_types::$ty { + fn from(t: $ty) -> Self { + Self(t.0) + } + } + + impl From for $ty { + fn from(t: primitive_types::$ty) -> Self { + Self(t.0) + } + } + )* + } + } + + impl_upstream_conversions!(H160, H256, U256); +} +pub use eth::{H160, H256, U256}; From 5008d6029157ac6704ca3f8c2ca8619c02666f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Buko=C5=A1ek?= Date: Thu, 3 Oct 2024 16:16:37 +0200 Subject: [PATCH 2/3] wip2 --- runtime-sdk/modules/evm-new/src/db.rs | 5 +- runtime-sdk/modules/evm-new/src/lib.rs | 136 +++++++++++++++++++++++-- 2 files changed, 129 insertions(+), 12 deletions(-) diff --git a/runtime-sdk/modules/evm-new/src/db.rs b/runtime-sdk/modules/evm-new/src/db.rs index de9efb5bfc..24cd935284 100644 --- a/runtime-sdk/modules/evm-new/src/db.rs +++ b/runtime-sdk/modules/evm-new/src/db.rs @@ -36,7 +36,7 @@ impl<'ctx, C: Context, Cfg: Config> OasisDB<'ctx, C, Cfg> { } impl<'ctx, C: Context, Cfg: Config> Database for OasisDB<'ctx, C, Cfg> { - type Error = Infallible; + type Error = String; /// Get basic account information. fn basic(&mut self, address: Address) -> Result, Self::Error> { @@ -76,8 +76,7 @@ impl<'ctx, C: Context, Cfg: Config> Database for OasisDB<'ctx, C, Cfg> { /// Get account code by its hash (unimplemented). fn code_by_hash(&mut self, _code_hash: B256) -> Result { - // XXX: return an error here instead. - Ok(Bytecode::new()) + Err("getting code by hash is not supported".to_string()) } /// Get storage value of address at index. diff --git a/runtime-sdk/modules/evm-new/src/lib.rs b/runtime-sdk/modules/evm-new/src/lib.rs index 2ead70547a..79dfe9c4e4 100644 --- a/runtime-sdk/modules/evm-new/src/lib.rs +++ b/runtime-sdk/modules/evm-new/src/lib.rs @@ -9,6 +9,7 @@ mod signed_call; pub mod state; pub mod types; +use base64::prelude::*; use revm::{ primitives::{ExecutionResult, Output, TxKind}, Evm, @@ -291,10 +292,85 @@ impl API for Module { ctx: &C, call: types::SimulateCallQuery, ) -> Result, Error> { - todo!() + let ( + types::SimulateCallQuery { + gas_price, + gas_limit, + caller, + address, + value, + data, + }, + tx_metadata, + ) = Self::decode_simulate_call_query(ctx, call)?; + + let (method, body, exec): (_, _, Box Result<_, _>>) = match address { + Some(address) => { + // Address is set, this is a simulated `evm.Call`. + ( + "evm.Call", + cbor::to_value(types::Call { + address, + value, + data: data.clone(), + }), + Box::new(move || Self::evm_call(ctx, caller, address, value, data, false)), + ) + } + None => { + // Address is not set, this is a simulated `evm.Create`. + ( + "evm.Create", + cbor::to_value(types::Create { + value, + init_code: data.clone(), + }), + Box::new(|| Self::evm_create(ctx, caller, value, data, false)), + ) + } + }; + let tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: method.to_owned(), + body, + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo { + address_spec: transaction::AddressSpec::Internal( + transaction::CallerAddress::EthAddress(caller.into()), + ), + nonce: 0, + }], + fee: transaction::Fee { + amount: token::BaseUnits::new( + gas_price + .checked_mul(U256::from(gas_limit)) + .ok_or(Error::FeeOverflow)? + .as_u128(), + Cfg::TOKEN_DENOMINATION, + ), + gas: gas_limit, + consensus_messages: 0, + proxy: None, + }, + ..Default::default() + }, + }; + + let evm_result = CurrentState::with_transaction_opts( + Options::new() + .with_tx(TransactionWithMeta::internal(tx)) + .with_mode(Mode::Simulate), + || TransactionResult::Rollback(exec()), + ); + Self::encode_evm_result(ctx, evm_result, tx_metadata) } } +// TODO: Most of evm_create and evm_call is the same, group the common code into one method as in the old implementation. impl Module { fn evm_create( ctx: &C, @@ -317,15 +393,25 @@ impl Module { let tx = evm.transact().unwrap(); // XXX: transact_commit? + err checking - let ExecutionResult::Success { - output: Output::Create(_, Some(address)), - .. - } = tx.result - else { - return Err(todo!()); + let ret = match tx.result { + ExecutionResult::Success { + reason, + gas_used, + gas_refunded, + logs, + output, + } => Ok(output.into_data().to_vec()), + ExecutionResult::Revert { gas_used, output } => { + Err(Error::Reverted(BASE64_STANDARD.encode(output.to_vec()))) // XXX: to_vec maybe not needed (check encoding) + } + ExecutionResult::Halt { reason, gas_used } => { + Err(crate::Error::ExecutionFailed(format!("{:?}", reason))) + } }; + // TODO: logs? also clamp data. + // TODO: gas... - todo!() + ret } fn evm_call( @@ -336,7 +422,39 @@ impl Module { data: Vec, estimate_gas: bool, ) -> Result, Error> { - todo!() + let mut db = db::OasisDB::<'_, C, Cfg>::new(ctx); + + let mut evm = Evm::builder() + .with_db(db) + .modify_tx_env(|tx| { + tx.transact_to = TxKind::Call(address.0.into()); + tx.caller = caller.0.into(); + tx.value = revm::primitives::U256::from_be_bytes(value.into()); // XXX: is BE ok? + tx.data = data.into(); + }) + .build(); + + let tx = evm.transact().unwrap(); // XXX: transact_commit? + err checking + + let ret = match tx.result { + ExecutionResult::Success { + reason, + gas_used, + gas_refunded, + logs, + output, + } => Ok(output.into_data().to_vec()), + ExecutionResult::Revert { gas_used, output } => { + Err(Error::Reverted(BASE64_STANDARD.encode(output.to_vec()))) // XXX: to_vec maybe not needed (check encoding) + } + ExecutionResult::Halt { reason, gas_used } => { + Err(crate::Error::ExecutionFailed(format!("{:?}", reason))) + } + }; + // TODO: logs? also clamp data. + // TODO: gas... + + ret } fn derive_caller() -> Result { From df1c5c76e263fed69244d8fbdc2c7ac80804bbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Buko=C5=A1ek?= Date: Tue, 29 Oct 2024 14:20:09 +0100 Subject: [PATCH 3/3] wip3 --- runtime-sdk/modules/evm-new/src/lib.rs | 143 +++++++++++++++---------- 1 file changed, 89 insertions(+), 54 deletions(-) diff --git a/runtime-sdk/modules/evm-new/src/lib.rs b/runtime-sdk/modules/evm-new/src/lib.rs index 79dfe9c4e4..7bb950ea1c 100644 --- a/runtime-sdk/modules/evm-new/src/lib.rs +++ b/runtime-sdk/modules/evm-new/src/lib.rs @@ -11,7 +11,7 @@ pub mod types; use base64::prelude::*; use revm::{ - primitives::{ExecutionResult, Output, TxKind}, + primitives::{ExecutionResult, TxEnv, TxKind}, Evm, }; @@ -370,28 +370,19 @@ impl API for Module { } } -// TODO: Most of evm_create and evm_call is the same, group the common code into one method as in the old implementation. impl Module { - fn evm_create( + fn evm_execute( ctx: &C, - caller: H160, - value: U256, - init_code: Vec, estimate_gas: bool, + f: F, ) -> Result, Error> { - let mut db = db::OasisDB::<'_, C, Cfg>::new(ctx); + // TODO: precompiles - let mut evm = Evm::builder() - .with_db(db) - .modify_tx_env(|tx| { - tx.transact_to = TxKind::Create; - tx.caller = caller.0.into(); - tx.value = revm::primitives::U256::from_be_bytes(value.into()); // XXX: is BE ok? - tx.data = init_code.into(); - }) - .build(); + let is_query = CurrentState::with_env(|env| !env.is_execute()); - let tx = evm.transact().unwrap(); // XXX: transact_commit? + err checking + let mut db = db::OasisDB::<'_, C, Cfg>::new(ctx); + let mut evm = Evm::builder().with_db(db).modify_tx_env(f).build(); + let tx = evm.transact().unwrap(); // XXX: err checking let ret = match tx.result { ExecutionResult::Success { @@ -400,20 +391,86 @@ impl Module { gas_refunded, logs, output, - } => Ok(output.into_data().to_vec()), + } => { + // Clamp data based on maximum allowed result size. + let data = output.into_data(); + let data = if !is_query && data.len() > Cfg::MAX_RESULT_SIZE { + data[..Cfg::MAX_RESULT_SIZE].to_vec() + } else { + data.to_vec() + }; + + // Use gas and refund unused gas. + // XXX: check + ::Core::use_tx_gas(gas_used)?; + ::Accounts::set_refund_unused_tx_fee(Cfg::REFUND_UNUSED_FEE); + + // Emit logs as events. + CurrentState::with(|state| { + for log in logs { + state.emit_event(crate::Event::Log { + address: H160::from_slice(&log.address.into_array()), + topics: log + .topics() + .iter() + .map(|&topic| H256::from_slice(&topic.as_slice())) + .collect(), + data: log.data.data.to_vec(), + }); + } + }); + + Ok(data) + } ExecutionResult::Revert { gas_used, output } => { - Err(Error::Reverted(BASE64_STANDARD.encode(output.to_vec()))) // XXX: to_vec maybe not needed (check encoding) + // Clamp data based on maximum allowed result size. + // XXX: to_vec maybe not needed (check encoding) + let data = if !is_query && output.len() > Cfg::MAX_RESULT_SIZE { + output[..Cfg::MAX_RESULT_SIZE].to_vec() + } else { + output.to_vec() + }; + + // Use gas and refund unused gas. + // XXX: check + ::Core::use_tx_gas(gas_used)?; + ::Accounts::set_refund_unused_tx_fee(Cfg::REFUND_UNUSED_FEE); + + Err(Error::Reverted(BASE64_STANDARD.encode(data))) } ExecutionResult::Halt { reason, gas_used } => { + // Use gas and refund unused gas. + // XXX: check + ::Core::use_tx_gas(gas_used)?; + ::Accounts::set_refund_unused_tx_fee(Cfg::REFUND_UNUSED_FEE); + Err(crate::Error::ExecutionFailed(format!("{:?}", reason))) } }; - // TODO: logs? also clamp data. - // TODO: gas... ret } + fn evm_create( + ctx: &C, + caller: H160, + value: U256, + init_code: Vec, + estimate_gas: bool, + ) -> Result, Error> { + Self::evm_execute(ctx, estimate_gas, |tx| { + tx.gas_limit = ::Core::remaining_tx_gas(); + tx.gas_price = CurrentState::with_env(|env| env.tx_auth_info().fee.gas_price()) + .try_into() + .unwrap(); // XXX: err checking + + tx.caller = caller.0.into(); + tx.transact_to = TxKind::Create; + tx.value = revm::primitives::U256::from_be_bytes(value.into()); // XXX: is BE ok? + tx.data = init_code.into(); + }) + } + fn evm_call( ctx: &C, caller: H160, @@ -422,39 +479,17 @@ impl Module { data: Vec, estimate_gas: bool, ) -> Result, Error> { - let mut db = db::OasisDB::<'_, C, Cfg>::new(ctx); - - let mut evm = Evm::builder() - .with_db(db) - .modify_tx_env(|tx| { - tx.transact_to = TxKind::Call(address.0.into()); - tx.caller = caller.0.into(); - tx.value = revm::primitives::U256::from_be_bytes(value.into()); // XXX: is BE ok? - tx.data = data.into(); - }) - .build(); - - let tx = evm.transact().unwrap(); // XXX: transact_commit? + err checking - - let ret = match tx.result { - ExecutionResult::Success { - reason, - gas_used, - gas_refunded, - logs, - output, - } => Ok(output.into_data().to_vec()), - ExecutionResult::Revert { gas_used, output } => { - Err(Error::Reverted(BASE64_STANDARD.encode(output.to_vec()))) // XXX: to_vec maybe not needed (check encoding) - } - ExecutionResult::Halt { reason, gas_used } => { - Err(crate::Error::ExecutionFailed(format!("{:?}", reason))) - } - }; - // TODO: logs? also clamp data. - // TODO: gas... - - ret + Self::evm_execute(ctx, estimate_gas, |tx| { + tx.gas_limit = ::Core::remaining_tx_gas(); + tx.gas_price = CurrentState::with_env(|env| env.tx_auth_info().fee.gas_price()) + .try_into() + .unwrap(); // XXX: err checking + + tx.caller = caller.0.into(); + tx.transact_to = TxKind::Call(address.0.into()); + tx.value = revm::primitives::U256::from_be_bytes(value.into()); // XXX: is BE ok? + tx.data = data.into(); + }) } fn derive_caller() -> Result {