Compare commits

...

7 Commits

Author SHA1 Message Date
Omar Abdulla 9676fca3fe Support compiler-version aware exceptions 2025-07-25 17:06:14 +03:00
Omar 2923d675cd Support Compile-time Linking (#79)
* Use wrappers for libraries in metadata.

* Create a unified way to access deployed contracts

* Support linking at compile time
2025-07-25 07:03:21 +00:00
Omar 8f5bcf08ad Support Calldata arithmetic (#77)
* Re-order the input file.

This commit reorders the input file such that we have a definitions
section and an implementations section and such that the the order of
the items in both sections is the same.

* Implement a reverse polish calculator for calldata arithmetic
2025-07-24 15:35:25 +00:00
Omar 90fb89adc0 Add a common crate (#75)
* Add a barebones common crate

* Refactor some code into the common crate

* Add a `ResolverApi` interface.

This commit adds a `ResolverApi` trait to the `format` crate that can be
implemented by any type that can act as a resolver. A resolver is able
to provide information on the chain state. This chain state could be
fresh or it could be cached (which is something that we will do in a
future PR).

This cleans up our crate graph so that `format` is not depending on the
node interactions crate for the `EthereumNode` trait.

* Cleanup the blocking executor
2025-07-24 12:42:45 +00:00
Omar b03ad3027e Pre-seed accounts with more ETH. (#73)
* Pre-seed accounts with more ETH.

This commit fixes and solves some issues around how much ETH we seed an
account with in genesis. Currently, any account that the node has keys
to sign for will be seeded with u128::MAX WEI in genesis. This also
includes the default signer account.

* Bump commit hash of polkadot SDK

* Change how the cache key is computed

* Revert "Change how the cache key is computed"

This reverts commit 75afdd9cfd.

* Revert "Bump commit hash of polkadot SDK"

This reverts commit 8aaa69780e.

* Add extra comments

* Revert "Add extra comments"

This reverts commit bd4de2c83d.

* Update the initial balance
2025-07-24 08:46:14 +00:00
Omar 972f3b6d5b Wait longer for geth receipts (#74) 2025-07-24 04:40:19 +00:00
Omar 6f4aa731ab Handle exceptions (#54)
* Add support for wrapper types

* Move `FilesWithExtensionIterator` to `core::common`

* Remove unneeded use of two `HashMap`s

* Make metadata structs more typed

* Impl new_from for wrapper types

* Implement the new input handling logic

* Fix edge-case in input handling

* Ignore macro doc comment tests

* Correct comment

* Fix edge-case in deployment order

* Handle calldata better

* Allow for the use of function signatures

* Add support for exceptions

* Cached nonce allocator

* Fix tests

* Add support for address replacement

* Cleanup implementation

* Cleanup mutability

* Wire up address replacement with rest of code

* Implement caller replacement

* Switch to callframe trace for exceptions

* Add a way to skip tests if they don't match the target

* Handle values from the metadata files

* Remove address replacement

* Correct the arguments

* Remove empty impl

* Remove address replacement

* Correct the arguments

* Remove empty impl

* Fix size_requirement underflow

* Add support for wildcards in exceptions

* Fix calldata construction of single calldata

* Better handling for length in equivalency checks

* Make initial balance a constant

* Fix size_requirement underflow

* Add support for wildcards in exceptions

* Fix calldata construction of single calldata

* Better handling for length in equivalency checks

* Fix tests
2025-07-24 03:45:53 +00:00
32 changed files with 1692 additions and 744 deletions
Generated
+91 -74
View File
@@ -67,9 +67,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "alloy" name = "alloy"
version = "1.0.20" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae58d888221eecf621595e2096836ce7cfc37be06bfa39d7f64aa6a3ea4c9e5b" checksum = "8ad4eb51e7845257b70c51b38ef8d842d5e5e93196701fcbd757577971a043c6"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-contract", "alloy-contract",
@@ -102,15 +102,16 @@ dependencies = [
[[package]] [[package]]
name = "alloy-consensus" name = "alloy-consensus"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad451f9a70c341d951bca4e811d74dbe1e193897acd17e9dbac1353698cc430b" checksum = "ca3b746060277f3d7f9c36903bb39b593a741cb7afcb0044164c28f0e9b673f0"
dependencies = [ dependencies = [
"alloy-eips", "alloy-eips",
"alloy-primitives", "alloy-primitives",
"alloy-rlp", "alloy-rlp",
"alloy-serde", "alloy-serde",
"alloy-trie", "alloy-trie",
"alloy-tx-macros",
"auto_impl", "auto_impl",
"c-kzg", "c-kzg",
"derive_more 2.0.1", "derive_more 2.0.1",
@@ -126,9 +127,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-consensus-any" name = "alloy-consensus-any"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142daffb15d5be1a2b20d2cd540edbcef03037b55d4ff69dc06beb4d06286dba" checksum = "bf98679329fa708fa809ea596db6d974da892b068ad45e48ac1956f582edf946"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-eips", "alloy-eips",
@@ -140,9 +141,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-contract" name = "alloy-contract"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebf25443920ecb9728cb087fe4dc04a0b290bd6ac85638c58fe94aba70f1a44e" checksum = "a10e47f5305ea08c37b1772086c1573e9a0a257227143996841172d37d3831bb"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-dyn-abi", "alloy-dyn-abi",
@@ -157,6 +158,7 @@ dependencies = [
"alloy-transport", "alloy-transport",
"futures", "futures",
"futures-util", "futures-util",
"serde_json",
"thiserror 2.0.12", "thiserror 2.0.12",
] ]
@@ -227,9 +229,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-eips" name = "alloy-eips"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3056872f6da48046913e76edb5ddced272861f6032f09461aea1a2497be5ae5d" checksum = "f562a81278a3ed83290e68361f2d1c75d018ae3b8589a314faf9303883e18ec9"
dependencies = [ dependencies = [
"alloy-eip2124", "alloy-eip2124",
"alloy-eip2930", "alloy-eip2930",
@@ -247,15 +249,16 @@ dependencies = [
[[package]] [[package]]
name = "alloy-genesis" name = "alloy-genesis"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c98fb40f07997529235cc474de814cd7bd9de561e101716289095696c0e4639d" checksum = "dc41384e9ab8c9b2fb387c52774d9d432656a28edcda1c2d4083e96051524518"
dependencies = [ dependencies = [
"alloy-eips", "alloy-eips",
"alloy-primitives", "alloy-primitives",
"alloy-serde", "alloy-serde",
"alloy-trie", "alloy-trie",
"serde", "serde",
"serde_with",
] ]
[[package]] [[package]]
@@ -272,12 +275,13 @@ dependencies = [
[[package]] [[package]]
name = "alloy-json-rpc" name = "alloy-json-rpc"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc08b31ebf9273839bd9a01f9333cbb7a3abb4e820c312ade349dd18bdc79581" checksum = "12c454fcfcd5d26ed3b8cae5933cbee9da5f0b05df19b46d4bd4446d1f082565"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-sol-types", "alloy-sol-types",
"http",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 2.0.12",
@@ -286,9 +290,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-network" name = "alloy-network"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed117b08f0cc190312bf0c38c34cf4f0dabfb4ea8f330071c587cd7160a88cb2" checksum = "42d6d39eabe5c7b3d8f23ac47b0b683b99faa4359797114636c66e0743103d05"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-consensus-any", "alloy-consensus-any",
@@ -312,9 +316,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-network-primitives" name = "alloy-network-primitives"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7162ff7be8649c0c391f4e248d1273e85c62076703a1f3ec7daf76b283d886d" checksum = "3704fa8b7ba9ba3f378d99b3d628c8bc8c2fc431b709947930f154e22a8368b6"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-eips", "alloy-eips",
@@ -342,7 +346,7 @@ dependencies = [
"keccak-asm", "keccak-asm",
"paste", "paste",
"proptest", "proptest",
"rand 0.9.1", "rand 0.9.2",
"ruint", "ruint",
"rustc-hash", "rustc-hash",
"serde", "serde",
@@ -352,9 +356,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-provider" name = "alloy-provider"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d84eba1fd8b6fe8b02f2acd5dd7033d0f179e304bd722d11e817db570d1fa6c4" checksum = "08800e8cbe70c19e2eb7cf3d7ff4b28bdd9b3933f8e1c8136c7d910617ba03bf"
dependencies = [ dependencies = [
"alloy-chains", "alloy-chains",
"alloy-consensus", "alloy-consensus",
@@ -380,6 +384,7 @@ dependencies = [
"either", "either",
"futures", "futures",
"futures-utils-wasm", "futures-utils-wasm",
"http",
"lru", "lru",
"parking_lot", "parking_lot",
"pin-project", "pin-project",
@@ -395,9 +400,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-pubsub" name = "alloy-pubsub"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8550f7306e0230fc835eb2ff4af0a96362db4b6fc3f25767d161e0ad0ac765bf" checksum = "ae68457a2c2ead6bd7d7acb5bf5f1623324b1962d4f8e7b0250657a3c3ab0a0b"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-primitives", "alloy-primitives",
@@ -438,9 +443,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-client" name = "alloy-rpc-client"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518a699422a3eab800f3dac2130d8f2edba8e4fff267b27a9c7dc6a2b0d313ee" checksum = "162301b5a57d4d8f000bf30f4dcb82f9f468f3e5e846eeb8598dd39e7886932c"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-primitives", "alloy-primitives",
@@ -448,7 +453,6 @@ dependencies = [
"alloy-transport", "alloy-transport",
"alloy-transport-http", "alloy-transport-http",
"alloy-transport-ipc", "alloy-transport-ipc",
"async-stream",
"futures", "futures",
"pin-project", "pin-project",
"reqwest", "reqwest",
@@ -458,16 +462,15 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tower", "tower",
"tracing", "tracing",
"tracing-futures",
"url", "url",
"wasmtimer", "wasmtimer",
] ]
[[package]] [[package]]
name = "alloy-rpc-types" name = "alloy-rpc-types"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c000cab4ec26a4b3e29d144e999e1c539c2fa0abed871bf90311eb3466187ca8" checksum = "6cd8ca94ae7e2b32cc3895d9981f3772aab0b4756aa60e9ed0bcfee50f0e1328"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-rpc-types-eth", "alloy-rpc-types-eth",
@@ -478,9 +481,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types-any" name = "alloy-rpc-types-any"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "508b2fbe66d952089aa694e53802327798806498cd29ff88c75135770ecaabfc" checksum = "076b47e834b367d8618c52dd0a0d6a711ddf66154636df394805300af4923b8a"
dependencies = [ dependencies = [
"alloy-consensus-any", "alloy-consensus-any",
"alloy-rpc-types-eth", "alloy-rpc-types-eth",
@@ -489,9 +492,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types-debug" name = "alloy-rpc-types-debug"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c832f2e851801093928dbb4b7bd83cd22270faf76b2e080646b806a285c8757" checksum = "94a2a86ad7b7d718c15e79d0779bd255561b6b22968dc5ed2e7c0fbc43bb55fe"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"serde", "serde",
@@ -499,9 +502,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types-eth" name = "alloy-rpc-types-eth"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcaf7dff0fdd756a714d58014f4f8354a1706ebf9fa2cf73431e0aeec3c9431e" checksum = "2c2f847e635ec0be819d06e2ada4bcc4e4204026a83c4bfd78ae8d550e027ae7"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-consensus-any", "alloy-consensus-any",
@@ -514,14 +517,15 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"serde", "serde",
"serde_json", "serde_json",
"serde_with",
"thiserror 2.0.12", "thiserror 2.0.12",
] ]
[[package]] [[package]]
name = "alloy-rpc-types-trace" name = "alloy-rpc-types-trace"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3507a04e868dd83219ad3cd6a8c58aefccb64d33f426b3934423a206343e84" checksum = "6fc58180302a94c934d455eeedb3ecb99cdc93da1dbddcdbbdb79dd6fe618b2a"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-rpc-types-eth", "alloy-rpc-types-eth",
@@ -533,9 +537,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-serde" name = "alloy-serde"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730e8f2edf2fc224cabd1c25d090e1655fa6137b2e409f92e5eec735903f1507" checksum = "ae699248d02ade9db493bbdae61822277dc14ae0f82a5a4153203b60e34422a6"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"serde", "serde",
@@ -544,9 +548,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-signer" name = "alloy-signer"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b0d2428445ec13edc711909e023d7779618504c4800be055a5b940025dbafe3" checksum = "3cf7d793c813515e2b627b19a15693960b3ed06670f9f66759396d06ebe5747b"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"async-trait", "async-trait",
@@ -559,9 +563,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-signer-local" name = "alloy-signer-local"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14fe6fedb7fe6e0dfae47fe020684f1d8e063274ef14bca387ddb7a6efa8ec1" checksum = "51a424bc5a11df0d898ce0fd15906b88ebe2a6e4f17a514b51bc93946bb756bd"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-network", "alloy-network",
@@ -648,9 +652,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport" name = "alloy-transport"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a712bdfeff42401a7dd9518f72f617574c36226a9b5414537fedc34350b73bf9" checksum = "4f317d20f047b3de4d9728c556e2e9a92c9a507702d2016424cd8be13a74ca5e"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-primitives", "alloy-primitives",
@@ -671,9 +675,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport-http" name = "alloy-transport-http"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ea5a76d7f2572174a382aedf36875bedf60bcc41116c9f031cf08040703a2dc" checksum = "ff084ac7b1f318c87b579d221f11b748341d68b9ddaa4ffca5e62ed2b8cfefb4"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-transport", "alloy-transport",
@@ -686,9 +690,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport-ipc" name = "alloy-transport-ipc"
version = "1.0.9" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "606af17a7e064d219746f6d2625676122c79d78bf73dfe746d6db9ecd7dbcb85" checksum = "edb099cdad8ed2e6a80811cdf9bbf715ebf4e34c981b4a6e2d1f9daacbf8b218"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-pubsub", "alloy-pubsub",
@@ -706,9 +710,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-trie" name = "alloy-trie"
version = "0.8.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500" checksum = "bada1fc392a33665de0dc50d401a3701b62583c655e3522a323490a5da016962"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-rlp", "alloy-rlp",
@@ -720,6 +724,19 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "alloy-tx-macros"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1154c8187a5ff985c95a8b2daa2fedcf778b17d7668e5e50e556c4ff9c881154"
dependencies = [
"alloy-primitives",
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@@ -3268,13 +3285,14 @@ dependencies = [
[[package]] [[package]]
name = "nybbles" name = "nybbles"
version = "0.3.4" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307" checksum = "675b3a54e5b12af997abc8b6638b0aee51a28caedab70d4967e0d5db3a3f1d06"
dependencies = [ dependencies = [
"alloy-rlp", "alloy-rlp",
"const-hex", "cfg-if",
"proptest", "proptest",
"ruint",
"serde", "serde",
"smallvec", "smallvec",
] ]
@@ -3719,9 +3737,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.1" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rand_core 0.9.3", "rand_core 0.9.3",
@@ -3930,10 +3948,22 @@ dependencies = [
"serde_stacker", "serde_stacker",
] ]
[[package]]
name = "revive-dt-common"
version = "0.1.0"
dependencies = [
"anyhow",
"futures",
"once_cell",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "revive-dt-compiler" name = "revive-dt-compiler"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alloy-primitives",
"anyhow", "anyhow",
"revive-common", "revive-common",
"revive-dt-config", "revive-dt-config",
@@ -3964,6 +3994,7 @@ dependencies = [
"clap", "clap",
"indexmap 2.10.0", "indexmap 2.10.0",
"rayon", "rayon",
"revive-dt-common",
"revive-dt-compiler", "revive-dt-compiler",
"revive-dt-config", "revive-dt-config",
"revive-dt-format", "revive-dt-format",
@@ -3985,7 +4016,7 @@ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-sol-types", "alloy-sol-types",
"anyhow", "anyhow",
"revive-dt-node-interaction", "revive-dt-common",
"semver 1.0.26", "semver 1.0.26",
"serde", "serde",
"serde_json", "serde_json",
@@ -3998,7 +4029,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"anyhow", "anyhow",
"revive-dt-common",
"revive-dt-config", "revive-dt-config",
"revive-dt-format",
"revive-dt-node-interaction", "revive-dt-node-interaction",
"serde", "serde",
"serde_json", "serde_json",
@@ -4015,10 +4048,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"anyhow", "anyhow",
"futures",
"once_cell",
"tokio",
"tracing",
] ]
[[package]] [[package]]
@@ -4113,7 +4142,7 @@ dependencies = [
"primitive-types 0.12.2", "primitive-types 0.12.2",
"proptest", "proptest",
"rand 0.8.5", "rand 0.8.5",
"rand 0.9.1", "rand 0.9.2",
"rlp", "rlp",
"ruint-macro", "ruint-macro",
"serde", "serde",
@@ -5489,18 +5518,6 @@ dependencies = [
"valuable", "valuable",
] ]
[[package]]
name = "tracing-futures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [
"futures",
"futures-task",
"pin-project",
"tracing",
]
[[package]] [[package]]
name = "tracing-log" name = "tracing-log"
version = "0.2.0" version = "0.2.0"
+3 -1
View File
@@ -11,6 +11,7 @@ repository = "https://github.com/paritytech/revive-differential-testing.git"
rust-version = "1.85.0" rust-version = "1.85.0"
[workspace.dependencies] [workspace.dependencies]
revive-dt-common = { version = "0.1.0", path = "crates/common" }
revive-dt-compiler = { version = "0.1.0", path = "crates/compiler" } revive-dt-compiler = { version = "0.1.0", path = "crates/compiler" }
revive-dt-config = { version = "0.1.0", path = "crates/config" } revive-dt-config = { version = "0.1.0", path = "crates/config" }
revive-dt-core = { version = "0.1.0", path = "crates/core" } revive-dt-core = { version = "0.1.0", path = "crates/core" }
@@ -59,7 +60,7 @@ revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af
revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" } revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
[workspace.dependencies.alloy] [workspace.dependencies.alloy]
version = "1.0" version = "1.0.22"
default-features = false default-features = false
features = [ features = [
"json-abi", "json-abi",
@@ -73,6 +74,7 @@ features = [
"network", "network",
"serde", "serde",
"rpc-types-eth", "rpc-types-eth",
"genesis",
] ]
[profile.bench] [profile.bench]
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "revive-dt-common"
description = "A library containing common concepts that other crates in the workspace can rely on"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true }
@@ -9,6 +9,7 @@ use tokio::{
runtime::Builder, runtime::Builder,
sync::{mpsc::UnboundedSender, oneshot}, sync::{mpsc::UnboundedSender, oneshot},
}; };
use tracing::Instrument;
/// A blocking async executor. /// A blocking async executor.
/// ///
@@ -22,7 +23,7 @@ use tokio::{
/// executor to drive an async computation: /// executor to drive an async computation:
/// ///
/// ```rust /// ```rust
/// use revive_dt_node_interaction::*; /// use revive_dt_common::concepts::*;
/// ///
/// fn blocking_function() { /// fn blocking_function() {
/// let result = BlockingExecutor::execute(async move { /// let result = BlockingExecutor::execute(async move {
@@ -63,6 +64,11 @@ impl BlockingExecutor {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<TaskMessage>(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<TaskMessage>();
thread::spawn(move || { thread::spawn(move || {
tracing::info!(
thread_id = ?std::thread::current().id(),
"Starting async runtime thread"
);
let runtime = Builder::new_current_thread() let runtime = Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
@@ -107,7 +113,9 @@ impl BlockingExecutor {
// in the task message. In doing this conversion, we lose some of the type information since // in the task message. In doing this conversion, we lose some of the type information since
// we're converting R => dyn Any. However, we will perform down-casting on the result to // we're converting R => dyn Any. However, we will perform down-casting on the result to
// convert it back into R. // convert it back into R.
let future = Box::pin(async move { Box::new(future.await) as Box<dyn Any + Send> }); let future = Box::pin(
async move { Box::new(future.await) as Box<dyn Any + Send> }.in_current_span(),
);
let task = TaskMessage::new(future, response_tx); let task = TaskMessage::new(future, response_tx);
if let Err(error) = STATE.tx.send(task) { if let Err(error) = STATE.tx.send(task) {
@@ -126,22 +134,17 @@ impl BlockingExecutor {
} }
}; };
match result.map(|result| { let result = match result {
*result Ok(result) => result,
.downcast::<R>()
.expect("Type mismatch in the downcast")
}) {
Ok(result) => Ok(result),
Err(error) => { Err(error) => {
tracing::error!( tracing::error!(?error, "An error occurred when running the async task");
?error, anyhow::bail!("An error occurred when running the async task: {error:?}")
"Failed to downcast the returned result into the expected type"
);
anyhow::bail!(
"Failed to downcast the returned result into the expected type: {error:?}"
)
}
} }
};
Ok(*result
.downcast::<R>()
.expect("An error occurred when downcasting into R. This is a bug"))
} }
} }
/// Represents the state of the async runtime. This runtime is designed to be a singleton runtime /// Represents the state of the async runtime. This runtime is designed to be a singleton runtime
@@ -200,7 +203,9 @@ mod test {
fn panics_in_futures_are_caught() { fn panics_in_futures_are_caught() {
// Act // Act
let result = BlockingExecutor::execute(async move { let result = BlockingExecutor::execute(async move {
panic!("This is a panic!"); panic!(
"If this panic causes, well, a panic, then this is an issue. If it's caught then all good!"
);
0xFFu8 0xFFu8
}); });
+3
View File
@@ -0,0 +1,3 @@
mod blocking_executor;
pub use blocking_executor::*;
+3
View File
@@ -0,0 +1,3 @@
mod files_with_extension_iterator;
pub use files_with_extension_iterator::*;
+6
View File
@@ -0,0 +1,6 @@
//! This crate provides common concepts, functionality, types, macros, and more that other crates in
//! the workspace can benefit from.
pub mod concepts;
pub mod iterators;
pub mod macros;
@@ -12,11 +12,9 @@
/// pub struct CaseId(usize); /// pub struct CaseId(usize);
/// ``` /// ```
/// ///
/// And would also implement a number of methods on this type making it easier /// And would also implement a number of methods on this type making it easier to use.
/// to use.
/// ///
/// These wrapper types become very useful as they make the code a lot easier /// These wrapper types become very useful as they make the code a lot easier to read.
/// to read.
/// ///
/// Take the following as an example: /// Take the following as an example:
/// ///
@@ -26,33 +24,31 @@
/// } /// }
/// ``` /// ```
/// ///
/// In the above code it's hard to understand what the various types refer to or /// In the above code it's hard to understand what the various types refer to or what to expect them
/// what to expect them to contain. /// to contain.
/// ///
/// With these wrapper types we're able to create code that's self-documenting /// With these wrapper types we're able to create code that's self-documenting in that the types
/// in that the types tell us what the code is referring to. The above code is /// tell us what the code is referring to. The above code is transformed into
/// transformed into
/// ///
/// ```rust,ignore /// ```rust,ignore
/// struct State { /// struct State {
/// contracts: HashMap<CaseId, HashMap<ContractName, ContractByteCode>> /// contracts: HashMap<CaseId, HashMap<ContractName, ContractByteCode>>
/// } /// }
/// ``` /// ```
///
/// Note that we follow the same syntax for defining wrapper structs but we do not permit the use of
/// generics.
#[macro_export] #[macro_export]
macro_rules! define_wrapper_type { macro_rules! define_wrapper_type {
( (
$(#[$meta: meta])* $(#[$meta: meta])*
$ident: ident($ty: ty) $(;)? $vis:vis struct $ident: ident($ty: ty);
) => { ) => {
$(#[$meta])* $(#[$meta])*
pub struct $ident($ty); $vis struct $ident($ty);
impl $ident { impl $ident {
pub fn new(value: $ty) -> Self { pub fn new(value: impl Into<$ty>) -> Self {
Self(value)
}
pub fn new_from<T: Into<$ty>>(value: T) -> Self {
Self(value.into()) Self(value.into())
} }
@@ -104,3 +100,7 @@ macro_rules! define_wrapper_type {
} }
}; };
} }
/// Technically not needed but this allows for the macro to be found in the `macros` module of the
/// crate in addition to being found in the root of the crate.
pub use define_wrapper_type;
+3
View File
@@ -0,0 +1,3 @@
mod define_wrapper_type;
pub use define_wrapper_type::*;
+3 -1
View File
@@ -9,11 +9,13 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
anyhow = { workspace = true }
revive-solc-json-interface = { workspace = true } revive-solc-json-interface = { workspace = true }
revive-dt-config = { workspace = true } revive-dt-config = { workspace = true }
revive-dt-solc-binaries = { workspace = true } revive-dt-solc-binaries = { workspace = true }
revive-common = { workspace = true } revive-common = { workspace = true }
alloy-primitives = { workspace = true }
anyhow = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
+21
View File
@@ -9,6 +9,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use alloy_primitives::Address;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_common::EVMVersion; use revive_common::EVMVersion;
@@ -158,6 +159,26 @@ where
self self
} }
pub fn with_library(
mut self,
scope: impl AsRef<Path>,
library_ident: impl AsRef<str>,
library_address: Address,
) -> Self {
self.input
.settings
.libraries
.get_or_insert_with(Default::default)
.entry(scope.as_ref().display().to_string())
.or_default()
.insert(
library_ident.as_ref().to_owned(),
library_address.to_string(),
);
self
}
pub fn try_build(self, solc_path: PathBuf) -> anyhow::Result<CompilerOutput<T::Options>> { pub fn try_build(self, solc_path: PathBuf) -> anyhow::Result<CompilerOutput<T::Options>> {
T::new(solc_path).build(CompilerInput { T::new(solc_path).build(CompilerInput {
extra_options: self.extra_options, extra_options: self.extra_options,
+4
View File
@@ -10,6 +10,10 @@ use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_solc_json_interface::SolcStandardJsonOutput; use revive_solc_json_interface::SolcStandardJsonOutput;
// TODO: I believe that we need to also pass the solc compiler to resolc so that resolc uses the
// specified solc compiler. I believe that currently we completely ignore the specified solc binary
// when invoking resolc which doesn't seem right if we're using solc as a compiler frontend.
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode. /// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
#[derive(Debug)] #[derive(Debug)]
pub struct Resolc { pub struct Resolc {
+6
View File
@@ -73,6 +73,12 @@ pub struct Arguments {
)] )]
pub account: String, pub account: String,
/// This argument controls which private keys the nodes should have access to and be added to
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
/// of the node.
#[arg(long = "private-keys-count", default_value_t = 30)]
pub private_keys_to_add: usize,
/// The differential testing leader node implementation. /// The differential testing leader node implementation.
#[arg(short, long = "leader", default_value = "geth")] #[arg(short, long = "leader", default_value = "geth")]
pub leader: TestingPlatform, pub leader: TestingPlatform,
+1
View File
@@ -13,6 +13,7 @@ name = "retester"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
revive-dt-common = { workspace = true }
revive-dt-compiler = { workspace = true } revive-dt-compiler = { workspace = true }
revive-dt-config = { workspace = true } revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true } revive-dt-format = { workspace = true }
+505 -147
View File
@@ -1,12 +1,16 @@
//! The test driver handles the compilation and execution of the test cases. //! The test driver handles the compilation and execution of the test cases.
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug;
use std::marker::PhantomData; use std::marker::PhantomData;
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder}; use alloy::network::{Ethereum, TransactionBuilder};
use alloy::rpc::types::TransactionReceipt; use alloy::rpc::types::TransactionReceipt;
use alloy::rpc::types::trace::geth::GethTrace; use alloy::rpc::types::trace::geth::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingOptions, GethTrace,
PreStateConfig,
};
use alloy::{ use alloy::{
primitives::Address, primitives::Address,
rpc::types::{ rpc::types::{
@@ -16,20 +20,21 @@ use alloy::{
}; };
use anyhow::Context; use anyhow::Context;
use indexmap::IndexMap; use indexmap::IndexMap;
use serde_json::Value;
use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_compiler::{Compiler, SolidityCompiler}; use revive_dt_compiler::{Compiler, SolidityCompiler};
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_format::case::CaseIdx; use revive_dt_format::case::CaseIdx;
use revive_dt_format::input::Method; use revive_dt_format::input::{Calldata, EtherValue, Expected, ExpectedOutput, Method};
use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdentifier}; use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdent};
use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode}; use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode};
use revive_dt_node::Node;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use revive_dt_report::reporter::{CompilationTask, Report, Span}; use revive_dt_report::reporter::{CompilationTask, Report, Span};
use revive_solc_json_interface::SolcStandardJsonOutput; use revive_solc_json_interface::SolcStandardJsonOutput;
use serde_json::Value;
use std::fmt::Debug;
use crate::Platform; use crate::Platform;
use crate::common::*;
pub struct State<'a, T: Platform> { pub struct State<'a, T: Platform> {
/// The configuration that the framework was started with. /// The configuration that the framework was started with.
@@ -52,6 +57,12 @@ pub struct State<'a, T: Platform> {
/// files. /// files.
deployed_contracts: HashMap<CaseIdx, HashMap<ContractInstance, (Address, JsonAbi)>>, deployed_contracts: HashMap<CaseIdx, HashMap<ContractInstance, (Address, JsonAbi)>>,
/// This is a map of the deployed libraries.
///
/// This map is not per case, but rather, per metadata file. This means that we do not redeploy
/// the libraries with each case.
deployed_libraries: HashMap<ContractInstance, (Address, JsonAbi)>,
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
@@ -65,6 +76,7 @@ where
span, span,
contracts: Default::default(), contracts: Default::default(),
deployed_contracts: Default::default(), deployed_contracts: Default::default(),
deployed_libraries: Default::default(),
phantom: Default::default(), phantom: Default::default(),
} }
} }
@@ -91,10 +103,28 @@ where
let compiler = Compiler::<T::Compiler>::new() let compiler = Compiler::<T::Compiler>::new()
.allow_path(metadata.directory()?) .allow_path(metadata.directory()?)
.solc_optimizer(mode.solc_optimize()); .solc_optimizer(mode.solc_optimize());
let mut compiler = FilesWithExtensionIterator::new(metadata.directory()?)
let compiler = FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol") .with_allowed_extension("sol")
.try_fold(compiler, |compiler, path| compiler.with_source(&path))?; .try_fold(compiler, |compiler, path| compiler.with_source(&path))?;
for (library_instance, (library_address, _)) in self.deployed_libraries.iter() {
let library_ident = &metadata
.contracts
.as_ref()
.and_then(|contracts| contracts.get(library_instance))
.expect("Impossible for library to not be found in contracts")
.contract_ident;
// Note the following: we need to tell solc which files require the libraries to be
// linked into them. We do not have access to this information and therefore we choose
// an easier, yet more compute intensive route, of telling solc that all of the files
// need to link the library and it will only perform the linking for the files that do
// actually need the library.
compiler = FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol")
.fold(compiler, |compiler, path| {
compiler.with_library(&path, library_ident.as_str(), *library_address)
});
}
let mut task = CompilationTask { let mut task = CompilationTask {
json_input: compiler.input(), json_input: compiler.input(),
@@ -136,16 +166,48 @@ where
} }
} }
pub fn build_and_publish_libraries(
&mut self,
metadata: &Metadata,
mode: &SolcMode,
node: &T::Blockchain,
) -> anyhow::Result<()> {
self.build_contracts(mode, metadata)?;
for library_instance in metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
self.get_or_deploy_contract_instance(
library_instance,
metadata,
None,
Input::default_caller(),
None,
None,
node,
)?;
}
Ok(())
}
pub fn handle_input( pub fn handle_input(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
case_idx: CaseIdx, case_idx: CaseIdx,
input: &Input, input: &Input,
node: &T::Blockchain, node: &T::Blockchain,
mode: &SolcMode,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> { ) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let deployment_receipts = let deployment_receipts =
self.handle_contract_deployment(metadata, case_idx, input, node)?; self.handle_contract_deployment(metadata, case_idx, input, node)?;
self.handle_input_execution(case_idx, input, deployment_receipts, node) let execution_receipt =
self.handle_input_execution(case_idx, input, deployment_receipts, node)?;
self.handle_input_expectations(case_idx, input, &execution_receipt, node, mode)?;
self.handle_input_diff(case_idx, execution_receipt, node)
} }
/// Handles the contract deployment for a given input performing it if it needs to be performed. /// Handles the contract deployment for a given input performing it if it needs to be performed.
@@ -165,12 +227,7 @@ where
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new(); let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in input.find_all_contract_instances().into_iter() { for instance in input.find_all_contract_instances().into_iter() {
if !self if !self.deployed_contracts(case_idx).contains_key(&instance) {
.deployed_contracts
.entry(case_idx)
.or_default()
.contains_key(&instance)
{
instances_we_must_deploy.entry(instance).or_insert(false); instances_we_must_deploy.entry(instance).or_insert(false);
} }
} }
@@ -186,19 +243,340 @@ where
let mut receipts = HashMap::new(); let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() { for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
// What we have at this moment is just a contract instance which is kind of like a variable let calldata = deploy_with_constructor_arguments.then_some(&input.calldata);
// name for an actual underlying contract. So, we need to resolve this instance to the info let value = deploy_with_constructor_arguments
// of the contract that it belongs to. .then_some(input.value)
let Some(ContractPathAndIdentifier { .flatten();
contract_source_path,
contract_ident, if let (_, _, Some(receipt)) = self.get_or_deploy_contract_instance(
}) = metadata.contract_sources()?.remove(&instance) &instance,
else { metadata,
tracing::error!("Contract source not found for instance"); case_idx,
anyhow::bail!("Contract source not found for instance {:?}", instance) input.caller,
calldata,
value,
node,
)? {
receipts.insert(instance.clone(), receipt);
}
}
Ok(receipts)
}
/// Handles the execution of the input in terms of the calls that need to be made.
fn handle_input_execution(
&mut self,
case_idx: CaseIdx,
input: &Input,
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
node: &T::Blockchain,
) -> anyhow::Result<TransactionReceipt> {
match input.method {
// This input was already executed when `handle_input` was called. We just need to
// lookup the transaction receipt in this case and continue on.
Method::Deployer => deployment_receipts
.remove(&input.instance)
.context("Failed to find deployment receipt"),
Method::Fallback | Method::FunctionName(_) => {
let tx = match input.legacy_transaction(self.deployed_contracts(case_idx), node) {
Ok(tx) => {
tracing::debug!("Legacy transaction data: {tx:#?}");
tx
}
Err(err) => {
tracing::error!("Failed to construct legacy transaction: {err:?}");
return Err(err);
}
}; };
let compiled_contract = self.contracts.iter().find_map(|output| { tracing::trace!("Executing transaction for input: {input:?}");
match node.execute_transaction(tx) {
Ok(receipt) => Ok(receipt),
Err(err) => {
tracing::error!(
"Failed to execute transaction when executing the contract: {}, {:?}",
&*input.instance,
err
);
Err(err)
}
}
}
}
}
fn handle_input_expectations(
&mut self,
case_idx: CaseIdx,
input: &Input,
execution_receipt: &TransactionReceipt,
node: &T::Blockchain,
mode: &SolcMode,
) -> anyhow::Result<()> {
let span = tracing::info_span!("Handling input expectations");
let _guard = span.enter();
// Resolving the `input.expected` into a series of expectations that we can then assert on.
let mut expectations = match input {
Input {
expected: Some(Expected::Calldata(calldata)),
..
} => vec![ExpectedOutput::new().with_calldata(calldata.clone())],
Input {
expected: Some(Expected::Expected(expected)),
..
} => vec![expected.clone()],
Input {
expected: Some(Expected::ExpectedMany(expected)),
..
} => expected.clone(),
Input { expected: None, .. } => vec![ExpectedOutput::new().with_success()],
};
// This is a bit of a special case and we have to support it separately on it's own. If it's
// a call to the deployer method, then the tests will assert that it "returns" the address
// of the contract. Deployments do not return the address of the contract but the runtime
// code of the contracts. Therefore, this assertion would always fail. So, we replace it
// with an assertion of "check if it succeeded"
if let Method::Deployer = &input.method {
for expectation in expectations.iter_mut() {
expectation.return_data = None;
}
}
// Note: we need to do assertions and checks on the output of the last call and this isn't
// available in the receipt. The only way to get this information is through tracing on the
// node.
let tracing_result = node
.trace_transaction(
execution_receipt,
GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
)),
..Default::default()
},
)?
.try_into_call_frame()
.expect("Impossible - we requested a callframe trace so we must get it back");
for expectation in expectations.iter() {
self.handle_input_expectation_item(
case_idx,
execution_receipt,
node,
expectation,
&tracing_result,
mode,
)?;
}
Ok(())
}
fn handle_input_expectation_item(
&mut self,
case_idx: CaseIdx,
execution_receipt: &TransactionReceipt,
node: &T::Blockchain,
expectation: &ExpectedOutput,
tracing_result: &CallFrame,
mode: &SolcMode,
) -> anyhow::Result<()> {
if let Some(ref version_requirement) = expectation.compiler_version {
let Some(compiler_version) = mode.last_patch_version(&self.config.solc) else {
anyhow::bail!("unsupported solc version: {:?}", &mode.solc_version);
};
if !version_requirement.matches(&compiler_version) {
return Ok(());
}
}
let deployed_contracts = self.deployed_contracts(case_idx);
let chain_state_provider = node;
// Handling the receipt state assertion.
let expected = !expectation.exception;
let actual = execution_receipt.status();
if actual != expected {
tracing::error!(expected, actual, "Transaction status assertion failed",);
anyhow::bail!(
"Transaction status assertion failed - Expected {expected} but got {actual}",
);
}
// Handling the calldata assertion
if let Some(ref expected_calldata) = expectation.return_data {
let expected = expected_calldata;
let actual = &tracing_result.output.as_ref().unwrap_or_default();
if !expected.is_equivalent(actual, deployed_contracts, chain_state_provider)? {
tracing::error!(
?execution_receipt,
?expected,
%actual,
"Calldata assertion failed"
);
anyhow::bail!("Calldata assertion failed - Expected {expected:?} but got {actual}",);
}
}
// Handling the events assertion
if let Some(ref expected_events) = expectation.events {
// Handling the events length assertion.
let expected = expected_events.len();
let actual = execution_receipt.logs().len();
if actual != expected {
tracing::error!(expected, actual, "Event count assertion failed",);
anyhow::bail!(
"Event count assertion failed - Expected {expected} but got {actual}",
);
}
// Handling the events assertion.
for (expected_event, actual_event) in
expected_events.iter().zip(execution_receipt.logs())
{
// Handling the emitter assertion.
if let Some(expected_address) = expected_event.address {
let expected = expected_address;
let actual = actual_event.address();
if actual != expected {
tracing::error!(
%expected,
%actual,
"Event emitter assertion failed",
);
anyhow::bail!(
"Event emitter assertion failed - Expected {expected} but got {actual}",
);
}
}
// Handling the topics assertion.
for (expected, actual) in expected_event
.topics
.as_slice()
.iter()
.zip(actual_event.topics())
{
let expected = Calldata::new_compound([expected]);
if !expected.is_equivalent(
&actual.0,
deployed_contracts,
chain_state_provider,
)? {
tracing::error!(
?execution_receipt,
?expected,
?actual,
"Event topics assertion failed",
);
anyhow::bail!(
"Event topics assertion failed - Expected {expected:?} but got {actual:?}",
);
}
}
// Handling the values assertion.
let expected = &expected_event.values;
let actual = &actual_event.data().data;
if !expected.is_equivalent(&actual.0, deployed_contracts, chain_state_provider)? {
tracing::error!(
?execution_receipt,
?expected,
?actual,
"Event value assertion failed",
);
anyhow::bail!(
"Event value assertion failed - Expected {expected:?} but got {actual:?}",
);
}
}
}
Ok(())
}
fn handle_input_diff(
&mut self,
_: CaseIdx,
execution_receipt: TransactionReceipt,
node: &T::Blockchain,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let span = tracing::info_span!("Handling input diff");
let _guard = span.enter();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let trace = node.trace_transaction(&execution_receipt, trace_options)?;
let diff = node.state_diff(&execution_receipt)?;
Ok((execution_receipt, trace, diff))
}
fn deployed_contracts(
&mut self,
case_idx: impl Into<Option<CaseIdx>>,
) -> &mut HashMap<ContractInstance, (Address, JsonAbi)> {
match case_idx.into() {
Some(case_idx) => self
.deployed_contracts
.entry(case_idx)
.or_insert_with(|| self.deployed_libraries.clone()),
None => &mut self.deployed_libraries,
}
}
/// Gets the information of a deployed contract or library from the state. If it's found to not
/// be deployed then it will be deployed.
///
/// If a [`CaseIdx`] is not specified then this contact instance address will be stored in the
/// cross-case deployed contracts address mapping.
#[allow(clippy::too_many_arguments)]
pub fn get_or_deploy_contract_instance(
&mut self,
contract_instance: &ContractInstance,
metadata: &Metadata,
case_idx: impl Into<Option<CaseIdx>>,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
node: &T::Blockchain,
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
let case_idx = case_idx.into();
if let Some((address, abi)) = self.deployed_libraries.get(contract_instance) {
return Ok((*address, abi.clone(), None));
}
if let Some(case_idx) = case_idx {
if let Some((address, abi)) = self
.deployed_contracts
.get(&case_idx)
.and_then(|contracts| contracts.get(contract_instance))
{
return Ok((*address, abi.clone(), None));
}
}
let Some(ContractPathAndIdent {
contract_source_path,
contract_ident,
}) = metadata.contract_sources()?.remove(contract_instance)
else {
tracing::error!("Contract source not found for instance");
anyhow::bail!(
"Contract source not found for instance {:?}",
contract_instance
)
};
let compiled_contract = self.contracts.iter().rev().find_map(|output| {
output output
.contracts .contracts
.as_ref()? .as_ref()?
@@ -215,13 +593,12 @@ where
contract_ident = contract_ident.as_ref(), contract_ident = contract_ident.as_ref(),
"Failed to find bytecode for contract" "Failed to find bytecode for contract"
); );
anyhow::bail!("Failed to find bytecode for contract {:?}", instance) anyhow::bail!(
"Failed to find bytecode for contract {:?}",
contract_instance
)
}; };
// TODO: When we want to do linking it would be best to do it at this stage here. We have
// the context from the metadata files and therefore know what needs to be linked and in
// what order it needs to happen.
let mut code = match alloy::hex::decode(&code.object) { let mut code = match alloy::hex::decode(&code.object) {
Ok(code) => code, Ok(code) => code,
Err(error) => { Err(error) => {
@@ -235,14 +612,39 @@ where
} }
}; };
if deploy_with_constructor_arguments { let Some(Value::String(metadata)) =
let encoded_input = input compiled_contract.and_then(|contract| contract.metadata.as_ref())
.encoded_input(self.deployed_contracts.entry(case_idx).or_default(), node)?; else {
code.extend(encoded_input.to_vec()); tracing::error!("Contract does not have a metadata field");
anyhow::bail!("Contract does not have a metadata field");
};
let Ok(metadata) = serde_json::from_str::<Value>(metadata) else {
tracing::error!(%metadata, "Failed to parse solc metadata into a structured value");
anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}");
};
let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else {
tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata");
anyhow::bail!("Failed to access the .output.abi field of the solc metadata {metadata}");
};
let Ok(abi) = serde_json::from_value::<JsonAbi>(abi.clone()) else {
tracing::error!(%metadata, "Failed to deserialize ABI into a structured format");
anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}");
};
if let Some(calldata) = calldata {
let calldata = calldata.calldata(self.deployed_contracts(case_idx), node)?;
code.extend(calldata);
} }
let tx = { let tx = {
let tx = TransactionRequest::default().from(input.caller); let tx = TransactionRequest::default().from(deployer);
let tx = match value {
Some(ref value) => tx.value(value.into_inner()),
_ => tx,
};
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code) TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
}; };
@@ -263,109 +665,15 @@ where
anyhow::bail!("Contract deployment didn't return an address"); anyhow::bail!("Contract deployment didn't return an address");
}; };
tracing::info!( tracing::info!(
instance_name = ?instance, instance_name = ?contract_instance,
instance_address = ?address, instance_address = ?address,
"Deployed contract" "Deployed contract"
); );
let Some(Value::String(metadata)) = self.deployed_contracts(case_idx)
compiled_contract.and_then(|contract| contract.metadata.as_ref()) .insert(contract_instance.clone(), (address, abi.clone()));
else {
tracing::error!("Contract does not have a metadata field");
anyhow::bail!("Contract does not have a metadata field");
};
let Ok(metadata) = serde_json::from_str::<Value>(metadata) else { Ok((address, abi, Some(receipt)))
tracing::error!(%metadata, "Failed to parse solc metadata into a structured value");
anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}");
};
let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else {
tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata");
anyhow::bail!(
"Failed to access the .output.abi field of the solc metadata {metadata}"
);
};
let Ok(abi) = serde_json::from_value::<JsonAbi>(abi.clone()) else {
tracing::error!(%metadata, "Failed to deserialize ABI into a structured format");
anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}");
};
self.deployed_contracts
.entry(case_idx)
.or_default()
.insert(instance.clone(), (address, abi));
receipts.insert(instance.clone(), receipt);
}
Ok(receipts)
}
/// Handles the execution of the input in terms of the calls that need to be made.
fn handle_input_execution(
&mut self,
case_idx: CaseIdx,
input: &Input,
deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
node: &T::Blockchain,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
tracing::trace!("Calling execute_input for input: {input:?}");
let receipt = match input.method {
// This input was already executed when `handle_input` was called. We just need to
// lookup the transaction receipt in this case and continue on.
Method::Deployer => deployment_receipts
.get(&input.instance)
.context("Failed to find deployment receipt")?
.clone(),
Method::Fallback | Method::FunctionName(_) => {
let tx = match input
.legacy_transaction(self.deployed_contracts.entry(case_idx).or_default(), node)
{
Ok(tx) => {
tracing::debug!("Legacy transaction data: {tx:#?}");
tx
}
Err(err) => {
tracing::error!("Failed to construct legacy transaction: {err:?}");
return Err(err);
}
};
tracing::trace!("Executing transaction for input: {input:?}");
match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(err) => {
tracing::error!(
"Failed to execute transaction when executing the contract: {}, {:?}",
&*input.instance,
err
);
return Err(err);
}
}
}
};
tracing::trace!(
"Transaction receipt for executed contract: {} - {:?}",
&*input.instance,
receipt,
);
let trace = node.trace_transaction(receipt.clone())?;
tracing::trace!(
"Trace result for contract: {} - {:?}",
&*input.instance,
trace
);
let diff = node.state_diff(receipt.clone())?;
Ok((receipt, trace, diff))
} }
} }
@@ -450,6 +758,22 @@ where
let tracing_span = tracing::info_span!("Handling metadata file"); let tracing_span = tracing::info_span!("Handling metadata file");
let _guard = tracing_span.enter(); let _guard = tracing_span.enter();
// We only execute this input if it's valid for the leader and the follower. Otherwise, we
// skip it with a warning.
if !self
.leader_node
.matches_target(self.metadata.targets.as_deref())
|| !self
.follower_node
.matches_target(self.metadata.targets.as_deref())
{
tracing::warn!(
targets = ?self.metadata.targets,
"Either the leader or follower node do not support the targets of the file"
);
return execution_result;
}
for mode in self.metadata.solc_modes() { for mode in self.metadata.solc_modes() {
let tracing_span = tracing::info_span!("With solc mode", solc_mode = ?mode); let tracing_span = tracing::info_span!("With solc mode", solc_mode = ?mode);
let _guard = tracing_span.enter(); let _guard = tracing_span.enter();
@@ -457,6 +781,42 @@ where
let mut leader_state = State::<L>::new(self.config, span); let mut leader_state = State::<L>::new(self.config, span);
let mut follower_state = State::<F>::new(self.config, span); let mut follower_state = State::<F>::new(self.config, span);
// Note: we are currently forced to do two compilation passes due to linking. In the
// first compilation pass we compile the libraries and publish them to the chain. In the
// second compilation pass we compile the contracts with the library addresses so that
// they're linked at compile-time.
let build_result = tracing::info_span!("Building and publishing libraries")
.in_scope(|| {
match leader_state.build_and_publish_libraries(self.metadata, &mode, self.leader_node) {
Ok(_) => {
tracing::debug!(target = ?Target::Leader, "Library building succeeded");
execution_result.add_successful_build(Target::Leader, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Leader, ?error, "Library building failed");
execution_result.add_failed_build(Target::Leader, mode.clone(), error);
return Err(());
}
}
match follower_state.build_and_publish_libraries(self.metadata, &mode, self.follower_node) {
Ok(_) => {
tracing::debug!(target = ?Target::Follower, "Library building succeeded");
execution_result.add_successful_build(Target::Follower, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Follower, ?error, "Library building failed");
execution_result.add_failed_build(Target::Follower, mode.clone(), error);
return Err(());
}
}
Ok(())
});
if build_result.is_err() {
// Note: We skip to the next solc mode as there's nothing that we can do at this
// point, the building has failed. We do NOT bail out of the execution as a whole.
continue;
}
// We build the contracts. If building the contracts for the metadata file fails then we // We build the contracts. If building the contracts for the metadata file fails then we
// have no other option but to keep note of this error and move on to the next solc mode // have no other option but to keep note of this error and move on to the next solc mode
// and NOT just bail out of the execution as a whole. // and NOT just bail out of the execution as a whole.
@@ -501,11 +861,11 @@ where
); );
let _guard = tracing_span.enter(); let _guard = tracing_span.enter();
let case_idx = CaseIdx::new_from(case_idx); let case_idx = CaseIdx::new(case_idx);
// For inputs if one of the inputs fail we move on to the next case (we do not move // For inputs if one of the inputs fail we move on to the next case (we do not move
// on to the next input as it doesn't make sense. It depends on the previous one). // on to the next input as it doesn't make sense. It depends on the previous one).
for (input_idx, input) in case.inputs.iter().enumerate() { for (input_idx, input) in case.inputs_iterator().enumerate() {
let tracing_span = tracing::info_span!("Handling input", input_idx); let tracing_span = tracing::info_span!("Handling input", input_idx);
let _guard = tracing_span.enter(); let _guard = tracing_span.enter();
@@ -513,8 +873,13 @@ where
tracing::info_span!("Executing input", contract_name = ?input.instance) tracing::info_span!("Executing input", contract_name = ?input.instance)
.in_scope(|| { .in_scope(|| {
let (leader_receipt, _, leader_diff) = match leader_state let (leader_receipt, _, leader_diff) = match leader_state
.handle_input(self.metadata, case_idx, input, self.leader_node) .handle_input(
{ self.metadata,
case_idx,
&input,
self.leader_node,
&mode,
) {
Ok(result) => result, Ok(result) => result,
Err(error) => { Err(error) => {
tracing::error!( tracing::error!(
@@ -541,8 +906,9 @@ where
.handle_input( .handle_input(
self.metadata, self.metadata,
case_idx, case_idx,
input, &input,
self.follower_node, self.follower_node,
&mode,
) { ) {
Ok(result) => result, Ok(result) => result,
Err(error) => { Err(error) => {
@@ -589,14 +955,6 @@ where
tracing::trace!("Leader logs: {:?}", leader_receipt.logs()); tracing::trace!("Leader logs: {:?}", leader_receipt.logs());
tracing::trace!("Follower logs: {:?}", follower_receipt.logs()); tracing::trace!("Follower logs: {:?}", follower_receipt.logs());
} }
if leader_receipt.status() != follower_receipt.status() {
tracing::debug!(
"Mismatch in status: leader = {}, follower = {}",
leader_receipt.status(),
follower_receipt.status()
);
}
} }
// Note: Only consider the case as having been successful after we have processed // Note: Only consider the case as having been successful after we have processed
+3 -3
View File
@@ -5,17 +5,17 @@
use revive_dt_compiler::{SolidityCompiler, revive_resolc, solc}; use revive_dt_compiler::{SolidityCompiler, revive_resolc, solc};
use revive_dt_config::TestingPlatform; use revive_dt_config::TestingPlatform;
use revive_dt_node::{geth, kitchensink::KitchensinkNode}; use revive_dt_format::traits::ResolverApi;
use revive_dt_node::{Node, geth, kitchensink::KitchensinkNode};
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
pub mod common;
pub mod driver; pub mod driver;
/// One platform can be tested differentially against another. /// One platform can be tested differentially against another.
/// ///
/// For this we need a blockchain node implementation and a compiler. /// For this we need a blockchain node implementation and a compiler.
pub trait Platform { pub trait Platform {
type Blockchain: EthereumNode; type Blockchain: EthereumNode + Node + ResolverApi;
type Compiler: SolidityCompiler; type Compiler: SolidityCompiler;
/// Returns the matching [TestingPlatform] of the [revive_dt_config::Arguments]. /// Returns the matching [TestingPlatform] of the [revive_dt_config::Arguments].
+1 -1
View File
@@ -9,7 +9,7 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
revive-dt-node-interaction = { workspace = true } revive-dt-common = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true } alloy-primitives = { workspace = true }
+34 -2
View File
@@ -1,6 +1,11 @@
use serde::Deserialize; use serde::Deserialize;
use crate::{define_wrapper_type, input::Input, mode::Mode}; use revive_dt_common::macros::define_wrapper_type;
use crate::{
input::{Expected, Input},
mode::Mode,
};
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
pub struct Case { pub struct Case {
@@ -9,10 +14,37 @@ pub struct Case {
pub modes: Option<Vec<Mode>>, pub modes: Option<Vec<Mode>>,
pub inputs: Vec<Input>, pub inputs: Vec<Input>,
pub group: Option<String>, pub group: Option<String>,
pub expected: Option<Expected>,
}
impl Case {
pub fn inputs_iterator(&self) -> impl Iterator<Item = Input> {
let inputs_len = self.inputs.len();
self.inputs
.clone()
.into_iter()
.enumerate()
.map(move |(idx, mut input)| {
if idx + 1 == inputs_len {
if input.expected.is_none() {
input.expected = self.expected.clone();
}
// TODO: What does it mean for us to have an `expected` field on the case itself
// but the final input also has an expected field that doesn't match the one on
// the case? What are we supposed to do with that final expected field on the
// case?
input
} else {
input
}
})
}
} }
define_wrapper_type!( define_wrapper_type!(
/// A wrapper type for the index of test cases found in metadata file. /// A wrapper type for the index of test cases found in metadata file.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
CaseIdx(usize); pub struct CaseIdx(usize);
); );
+537 -194
View File
@@ -7,26 +7,28 @@ use alloy::{
primitives::{Address, Bytes, U256}, primitives::{Address, Bytes, U256},
rpc::types::TransactionRequest, rpc::types::TransactionRequest,
}; };
use alloy_primitives::{FixedBytes, utils::parse_units};
use anyhow::Context;
use semver::VersionReq; use semver::VersionReq;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use serde_json::Value;
use revive_dt_node_interaction::EthereumNode; use revive_dt_common::macros::define_wrapper_type;
use crate::metadata::ContractInstance; use crate::metadata::ContractInstance;
use crate::traits::ResolverApi;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Input { pub struct Input {
#[serde(default = "default_caller")] #[serde(default = "Input::default_caller")]
pub caller: Address, pub caller: Address,
pub comment: Option<String>, pub comment: Option<String>,
#[serde(default = "default_instance")] #[serde(default = "Input::default_instance")]
pub instance: ContractInstance, pub instance: ContractInstance,
pub method: Method, pub method: Method,
#[serde(default)] #[serde(default)]
pub calldata: Calldata, pub calldata: Calldata,
pub expected: Option<Expected>, pub expected: Option<Expected>,
pub value: Option<String>, pub value: Option<EtherValue>,
pub storage: Option<HashMap<String, Calldata>>, pub storage: Option<HashMap<String, Calldata>>,
} }
@@ -40,17 +42,100 @@ pub enum Expected {
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct ExpectedOutput { pub struct ExpectedOutput {
compiler_version: Option<VersionReq>, pub compiler_version: Option<VersionReq>,
return_data: Option<Calldata>, pub return_data: Option<Calldata>,
events: Option<Value>, pub events: Option<Vec<Event>>,
exception: Option<bool>, #[serde(default)]
pub exception: bool,
} }
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Event {
pub address: Option<Address>,
pub topics: Vec<String>,
pub values: Calldata,
}
/// A type definition for the calldata supported by the testing framework.
///
/// We choose to document all of the types used in [`Calldata`] in this one doc comment to elaborate
/// on why they exist and consolidate all of the documentation for calldata in a single place where
/// it can be viewed and understood.
///
/// The [`Single`] variant of this enum is quite simple and straightforward: it's a hex-encoded byte
/// array of the calldata.
///
/// The [`Compound`] type is more intricate and allows for capabilities such as resolution and some
/// simple arithmetic operations. It houses a vector of [`CalldataItem`]s which is just a wrapper
/// around an owned string.
///
/// A [`CalldataItem`] could be a simple hex string of a single calldata argument, but it could also
/// be something that requires resolution such as `MyContract.address` which is a variable that is
/// understood by the resolution logic to mean "Lookup the address of this particular contract
/// instance".
///
/// In addition to the above, the format supports some simple arithmetic operations like add, sub,
/// divide, multiply, bitwise AND, bitwise OR, and bitwise XOR. Our parser understands the [reverse
/// polish notation] simply because it's easy to write a calculator for that notation and since we
/// do not have plans to use arithmetic too often in tests. In reverse polish notation a typical
/// `2 + 4` would be written as `2 4 +` which makes this notation very simple to implement through
/// a stack.
///
/// Combining the above, a single [`CalldataItem`] could employ both resolution and arithmetic at
/// the same time. For example, a [`CalldataItem`] of `$BLOCK_NUMBER $BLOCK_NUMBER +` means that
/// the block number should be retrieved and then it should be added to itself.
///
/// Internally, we split the [`CalldataItem`] by spaces. Therefore, `$BLOCK_NUMBER $BLOCK_NUMBER+`
/// is invalid but `$BLOCK_NUMBER $BLOCK_NUMBER +` is valid and can be understood by the parser and
/// calculator. After the split is done, each token is parsed into a [`CalldataToken<&str>`] forming
/// an [`Iterator`] over [`CalldataToken<&str>`]. A [`CalldataToken<&str>`] can then be resolved
/// into a [`CalldataToken<U256>`] through the resolution logic. Finally, after resolution is done,
/// this iterator of [`CalldataToken<U256>`] is collapsed into the final result by applying the
/// arithmetic operations requested.
///
/// For example, supplying a [`Compound`] calldata of `0xdeadbeef` produces an iterator of a single
/// [`CalldataToken<&str>`] items of the value [`CalldataToken::Item`] of the string value 12 which
/// we can then resolve into the appropriate [`U256`] value and convert into calldata.
///
/// In summary, the various types used in [`Calldata`] represent the following:
/// - [`CalldataItem`]: A calldata string from the metadata files.
/// - [`CalldataToken<&str>`]: Typically used in an iterator of items from the space splitted
/// [`CalldataItem`] and represents a token that has not yet been resolved into its value.
/// - [`CalldataToken<U256>`]: Represents a token that's been resolved from being a string and into
/// the word-size calldata argument on which we can perform arithmetic.
///
/// [`Single`]: Calldata::Single
/// [`Compound`]: Calldata::Compound
/// [reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum Calldata { pub enum Calldata {
Single(String), Single(Bytes),
Compound(Vec<String>), Compound(Vec<CalldataItem>),
}
define_wrapper_type! {
/// This represents an item in the [`Calldata::Compound`] variant.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CalldataItem(String);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
enum CalldataToken<T> {
Item(T),
Operation(Operation),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
enum Operation {
Addition,
Subtraction,
Multiplication,
Division,
BitwiseAnd,
BitwiseOr,
BitwiseXor,
} }
/// Specify how the contract is called. /// Specify how the contract is called.
@@ -74,67 +159,22 @@ pub enum Method {
FunctionName(String), FunctionName(String),
} }
impl Default for Calldata { define_wrapper_type!(
fn default() -> Self { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
Self::Compound(Default::default()) pub struct EtherValue(U256);
} );
}
impl Calldata {
pub fn find_all_contract_instances(&self, vec: &mut Vec<ContractInstance>) {
if let Calldata::Compound(compound) = self {
for item in compound {
if let Some(instance) = item.strip_suffix(".address") {
vec.push(ContractInstance::new_from(instance))
}
}
}
}
pub fn construct_call_data(
&self,
buffer: &mut Vec<u8>,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<()> {
match self {
Calldata::Single(string) => {
alloy::hex::decode_to_slice(string, buffer)?;
}
Calldata::Compound(items) => {
for (arg_idx, arg) in items.iter().enumerate() {
match resolve_argument(arg, deployed_contracts, chain_state_provider) {
Ok(resolved) => {
buffer.extend(resolved.to_be_bytes::<32>());
}
Err(error) => {
tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument");
return Err(error);
}
};
}
}
};
Ok(())
}
pub fn size_requirement(&self) -> usize {
match self {
Calldata::Single(single) => (single.len() - 2) / 2,
Calldata::Compound(items) => items.len() * 32,
}
}
}
impl ExpectedOutput {
pub fn find_all_contract_instances(&self, vec: &mut Vec<ContractInstance>) {
if let Some(ref cd) = self.return_data {
cd.find_all_contract_instances(vec);
}
}
}
impl Input { impl Input {
pub const fn default_caller() -> Address {
Address(FixedBytes(alloy::hex!(
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
)))
}
fn default_instance() -> ContractInstance {
ContractInstance::new("Test")
}
fn instance_to_address( fn instance_to_address(
&self, &self,
instance: &ContractInstance, instance: &ContractInstance,
@@ -149,16 +189,13 @@ impl Input {
pub fn encoded_input( pub fn encoded_input(
&self, &self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl EthereumNode, chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<Bytes> { ) -> anyhow::Result<Bytes> {
match self.method { match self.method {
Method::Deployer | Method::Fallback => { Method::Deployer | Method::Fallback => {
let mut calldata = Vec::<u8>::with_capacity(self.calldata.size_requirement()); let calldata = self
self.calldata.construct_call_data( .calldata
&mut calldata, .calldata(deployed_contracts, chain_state_provider)?;
deployed_contracts,
chain_state_provider,
)?;
Ok(calldata.into()) Ok(calldata.into())
} }
@@ -204,7 +241,7 @@ impl Input {
// a new buffer for each one of the resolved arguments. // a new buffer for each one of the resolved arguments.
let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement()); let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement());
calldata.extend(function.selector().0); calldata.extend(function.selector().0);
self.calldata.construct_call_data( self.calldata.calldata_into_slice(
&mut calldata, &mut calldata,
deployed_contracts, deployed_contracts,
chain_state_provider, chain_state_provider,
@@ -219,10 +256,14 @@ impl Input {
pub fn legacy_transaction( pub fn legacy_transaction(
&self, &self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl EthereumNode, chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<TransactionRequest> { ) -> anyhow::Result<TransactionRequest> {
let input_data = self.encoded_input(deployed_contracts, chain_state_provider)?; let input_data = self.encoded_input(deployed_contracts, chain_state_provider)?;
let transaction_request = TransactionRequest::default(); let transaction_request = TransactionRequest::default().from(self.caller).value(
self.value
.map(|value| value.into_inner())
.unwrap_or_default(),
);
match self.method { match self.method {
Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)), Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)),
_ => Ok(transaction_request _ => Ok(transaction_request
@@ -236,57 +277,262 @@ impl Input {
vec.push(self.instance.clone()); vec.push(self.instance.clone());
self.calldata.find_all_contract_instances(&mut vec); self.calldata.find_all_contract_instances(&mut vec);
match &self.expected {
Some(Expected::Calldata(cd)) => {
cd.find_all_contract_instances(&mut vec);
}
Some(Expected::Expected(expected)) => {
expected.find_all_contract_instances(&mut vec);
}
Some(Expected::ExpectedMany(expected)) => {
for expected in expected {
expected.find_all_contract_instances(&mut vec);
}
}
None => {}
}
vec vec
} }
} }
fn default_instance() -> ContractInstance { impl ExpectedOutput {
ContractInstance::new_from("Test") pub fn new() -> Self {
Default::default()
} }
fn default_caller() -> Address { pub fn with_success(mut self) -> Self {
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap() self.exception = false;
self
} }
/// This function takes in the string calldata argument provided in the JSON input and resolves it pub fn with_failure(mut self) -> Self {
/// into a [`U256`] which is later used to construct the calldata. self.exception = true;
self
}
pub fn with_calldata(mut self, calldata: Calldata) -> Self {
self.return_data = Some(calldata);
self
}
}
impl Default for Calldata {
fn default() -> Self {
Self::Compound(Default::default())
}
}
impl Calldata {
pub fn new_single(item: impl Into<Bytes>) -> Self {
Self::Single(item.into())
}
pub fn new_compound(items: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
Self::Compound(
items
.into_iter()
.map(|item| item.as_ref().to_owned())
.map(CalldataItem::new)
.collect(),
)
}
pub fn find_all_contract_instances(&self, vec: &mut Vec<ContractInstance>) {
if let Calldata::Compound(compound) = self {
for item in compound {
if let Some(instance) =
item.strip_suffix(CalldataToken::<()>::ADDRESS_VARIABLE_SUFFIX)
{
vec.push(ContractInstance::new(instance))
}
}
}
}
pub fn calldata(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<Vec<u8>> {
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
self.calldata_into_slice(&mut buffer, deployed_contracts, chain_state_provider)?;
Ok(buffer)
}
pub fn calldata_into_slice(
&self,
buffer: &mut Vec<u8>,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<()> {
match self {
Calldata::Single(bytes) => {
buffer.extend_from_slice(bytes);
}
Calldata::Compound(items) => {
for (arg_idx, arg) in items.iter().enumerate() {
match arg.resolve(deployed_contracts, chain_state_provider) {
Ok(resolved) => {
buffer.extend(resolved.to_be_bytes::<32>());
}
Err(error) => {
tracing::error!(?arg, arg_idx, ?error, "Failed to resolve argument");
return Err(error);
}
};
}
}
};
Ok(())
}
pub fn size_requirement(&self) -> usize {
match self {
Calldata::Single(single) => single.len(),
Calldata::Compound(items) => items.len() * 32,
}
}
/// Checks if this [`Calldata`] is equivalent to the passed calldata bytes.
pub fn is_equivalent(
&self,
other: &[u8],
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<bool> {
match self {
Calldata::Single(calldata) => Ok(calldata == other),
Calldata::Compound(items) => {
// Chunking the "other" calldata into 32 byte chunks since each
// one of the items in the compound calldata represents 32 bytes
for (this, other) in items.iter().zip(other.chunks(32)) {
// The matterlabs format supports wildcards and therefore we
// also need to support them.
if this.as_ref() == "*" {
continue;
}
let other = if other.len() < 32 {
let mut vec = other.to_vec();
vec.resize(32, 0);
std::borrow::Cow::Owned(vec)
} else {
std::borrow::Cow::Borrowed(other)
};
let this = this.resolve(deployed_contracts, chain_state_provider)?;
let other = U256::from_be_slice(&other);
if this != other {
return Ok(false);
}
}
Ok(true)
}
}
}
}
impl CalldataItem {
fn resolve(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<U256> {
let mut stack = Vec::<CalldataToken<U256>>::new();
for token in self
.calldata_tokens()
.map(|token| token.resolve(deployed_contracts, chain_state_provider))
{
let token = token?;
let new_token = match token {
CalldataToken::Item(_) => token,
CalldataToken::Operation(operation) => {
let right_operand = stack
.pop()
.and_then(CalldataToken::into_item)
.context("Invalid calldata arithmetic operation")?;
let left_operand = stack
.pop()
.and_then(CalldataToken::into_item)
.context("Invalid calldata arithmetic operation")?;
let result = match operation {
Operation::Addition => left_operand.checked_add(right_operand),
Operation::Subtraction => left_operand.checked_sub(right_operand),
Operation::Multiplication => left_operand.checked_mul(right_operand),
Operation::Division => left_operand.checked_div(right_operand),
Operation::BitwiseAnd => Some(left_operand & right_operand),
Operation::BitwiseOr => Some(left_operand | right_operand),
Operation::BitwiseXor => Some(left_operand ^ right_operand),
}
.context("Invalid calldata arithmetic operation")?;
CalldataToken::Item(result)
}
};
stack.push(new_token)
}
match stack.as_slice() {
// Empty stack means that we got an empty compound calldata which we resolve to zero.
[] => Ok(U256::ZERO),
[CalldataToken::Item(item)] => Ok(*item),
_ => Err(anyhow::anyhow!("Invalid calldata arithmetic operation")),
}
}
fn calldata_tokens<'a>(&'a self) -> impl Iterator<Item = CalldataToken<&'a str>> + 'a {
self.0.split(' ').map(|item| match item {
"+" => CalldataToken::Operation(Operation::Addition),
"-" => CalldataToken::Operation(Operation::Subtraction),
"/" => CalldataToken::Operation(Operation::Division),
"*" => CalldataToken::Operation(Operation::Multiplication),
"&" => CalldataToken::Operation(Operation::BitwiseAnd),
"|" => CalldataToken::Operation(Operation::BitwiseOr),
"^" => CalldataToken::Operation(Operation::BitwiseXor),
_ => CalldataToken::Item(item),
})
}
}
impl<T> CalldataToken<T> {
const ADDRESS_VARIABLE_SUFFIX: &str = ".address";
const NEGATIVE_VALUE_PREFIX: char = '-';
const HEX_LITERAL_PREFIX: &str = "0x";
const CHAIN_VARIABLE: &str = "$CHAIN_ID";
const GAS_LIMIT_VARIABLE: &str = "$GAS_LIMIT";
const COINBASE_VARIABLE: &str = "$COINBASE";
const DIFFICULTY_VARIABLE: &str = "$DIFFICULTY";
const BLOCK_HASH_VARIABLE_PREFIX: &str = "$BLOCK_HASH";
const BLOCK_NUMBER_VARIABLE: &str = "$BLOCK_NUMBER";
const BLOCK_TIMESTAMP_VARIABLE: &str = "$BLOCK_TIMESTAMP";
fn into_item(self) -> Option<T> {
match self {
CalldataToken::Item(item) => Some(item),
CalldataToken::Operation(_) => None,
}
}
}
impl<T: AsRef<str>> CalldataToken<T> {
/// This function takes in the string calldata argument provided in the JSON input and resolves
/// it into a [`U256`] which is later used to construct the calldata.
/// ///
/// # Note /// # Note
/// ///
/// This piece of code is taken from the matter-labs-tester repository which is licensed under MIT /// This piece of code is taken from the matter-labs-tester repository which is licensed under
/// or Apache. The original source code can be found here: /// MIT or Apache. The original source code can be found here:
/// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146 /// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146
fn resolve_argument( fn resolve(
value: &str, self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl EthereumNode, chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<U256> { ) -> anyhow::Result<CalldataToken<U256>> {
if let Some(instance) = value.strip_suffix(".address") { match self {
Self::Item(item) => {
let item = item.as_ref();
let value = if let Some(instance) = item.strip_suffix(Self::ADDRESS_VARIABLE_SUFFIX)
{
Ok(U256::from_be_slice( Ok(U256::from_be_slice(
deployed_contracts deployed_contracts
.get(&ContractInstance::new_from(instance)) .get(&ContractInstance::new(instance))
.map(|(a, _)| *a) .map(|(a, _)| *a)
.ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))? .ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))?
.as_ref(), .as_ref(),
)) ))
} else if let Some(value) = value.strip_prefix('-') { } else if let Some(value) = item.strip_prefix(Self::NEGATIVE_VALUE_PREFIX) {
let value = U256::from_str_radix(value, 10) let value = U256::from_str_radix(value, 10).map_err(|error| {
.map_err(|error| anyhow::anyhow!("Invalid decimal literal after `-`: {}", error))?; anyhow::anyhow!("Invalid decimal literal after `-`: {}", error)
})?;
if value > U256::ONE << 255u8 { if value > U256::ONE << 255u8 {
anyhow::bail!("Decimal literal after `-` is too big"); anyhow::bail!("Decimal literal after `-` is too big");
} }
@@ -294,23 +540,26 @@ fn resolve_argument(
.checked_sub(U256::ONE) .checked_sub(U256::ONE)
.ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?; .ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?;
Ok(U256::MAX.checked_sub(value).expect("Always valid")) Ok(U256::MAX.checked_sub(value).expect("Always valid"))
} else if let Some(value) = value.strip_prefix("0x") { } else if let Some(value) = item.strip_prefix(Self::HEX_LITERAL_PREFIX) {
Ok(U256::from_str_radix(value, 16) Ok(U256::from_str_radix(value, 16).map_err(|error| {
.map_err(|error| anyhow::anyhow!("Invalid hexadecimal literal: {}", error))?) anyhow::anyhow!("Invalid hexadecimal literal: {}", error)
} else if value == "$CHAIN_ID" { })?)
} else if item == Self::CHAIN_VARIABLE {
let chain_id = chain_state_provider.chain_id()?; let chain_id = chain_state_provider.chain_id()?;
Ok(U256::from(chain_id)) Ok(U256::from(chain_id))
} else if value == "$GAS_LIMIT" { } else if item == Self::GAS_LIMIT_VARIABLE {
let gas_limit = chain_state_provider.block_gas_limit(BlockNumberOrTag::Latest)?; let gas_limit =
chain_state_provider.block_gas_limit(BlockNumberOrTag::Latest)?;
Ok(U256::from(gas_limit)) Ok(U256::from(gas_limit))
} else if value == "$COINBASE" { } else if item == Self::COINBASE_VARIABLE {
let coinbase = chain_state_provider.block_coinbase(BlockNumberOrTag::Latest)?; let coinbase = chain_state_provider.block_coinbase(BlockNumberOrTag::Latest)?;
Ok(U256::from_be_slice(coinbase.as_ref())) Ok(U256::from_be_slice(coinbase.as_ref()))
} else if value == "$DIFFICULTY" { } else if item == Self::DIFFICULTY_VARIABLE {
let block_difficulty = chain_state_provider.block_difficulty(BlockNumberOrTag::Latest)?; let block_difficulty =
chain_state_provider.block_difficulty(BlockNumberOrTag::Latest)?;
Ok(block_difficulty) Ok(block_difficulty)
} else if value.starts_with("$BLOCK_HASH") { } else if item.starts_with(Self::BLOCK_HASH_VARIABLE_PREFIX) {
let offset: u64 = value let offset: u64 = item
.split(':') .split(':')
.next_back() .next_back()
.and_then(|value| value.parse().ok()) .and_then(|value| value.parse().ok())
@@ -319,18 +568,51 @@ fn resolve_argument(
let current_block_number = chain_state_provider.last_block_number()?; let current_block_number = chain_state_provider.last_block_number()?;
let desired_block_number = current_block_number - offset; let desired_block_number = current_block_number - offset;
let block_hash = chain_state_provider.block_hash(desired_block_number.into())?; let block_hash =
chain_state_provider.block_hash(desired_block_number.into())?;
Ok(U256::from_be_bytes(block_hash.0)) Ok(U256::from_be_bytes(block_hash.0))
} else if value == "$BLOCK_NUMBER" { } else if item == Self::BLOCK_NUMBER_VARIABLE {
let current_block_number = chain_state_provider.last_block_number()?; let current_block_number = chain_state_provider.last_block_number()?;
Ok(U256::from(current_block_number)) Ok(U256::from(current_block_number))
} else if value == "$BLOCK_TIMESTAMP" { } else if item == Self::BLOCK_TIMESTAMP_VARIABLE {
let timestamp = chain_state_provider.block_timestamp(BlockNumberOrTag::Latest)?; let timestamp =
chain_state_provider.block_timestamp(BlockNumberOrTag::Latest)?;
Ok(U256::from(timestamp)) Ok(U256::from(timestamp))
} else { } else {
Ok(U256::from_str_radix(value, 10) Ok(U256::from_str_radix(item, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?)
};
value.map(CalldataToken::Item)
}
Self::Operation(operation) => Ok(CalldataToken::Operation(operation)),
}
}
}
impl Serialize for EtherValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
format!("{} wei", self.0).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for EtherValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
let mut splitted = string.split(' ');
let (Some(value), Some(unit)) = (splitted.next(), splitted.next()) else {
return Err(serde::de::Error::custom("Failed to parse the value"));
};
let parsed = parse_units(value, unit.replace("eth", "ether"))
.map_err(|_| serde::de::Error::custom("Failed to parse units"))?
.into();
Ok(Self(parsed))
} }
} }
@@ -343,34 +625,9 @@ mod tests {
use alloy_sol_types::SolValue; use alloy_sol_types::SolValue;
use std::collections::HashMap; use std::collections::HashMap;
struct DummyEthereumNode; struct MockResolver;
impl EthereumNode for DummyEthereumNode {
fn execute_transaction(
&self,
_: TransactionRequest,
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
unimplemented!()
}
fn trace_transaction(
&self,
_: alloy::rpc::types::TransactionReceipt,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
unimplemented!()
}
fn state_diff(
&self,
_: alloy::rpc::types::TransactionReceipt,
) -> anyhow::Result<alloy::rpc::types::trace::geth::DiffMode> {
unimplemented!()
}
fn fetch_add_nonce(&self, _: Address) -> anyhow::Result<u64> {
unimplemented!()
}
impl ResolverApi for MockResolver {
fn chain_id(&self) -> anyhow::Result<alloy_primitives::ChainId> { fn chain_id(&self) -> anyhow::Result<alloy_primitives::ChainId> {
Ok(0x123) Ok(0x123)
} }
@@ -430,19 +687,19 @@ mod tests {
.0; .0;
let input = Input { let input = Input {
instance: ContractInstance::new_from("Contract"), instance: ContractInstance::new("Contract"),
method: Method::FunctionName("store".to_owned()), method: Method::FunctionName("store".to_owned()),
calldata: Calldata::Compound(vec!["42".into()]), calldata: Calldata::new_compound(["42"]),
..Default::default() ..Default::default()
}; };
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new_from("Contract"), ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi), (Address::ZERO, parsed_abi),
); );
let encoded = input.encoded_input(&contracts, &DummyEthereumNode).unwrap(); let encoded = input.encoded_input(&contracts, &MockResolver).unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (u64,); type T = (u64,);
@@ -474,19 +731,17 @@ mod tests {
let input: Input = Input { let input: Input = Input {
instance: "Contract".to_owned().into(), instance: "Contract".to_owned().into(),
method: Method::FunctionName("send(address)".to_owned()), method: Method::FunctionName("send(address)".to_owned()),
calldata: Calldata::Compound(vec![ calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
"0x1000000000000000000000000000000000000001".to_string(),
]),
..Default::default() ..Default::default()
}; };
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new_from("Contract"), ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi), (Address::ZERO, parsed_abi),
); );
let encoded = input.encoded_input(&contracts, &DummyEthereumNode).unwrap(); let encoded = input.encoded_input(&contracts, &MockResolver).unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy_primitives::Address,);
@@ -519,21 +774,19 @@ mod tests {
.0; .0;
let input: Input = Input { let input: Input = Input {
instance: ContractInstance::new_from("Contract"), instance: ContractInstance::new("Contract"),
method: Method::FunctionName("send".to_owned()), method: Method::FunctionName("send".to_owned()),
calldata: Calldata::Compound(vec![ calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
"0x1000000000000000000000000000000000000001".to_string(),
]),
..Default::default() ..Default::default()
}; };
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new_from("Contract"), ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi), (Address::ZERO, parsed_abi),
); );
let encoded = input.encoded_input(&contracts, &DummyEthereumNode).unwrap(); let encoded = input.encoded_input(&contracts, &MockResolver).unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy_primitives::Address,);
@@ -544,17 +797,25 @@ mod tests {
); );
} }
fn resolve_calldata_item(
input: &str,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<U256> {
CalldataItem::new(input).resolve(deployed_contracts, chain_state_provider)
}
#[test] #[test]
fn resolver_can_resolve_chain_id_variable() { fn resolver_can_resolve_chain_id_variable() {
// Arrange // Arrange
let input = "$CHAIN_ID"; let input = "$CHAIN_ID";
// Act // Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert // Assert
let resolved = resolved.expect("Failed to resolve argument"); let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(DummyEthereumNode.chain_id().unwrap())) assert_eq!(resolved, U256::from(MockResolver.chain_id().unwrap()))
} }
#[test] #[test]
@@ -563,17 +824,13 @@ mod tests {
let input = "$GAS_LIMIT"; let input = "$GAS_LIMIT";
// Act // Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert // Assert
let resolved = resolved.expect("Failed to resolve argument"); let resolved = resolved.expect("Failed to resolve argument");
assert_eq!( assert_eq!(
resolved, resolved,
U256::from( U256::from(MockResolver.block_gas_limit(Default::default()).unwrap())
DummyEthereumNode
.block_gas_limit(Default::default())
.unwrap()
)
) )
} }
@@ -583,14 +840,14 @@ mod tests {
let input = "$COINBASE"; let input = "$COINBASE";
// Act // Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert // Assert
let resolved = resolved.expect("Failed to resolve argument"); let resolved = resolved.expect("Failed to resolve argument");
assert_eq!( assert_eq!(
resolved, resolved,
U256::from_be_slice( U256::from_be_slice(
DummyEthereumNode MockResolver
.block_coinbase(Default::default()) .block_coinbase(Default::default())
.unwrap() .unwrap()
.as_ref() .as_ref()
@@ -604,15 +861,13 @@ mod tests {
let input = "$DIFFICULTY"; let input = "$DIFFICULTY";
// Act // Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert // Assert
let resolved = resolved.expect("Failed to resolve argument"); let resolved = resolved.expect("Failed to resolve argument");
assert_eq!( assert_eq!(
resolved, resolved,
DummyEthereumNode MockResolver.block_difficulty(Default::default()).unwrap()
.block_difficulty(Default::default())
.unwrap()
) )
} }
@@ -622,13 +877,13 @@ mod tests {
let input = "$BLOCK_HASH"; let input = "$BLOCK_HASH";
// Act // Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert // Assert
let resolved = resolved.expect("Failed to resolve argument"); let resolved = resolved.expect("Failed to resolve argument");
assert_eq!( assert_eq!(
resolved, resolved,
U256::from_be_bytes(DummyEthereumNode.block_hash(Default::default()).unwrap().0) U256::from_be_bytes(MockResolver.block_hash(Default::default()).unwrap().0)
) )
} }
@@ -638,13 +893,13 @@ mod tests {
let input = "$BLOCK_NUMBER"; let input = "$BLOCK_NUMBER";
// Act // Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert // Assert
let resolved = resolved.expect("Failed to resolve argument"); let resolved = resolved.expect("Failed to resolve argument");
assert_eq!( assert_eq!(
resolved, resolved,
U256::from(DummyEthereumNode.last_block_number().unwrap()) U256::from(MockResolver.last_block_number().unwrap())
) )
} }
@@ -654,17 +909,105 @@ mod tests {
let input = "$BLOCK_TIMESTAMP"; let input = "$BLOCK_TIMESTAMP";
// Act // Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert // Assert
let resolved = resolved.expect("Failed to resolve argument"); let resolved = resolved.expect("Failed to resolve argument");
assert_eq!( assert_eq!(
resolved, resolved,
U256::from( U256::from(MockResolver.block_timestamp(Default::default()).unwrap())
DummyEthereumNode
.block_timestamp(Default::default())
.unwrap()
)
) )
} }
#[test]
fn simple_addition_can_be_resolved() {
// Arrange
let input = "2 4 +";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(6));
}
#[test]
fn simple_subtraction_can_be_resolved() {
// Arrange
let input = "4 2 -";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(2));
}
#[test]
fn simple_multiplication_can_be_resolved() {
// Arrange
let input = "4 2 *";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(8));
}
#[test]
fn simple_division_can_be_resolved() {
// Arrange
let input = "4 2 /";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(2));
}
#[test]
fn arithmetic_errors_are_not_panics() {
// Arrange
let input = "4 0 /";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
assert!(resolved.is_err())
}
#[test]
fn arithmetic_with_resolution_works() {
// Arrange
let input = "$BLOCK_NUMBER 10 +";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(MockResolver.last_block_number().unwrap() + 10)
);
}
#[test]
fn incorrect_number_of_arguments_errors() {
// Arrange
let input = "$BLOCK_NUMBER 10 + +";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
assert!(resolved.is_err())
}
} }
+1 -1
View File
@@ -3,6 +3,6 @@
pub mod case; pub mod case;
pub mod corpus; pub mod corpus;
pub mod input; pub mod input;
pub mod macros;
pub mod metadata; pub mod metadata;
pub mod mode; pub mod mode;
pub mod traits;
+26 -20
View File
@@ -9,9 +9,10 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type;
use crate::{ use crate::{
case::Case, case::Case,
define_wrapper_type,
mode::{Mode, SolcMode}, mode::{Mode, SolcMode},
}; };
@@ -44,10 +45,11 @@ impl Deref for MetadataFile {
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
pub struct Metadata { pub struct Metadata {
pub targets: Option<Vec<String>>,
pub cases: Vec<Case>, pub cases: Vec<Case>,
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdentifier>>, pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
// TODO: Convert into wrapper types for clarity. // TODO: Convert into wrapper types for clarity.
pub libraries: Option<BTreeMap<String, BTreeMap<String, String>>>, pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
pub ignore: Option<bool>, pub ignore: Option<bool>,
pub modes: Option<Vec<Mode>>, pub modes: Option<Vec<Mode>>,
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,
@@ -84,7 +86,7 @@ impl Metadata {
/// Returns the contract sources with canonicalized paths for the files /// Returns the contract sources with canonicalized paths for the files
pub fn contract_sources( pub fn contract_sources(
&self, &self,
) -> anyhow::Result<BTreeMap<ContractInstance, ContractPathAndIdentifier>> { ) -> anyhow::Result<BTreeMap<ContractInstance, ContractPathAndIdent>> {
let directory = self.directory()?; let directory = self.directory()?;
let mut sources = BTreeMap::new(); let mut sources = BTreeMap::new();
let Some(contracts) = &self.contracts else { let Some(contracts) = &self.contracts else {
@@ -93,7 +95,7 @@ impl Metadata {
for ( for (
alias, alias,
ContractPathAndIdentifier { ContractPathAndIdent {
contract_source_path, contract_source_path,
contract_ident, contract_ident,
}, },
@@ -105,7 +107,7 @@ impl Metadata {
sources.insert( sources.insert(
alias, alias,
ContractPathAndIdentifier { ContractPathAndIdent {
contract_source_path: absolute_path, contract_source_path: absolute_path,
contract_ident, contract_ident,
}, },
@@ -191,10 +193,10 @@ impl Metadata {
metadata.file_path = Some(path.to_path_buf()); metadata.file_path = Some(path.to_path_buf());
metadata.contracts = Some( metadata.contracts = Some(
[( [(
ContractInstance::new_from("test"), ContractInstance::new("test"),
ContractPathAndIdentifier { ContractPathAndIdent {
contract_source_path: path.to_path_buf(), contract_source_path: path.to_path_buf(),
contract_ident: ContractIdent::new_from("Test"), contract_ident: ContractIdent::new("Test"),
}, },
)] )]
.into(), .into(),
@@ -216,18 +218,22 @@ define_wrapper_type!(
/// Represents a contract instance found a metadata file. /// Represents a contract instance found a metadata file.
/// ///
/// Typically, this is used as the key to the "contracts" field of metadata files. /// Typically, this is used as the key to the "contracts" field of metadata files.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)] #[serde(transparent)]
ContractInstance(String); pub struct ContractInstance(String);
); );
define_wrapper_type!( define_wrapper_type!(
/// Represents a contract identifier found a metadata file. /// Represents a contract identifier found a metadata file.
/// ///
/// A contract identifier is the name of the contract in the source code. /// A contract identifier is the name of the contract in the source code.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)] #[serde(transparent)]
ContractIdent(String); pub struct ContractIdent(String);
); );
/// Represents an identifier used for contracts. /// Represents an identifier used for contracts.
@@ -239,7 +245,7 @@ define_wrapper_type!(
/// ``` /// ```
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
pub struct ContractPathAndIdentifier { pub struct ContractPathAndIdent {
/// The path of the contract source code relative to the directory containing the metadata file. /// The path of the contract source code relative to the directory containing the metadata file.
pub contract_source_path: PathBuf, pub contract_source_path: PathBuf,
@@ -247,7 +253,7 @@ pub struct ContractPathAndIdentifier {
pub contract_ident: ContractIdent, pub contract_ident: ContractIdent,
} }
impl Display for ContractPathAndIdentifier { impl Display for ContractPathAndIdent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
@@ -258,7 +264,7 @@ impl Display for ContractPathAndIdentifier {
} }
} }
impl FromStr for ContractPathAndIdentifier { impl FromStr for ContractPathAndIdent {
type Err = anyhow::Error; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -294,7 +300,7 @@ impl FromStr for ContractPathAndIdentifier {
} }
} }
impl TryFrom<String> for ContractPathAndIdentifier { impl TryFrom<String> for ContractPathAndIdent {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> { fn try_from(value: String) -> Result<Self, Self::Error> {
@@ -302,8 +308,8 @@ impl TryFrom<String> for ContractPathAndIdentifier {
} }
} }
impl From<ContractPathAndIdentifier> for String { impl From<ContractPathAndIdent> for String {
fn from(value: ContractPathAndIdentifier) -> Self { fn from(value: ContractPathAndIdent) -> Self {
value.to_string() value.to_string()
} }
} }
@@ -318,7 +324,7 @@ mod test {
let string = "ERC20/ERC20.sol:ERC20"; let string = "ERC20/ERC20.sol:ERC20";
// Act // Act
let identifier = ContractPathAndIdentifier::from_str(string); let identifier = ContractPathAndIdent::from_str(string);
// Assert // Assert
let identifier = identifier.expect("Failed to parse"); let identifier = identifier.expect("Failed to parse");
+30
View File
@@ -0,0 +1,30 @@
use alloy::eips::BlockNumberOrTag;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use anyhow::Result;
/// A trait of the interface are required to implement to be used by the resolution logic that this
/// crate implements to go from string calldata and into the bytes calldata.
pub trait ResolverApi {
/// Returns the ID of the chain that the node is on.
fn chain_id(&self) -> Result<ChainId>;
// TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit
// when we implement the changes to the gas we need to adjust this to be a u64.
/// Returns the gas limit of the specified block.
fn block_gas_limit(&self, number: BlockNumberOrTag) -> Result<u128>;
/// Returns the coinbase of the specified block.
fn block_coinbase(&self, number: BlockNumberOrTag) -> Result<Address>;
/// Returns the difficulty of the specified block.
fn block_difficulty(&self, number: BlockNumberOrTag) -> Result<U256>;
/// Returns the hash of the specified block.
fn block_hash(&self, number: BlockNumberOrTag) -> Result<BlockHash>;
/// Returns the timestamp of the specified block,
fn block_timestamp(&self, number: BlockNumberOrTag) -> Result<BlockTimestamp>;
/// Returns the number of the last block.
fn last_block_number(&self) -> Result<BlockNumber>;
}
-4
View File
@@ -11,7 +11,3 @@ rust-version.workspace = true
[dependencies] [dependencies]
alloy = { workspace = true } alloy = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true }
+7 -34
View File
@@ -1,48 +1,21 @@
//! This crate implements all node interactions. //! This crate implements all node interactions.
use alloy::eips::BlockNumberOrTag; use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethTrace};
use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; use alloy::rpc::types::{TransactionReceipt, TransactionRequest};
use anyhow::Result; use anyhow::Result;
mod blocking_executor;
pub use blocking_executor::*;
/// An interface for all interactions with Ethereum compatible nodes. /// An interface for all interactions with Ethereum compatible nodes.
pub trait EthereumNode { pub trait EthereumNode {
/// Execute the [TransactionRequest] and return a [TransactionReceipt]. /// Execute the [TransactionRequest] and return a [TransactionReceipt].
fn execute_transaction(&self, transaction: TransactionRequest) -> Result<TransactionReceipt>; fn execute_transaction(&self, transaction: TransactionRequest) -> Result<TransactionReceipt>;
/// Trace the transaction in the [TransactionReceipt] and return a [GethTrace]. /// Trace the transaction in the [TransactionReceipt] and return a [GethTrace].
fn trace_transaction(&self, transaction: TransactionReceipt) -> Result<GethTrace>; fn trace_transaction(
&self,
receipt: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> Result<GethTrace>;
/// Returns the state diff of the transaction hash in the [TransactionReceipt]. /// Returns the state diff of the transaction hash in the [TransactionReceipt].
fn state_diff(&self, transaction: TransactionReceipt) -> Result<DiffMode>; fn state_diff(&self, receipt: &TransactionReceipt) -> Result<DiffMode>;
/// Returns the next available nonce for the given [Address].
fn fetch_add_nonce(&self, address: Address) -> Result<u64>;
/// Returns the ID of the chain that the node is on.
fn chain_id(&self) -> Result<ChainId>;
// TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit
// when we implement the changes to the gas we need to adjust this to be a u64.
/// Returns the gas limit of the specified block.
fn block_gas_limit(&self, number: BlockNumberOrTag) -> Result<u128>;
/// Returns the coinbase of the specified block.
fn block_coinbase(&self, number: BlockNumberOrTag) -> Result<Address>;
/// Returns the difficulty of the specified block.
fn block_difficulty(&self, number: BlockNumberOrTag) -> Result<U256>;
/// Returns the hash of the specified block.
fn block_hash(&self, number: BlockNumberOrTag) -> Result<BlockHash>;
/// Returns the timestamp of the specified block,
fn block_timestamp(&self, number: BlockNumberOrTag) -> Result<BlockTimestamp>;
/// Returns the number of the last block.
fn last_block_number(&self) -> Result<BlockNumber>;
} }
+3 -1
View File
@@ -14,8 +14,10 @@ alloy = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
revive-dt-node-interaction = { workspace = true } revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true } revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
revive-dt-node-interaction = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
+78
View File
@@ -0,0 +1,78 @@
use alloy::{
network::{Network, TransactionBuilder},
providers::{
Provider, SendableTx,
fillers::{GasFiller, TxFiller},
},
transports::TransportResult,
};
#[derive(Clone, Debug)]
pub struct FallbackGasFiller {
inner: GasFiller,
default_gas_limit: u64,
default_max_fee_per_gas: u128,
default_priority_fee: u128,
}
impl FallbackGasFiller {
pub fn new(
default_gas_limit: u64,
default_max_fee_per_gas: u128,
default_priority_fee: u128,
) -> Self {
Self {
inner: GasFiller,
default_gas_limit,
default_max_fee_per_gas,
default_priority_fee,
}
}
}
impl<N> TxFiller<N> for FallbackGasFiller
where
N: Network,
{
type Fillable = Option<<GasFiller as TxFiller<N>>::Fillable>;
fn status(
&self,
tx: &<N as Network>::TransactionRequest,
) -> alloy::providers::fillers::FillerControlFlow {
<GasFiller as TxFiller<N>>::status(&self.inner, tx)
}
fn fill_sync(&self, _: &mut alloy::providers::SendableTx<N>) {}
async fn prepare<P: Provider<N>>(
&self,
provider: &P,
tx: &<N as Network>::TransactionRequest,
) -> TransportResult<Self::Fillable> {
// Try to fetch GasFillers “fillable” (gas_price, base_fee, estimate_gas, …)
// If it errors (i.e. tx would revert under eth_estimateGas), swallow it.
match self.inner.prepare(provider, tx).await {
Ok(fill) => Ok(Some(fill)),
Err(_) => Ok(None),
}
}
async fn fill(
&self,
fillable: Self::Fillable,
mut tx: alloy::providers::SendableTx<N>,
) -> TransportResult<SendableTx<N>> {
if let Some(fill) = fillable {
// our inner GasFiller succeeded — use it
self.inner.fill(fill, tx).await
} else {
if let Some(builder) = tx.as_mut_builder() {
builder.set_gas_limit(self.default_gas_limit);
builder.set_max_fee_per_gas(self.default_max_fee_per_gas);
builder.set_max_priority_fee_per_gas(self.default_priority_fee);
}
Ok(tx)
}
}
}
+5
View File
@@ -0,0 +1,5 @@
/// This constant defines how much Wei accounts are pre-seeded with in genesis.
///
/// Note: After changing this number, check that the tests for kitchensink work as we encountered
/// some issues with different values of the initial balance on Kitchensink.
pub const INITIAL_BALANCE: u128 = 10u128.pow(37);
+98 -86
View File
@@ -1,37 +1,37 @@
//! The go-ethereum node implementation. //! The go-ethereum node implementation.
use std::{ use std::{
collections::HashMap,
fs::{File, OpenOptions, create_dir_all, remove_dir_all}, fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, BufReader, Read, Write}, io::{BufRead, BufReader, Read, Write},
path::PathBuf, path::PathBuf,
process::{Child, Command, Stdio}, process::{Child, Command, Stdio},
sync::{ sync::atomic::{AtomicU32, Ordering},
Mutex,
atomic::{AtomicU32, Ordering},
},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use alloy::{ use alloy::{
eips::BlockNumberOrTag, eips::BlockNumberOrTag,
network::{Ethereum, EthereumWallet}, genesis::{Genesis, GenesisAccount},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, U256}, network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, U256},
providers::{ providers::{
Provider, ProviderBuilder, Provider, ProviderBuilder,
ext::DebugApi, ext::DebugApi,
fillers::{FillProvider, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
}, },
rpc::types::{ rpc::types::{
TransactionReceipt, TransactionRequest, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
}, },
signers::local::PrivateKeySigner,
}; };
use revive_dt_common::concepts::BlockingExecutor;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode}; use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode;
use tracing::Level; use tracing::Level;
use crate::Node; use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0); static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -54,7 +54,7 @@ pub struct Instance {
network_id: u64, network_id: u64,
start_timeout: u64, start_timeout: u64,
wallet: EthereumWallet, wallet: EthereumWallet,
nonces: Mutex<HashMap<Address, u64>>, nonce_manager: CachedNonceManager,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the /// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in /// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of /// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -76,14 +76,27 @@ impl Instance {
const GETH_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log"; const GETH_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log";
const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log"; const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
/// Create the node directory and call `geth init` to configure the genesis. /// Create the node directory and call `geth init` to configure the genesis.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> { fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
create_dir_all(&self.base_directory)?; create_dir_all(&self.base_directory)?;
create_dir_all(&self.logs_directory)?; create_dir_all(&self.logs_directory)?;
let mut genesis = serde_json::from_str::<Genesis>(&genesis)?;
for signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
// Note, the use of the entry API here means that we only modify the entries for any
// account that is not in the `alloc` field of the genesis state.
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
}
let genesis_path = self.base_directory.join(Self::GENESIS_JSON_FILE); let genesis_path = self.base_directory.join(Self::GENESIS_JSON_FILE);
File::create(&genesis_path)?.write_all(genesis.as_bytes())?; serde_json::to_writer(File::create(&genesis_path)?, &genesis)?;
let mut child = Command::new(&self.geth) let mut child = Command::new(&self.geth)
.arg("init") .arg("init")
@@ -206,8 +219,19 @@ impl Instance {
> + 'static { > + 'static {
let connection_string = self.connection_string(); let connection_string = self.connection_string();
let wallet = self.wallet.clone(); let wallet = self.wallet.clone();
// Note: We would like all providers to make use of the same nonce manager so that we have
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move { Box::pin(async move {
ProviderBuilder::new() ProviderBuilder::new()
.disable_recommended_fillers()
.filler(FallbackGasFiller::new(500_000_000, 500_000_000, 1))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
.wallet(wallet) .wallet(wallet)
.connect(&connection_string) .connect(&connection_string)
.await .await
@@ -224,7 +248,7 @@ impl EthereumNode for Instance {
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> { ) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
let provider = self.provider(); let provider = self.provider();
BlockingExecutor::execute(async move { BlockingExecutor::execute(async move {
let outer_span = tracing::debug_span!("Submitting transaction", ?transaction,); let outer_span = tracing::debug_span!("Submitting transaction", ?transaction);
let _outer_guard = outer_span.enter(); let _outer_guard = outer_span.enter();
let provider = provider.await?; let provider = provider.await?;
@@ -247,57 +271,45 @@ impl EthereumNode for Instance {
// it eventually works, but we only do that if the error we get back is the "transaction // it eventually works, but we only do that if the error we get back is the "transaction
// indexing is in progress" error or if the receipt is None. // indexing is in progress" error or if the receipt is None.
// //
// At the moment we do not allow for the 60 seconds to be modified and we take it as // Getting the transaction indexed and taking a receipt can take a long time especially
// being an implementation detail that's invisible to anything outside of this module. // when a lot of transactions are being submitted to the node. Thus, while initially we
// // only allowed for 60 seconds of waiting with a 1 second delay in polling, we need to
// We allow a total of 60 retries for getting the receipt with one second between each // allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting
// retry and the next which means that we allow for a total of 60 seconds of waiting // with exponential backoff each time we attempt to get the receipt and find that it's
// before we consider that we're unable to get the transaction receipt. // not available.
let mut retries = 0; let mut retries = 0;
let mut total_wait_duration = Duration::from_secs(0);
let max_allowed_wait_duration = Duration::from_secs(5 * 60);
loop { loop {
match provider.get_transaction_receipt(*transaction_hash).await { if total_wait_duration >= max_allowed_wait_duration {
Ok(Some(receipt)) => {
tracing::info!("Obtained the transaction receipt");
break Ok(receipt);
}
Ok(None) => {
if retries == 60 {
tracing::error!( tracing::error!(
"Polled for transaction receipt for 60 seconds but failed to get it" ?total_wait_duration,
?max_allowed_wait_duration,
retry_count = retries,
"Failed to get receipt after polling for it"
); );
break Err(anyhow::anyhow!("Failed to get the transaction receipt")); anyhow::bail!(
} else { "Polled for receipt for {total_wait_duration:?} but failed to get it"
tracing::trace!(
retries,
"Sleeping for 1 second and trying to get the receipt again"
); );
retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
} }
match provider.get_transaction_receipt(*transaction_hash).await {
Ok(Some(receipt)) => break Ok(receipt),
Ok(None) => {}
Err(error) => { Err(error) => {
let error_string = error.to_string(); let error_string = error.to_string();
if error_string.contains("transaction indexing is in progress") { if !error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
if retries == 60 {
tracing::error!(
"Polled for transaction receipt for 60 seconds but failed to get it"
);
break Err(error.into()); break Err(error.into());
} else { }
tracing::trace!( }
retries, };
"Sleeping for 1 second and trying to get the receipt again"
); let next_wait_duration = Duration::from_secs(2u64.pow(retries))
.min(max_allowed_wait_duration - total_wait_duration);
total_wait_duration += next_wait_duration;
retries += 1; retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue; tokio::time::sleep(next_wait_duration).await;
}
} else {
break Err(error.into());
}
}
}
} }
})? })?
} }
@@ -305,55 +317,37 @@ impl EthereumNode for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn trace_transaction( fn trace_transaction(
&self, &self,
transaction: TransactionReceipt, transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> { ) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let tx_hash = transaction.transaction_hash;
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let provider = self.provider(); let provider = self.provider();
BlockingExecutor::execute(async move { BlockingExecutor::execute(async move {
Ok(provider Ok(provider
.await? .await?
.debug_trace_transaction(transaction.transaction_hash, trace_options) .debug_trace_transaction(tx_hash, trace_options)
.await?) .await?)
})? })?
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn state_diff( fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
&self, let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
transaction: alloy::rpc::types::TransactionReceipt, diff_mode: Some(true),
) -> anyhow::Result<DiffMode> { disable_code: None,
disable_storage: None,
});
match self match self
.trace_transaction(transaction)? .trace_transaction(transaction, trace_options)?
.try_into_pre_state_frame()? .try_into_pre_state_frame()?
{ {
PreStateFrame::Diff(diff) => Ok(diff), PreStateFrame::Diff(diff) => Ok(diff),
_ => anyhow::bail!("expected a diff mode trace"), _ => anyhow::bail!("expected a diff mode trace"),
} }
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let provider = self.provider();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
provider
.await?
.get_transaction_count(address)
.await
.map_err(Into::into)
})??;
let mut nonces = self.nonces.lock().unwrap();
let current = nonces.entry(address).or_insert(onchain_nonce);
let value = *current;
*current += 1;
Ok(value)
} }
impl ResolverApi for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> { fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider(); let provider = self.provider();
@@ -442,6 +436,15 @@ impl Node for Instance {
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst); let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
let base_directory = geth_directory.join(id.to_string()); let base_directory = geth_directory.join(id.to_string());
let mut wallet = config.wallet();
for signer in (1..=config.private_keys_to_add)
.map(|id| U256::from(id))
.map(|id| id.to_be_bytes::<32>())
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
{
wallet.register_signer(signer);
}
Self { Self {
connection_string: base_directory.join(Self::IPC_FILE).display().to_string(), connection_string: base_directory.join(Self::IPC_FILE).display().to_string(),
data_directory: base_directory.join(Self::DATA_DIRECTORY), data_directory: base_directory.join(Self::DATA_DIRECTORY),
@@ -452,11 +455,11 @@ impl Node for Instance {
handle: None, handle: None,
network_id: config.network_id, network_id: config.network_id,
start_timeout: config.geth_start_timeout, start_timeout: config.geth_start_timeout,
wallet: config.wallet(), wallet,
nonces: Mutex::new(HashMap::new()),
// We know that we only need to be storing 2 files so we can specify that when creating // We know that we only need to be storing 2 files so we can specify that when creating
// the vector. It's the stdout and stderr of the geth node. // the vector. It's the stdout and stderr of the geth node.
logs_file_to_flush: Vec::with_capacity(2), logs_file_to_flush: Vec::with_capacity(2),
nonce_manager: Default::default(),
} }
} }
@@ -505,6 +508,14 @@ impl Node for Instance {
.stdout; .stdout;
Ok(String::from_utf8_lossy(&output).into()) Ok(String::from_utf8_lossy(&output).into())
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets {
None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "evm"),
}
}
} }
impl Drop for Instance { impl Drop for Instance {
@@ -517,6 +528,7 @@ impl Drop for Instance {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use temp_dir::TempDir; use temp_dir::TempDir;
use crate::{GENESIS_JSON, Node}; use crate::{GENESIS_JSON, Node};
+103 -81
View File
@@ -1,46 +1,47 @@
use std::{ use std::{
collections::HashMap,
fs::{File, OpenOptions, create_dir_all, remove_dir_all}, fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, Write}, io::{BufRead, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Child, Command, Stdio}, process::{Child, Command, Stdio},
sync::{ sync::atomic::{AtomicU32, Ordering},
Mutex,
atomic::{AtomicU32, Ordering},
},
time::Duration, time::Duration,
}; };
use alloy::{ use alloy::{
consensus::{BlockHeader, TxEnvelope}, consensus::{BlockHeader, TxEnvelope},
eips::BlockNumberOrTag, eips::BlockNumberOrTag,
hex, genesis::{Genesis, GenesisAccount},
network::{ network::{
Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, Ethereum, EthereumWallet, Network, NetworkWallet, TransactionBuilder,
UnbuiltTransactionError, TransactionBuilderError, UnbuiltTransactionError,
},
primitives::{
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes, U256,
}, },
primitives::{Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, U256},
providers::{ providers::{
Provider, ProviderBuilder, Provider, ProviderBuilder,
ext::DebugApi, ext::DebugApi,
fillers::{FillProvider, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
}, },
rpc::types::{ rpc::types::{
TransactionReceipt, TransactionReceipt,
eth::{Block, Header, Transaction}, eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
}, },
signers::local::PrivateKeySigner,
}; };
use revive_dt_format::traits::ResolverApi;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json}; use serde_json::{Value as JsonValue, json};
use sp_core::crypto::Ss58Codec; use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32; use sp_runtime::AccountId32;
use tracing::Level; use tracing::Level;
use revive_dt_common::concepts::BlockingExecutor;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode}; use revive_dt_node_interaction::EthereumNode;
use crate::Node; use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0); static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -55,7 +56,7 @@ pub struct KitchensinkNode {
logs_directory: PathBuf, logs_directory: PathBuf,
process_substrate: Option<Child>, process_substrate: Option<Child>,
process_proxy: Option<Child>, process_proxy: Option<Child>,
nonces: Mutex<HashMap<Address, u64>>, nonce_manager: CachedNonceManager,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the /// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in /// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of /// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -127,7 +128,20 @@ impl KitchensinkNode {
None None
}) })
.collect(); .collect();
let mut eth_balances = self.extract_balance_from_genesis_file(genesis)?; let mut eth_balances = {
let mut genesis = serde_json::from_str::<Genesis>(genesis)?;
for signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
// Note, the use of the entry API here means that we only modify the entries for any
// account that is not in the `alloc` field of the genesis state.
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
}
self.extract_balance_from_genesis_file(&genesis)?
};
merged_balances.append(&mut eth_balances); merged_balances.append(&mut eth_balances);
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] = chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] =
@@ -241,42 +255,27 @@ impl KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn extract_balance_from_genesis_file( fn extract_balance_from_genesis_file(
&self, &self,
genesis_str: &str, genesis: &Genesis,
) -> anyhow::Result<Vec<(String, u128)>> { ) -> anyhow::Result<Vec<(String, u128)>> {
let genesis_json: JsonValue = serde_json::from_str(genesis_str)?; genesis
let alloc = genesis_json .alloc
.get("alloc") .iter()
.and_then(|a| a.as_object()) .try_fold(Vec::new(), |mut vec, (address, acc)| {
.ok_or_else(|| anyhow::anyhow!("Missing 'alloc' in genesis"))?; let substrate_address = Self::eth_to_substrate_address(address);
let balance = acc.balance.try_into()?;
let mut balances = Vec::new(); vec.push((substrate_address, balance));
for (eth_addr, obj) in alloc.iter() { Ok(vec)
let balance_str = obj.get("balance").and_then(|b| b.as_str()).unwrap_or("0"); })
let balance = if balance_str.starts_with("0x") {
u128::from_str_radix(balance_str.trim_start_matches("0x"), 16)?
} else {
balance_str.parse::<u128>()?
};
let substrate_addr = Self::eth_to_substrate_address(eth_addr)?;
balances.push((substrate_addr.clone(), balance));
}
Ok(balances)
} }
fn eth_to_substrate_address(eth_addr: &str) -> anyhow::Result<String> { fn eth_to_substrate_address(address: &Address) -> String {
let eth_bytes = hex::decode(eth_addr.trim_start_matches("0x"))?; let eth_bytes = address.0.0;
if eth_bytes.len() != 20 {
anyhow::bail!(
"Invalid Ethereum address length: expected 20 bytes, got {}",
eth_bytes.len()
);
}
let mut padded = [0xEEu8; 32]; let mut padded = [0xEEu8; 32];
padded[..20].copy_from_slice(&eth_bytes); padded[..20].copy_from_slice(&eth_bytes);
let account_id = AccountId32::from(padded); let account_id = AccountId32::from(padded);
Ok(account_id.to_ss58check()) account_id.to_ss58check()
} }
fn wait_ready(logs_file_path: &Path, marker: &str, timeout: Duration) -> anyhow::Result<()> { fn wait_ready(logs_file_path: &Path, marker: &str, timeout: Duration) -> anyhow::Result<()> {
@@ -350,9 +349,24 @@ impl KitchensinkNode {
> + 'static { > + 'static {
let connection_string = self.connection_string(); let connection_string = self.connection_string();
let wallet = self.wallet.clone(); let wallet = self.wallet.clone();
// Note: We would like all providers to make use of the same nonce manager so that we have
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move { Box::pin(async move {
ProviderBuilder::new() ProviderBuilder::new()
.disable_recommended_fillers()
.network::<KitchenSinkNetwork>() .network::<KitchenSinkNetwork>()
.filler(FallbackGasFiller::new(
30_000_000,
200_000_000_000,
3_000_000_000,
))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
.wallet(wallet) .wallet(wallet)
.connect(&connection_string) .connect(&connection_string)
.await .await
@@ -384,52 +398,37 @@ impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn trace_transaction( fn trace_transaction(
&self, &self,
transaction: TransactionReceipt, transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> { ) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let tx_hash = transaction.transaction_hash;
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let provider = self.provider(); let provider = self.provider();
BlockingExecutor::execute(async move { BlockingExecutor::execute(async move {
Ok(provider Ok(provider
.await? .await?
.debug_trace_transaction(transaction.transaction_hash, trace_options) .debug_trace_transaction(tx_hash, trace_options)
.await?) .await?)
})? })?
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn state_diff(&self, transaction: TransactionReceipt) -> anyhow::Result<DiffMode> { fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
match self match self
.trace_transaction(transaction)? .trace_transaction(transaction, trace_options)?
.try_into_pre_state_frame()? .try_into_pre_state_frame()?
{ {
PreStateFrame::Diff(diff) => Ok(diff), PreStateFrame::Diff(diff) => Ok(diff),
_ => anyhow::bail!("expected a diff mode trace"), _ => anyhow::bail!("expected a diff mode trace"),
} }
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let provider = self.provider();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
provider
.await?
.get_transaction_count(address)
.await
.map_err(Into::into)
})??;
let mut nonces = self.nonces.lock().unwrap();
let current = nonces.entry(address).or_insert(onchain_nonce);
let value = *current;
*current += 1;
Ok(value)
} }
impl ResolverApi for KitchensinkNode {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> { fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider(); let provider = self.provider();
@@ -519,17 +518,26 @@ impl Node for KitchensinkNode {
let base_directory = kitchensink_directory.join(id.to_string()); let base_directory = kitchensink_directory.join(id.to_string());
let logs_directory = base_directory.join(Self::LOGS_DIRECTORY); let logs_directory = base_directory.join(Self::LOGS_DIRECTORY);
let mut wallet = config.wallet();
for signer in (1..=config.private_keys_to_add)
.map(|id| U256::from(id))
.map(|id| id.to_be_bytes::<32>())
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
{
wallet.register_signer(signer);
}
Self { Self {
id, id,
substrate_binary: config.kitchensink.clone(), substrate_binary: config.kitchensink.clone(),
eth_proxy_binary: config.eth_proxy.clone(), eth_proxy_binary: config.eth_proxy.clone(),
rpc_url: String::new(), rpc_url: String::new(),
wallet: config.wallet(), wallet,
base_directory, base_directory,
logs_directory, logs_directory,
process_substrate: None, process_substrate: None,
process_proxy: None, process_proxy: None,
nonces: Mutex::new(HashMap::new()), nonce_manager: Default::default(),
// We know that we only need to be storing 4 files so we can specify that when creating // We know that we only need to be storing 4 files so we can specify that when creating
// the vector. It's the stdout and stderr of the substrate-node and the eth-rpc. // the vector. It's the stdout and stderr of the substrate-node and the eth-rpc.
logs_file_to_flush: Vec::with_capacity(4), logs_file_to_flush: Vec::with_capacity(4),
@@ -585,6 +593,14 @@ impl Node for KitchensinkNode {
.stdout; .stdout;
Ok(String::from_utf8_lossy(&output).into()) Ok(String::from_utf8_lossy(&output).into())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets {
None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "pvm"),
}
}
} }
impl Drop for KitchensinkNode { impl Drop for KitchensinkNode {
@@ -640,6 +656,12 @@ impl TransactionBuilder<KitchenSinkNetwork> for <Ethereum as Network>::Transacti
) )
} }
fn take_nonce(&mut self) -> Option<u64> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::take_nonce(
self,
)
}
fn input(&self) -> Option<&alloy::primitives::Bytes> { fn input(&self) -> Option<&alloy::primitives::Bytes> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::input(self) <<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::input(self)
} }
@@ -1020,7 +1042,7 @@ mod tests {
use alloy::rpc::types::TransactionRequest; use alloy::rpc::types::TransactionRequest;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::LazyLock; use std::sync::{LazyLock, Mutex};
use temp_dir::TempDir; use temp_dir::TempDir;
use std::fs; use std::fs;
@@ -1129,12 +1151,12 @@ mod tests {
let contents = fs::read_to_string(&final_chainspec_path).expect("Failed to read chainspec"); let contents = fs::read_to_string(&final_chainspec_path).expect("Failed to read chainspec");
// Validate that the Substrate addresses derived from the Ethereum addresses are in the file // Validate that the Substrate addresses derived from the Ethereum addresses are in the file
let first_eth_addr = let first_eth_addr = KitchensinkNode::eth_to_substrate_address(
KitchensinkNode::eth_to_substrate_address("90F8bf6A479f320ead074411a4B0e7944Ea8c9C1") &"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap(),
.unwrap(); );
let second_eth_addr = let second_eth_addr = KitchensinkNode::eth_to_substrate_address(
KitchensinkNode::eth_to_substrate_address("Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2") &"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap(),
.unwrap(); );
assert!( assert!(
contents.contains(&first_eth_addr), contents.contains(&first_eth_addr),
@@ -1162,7 +1184,7 @@ mod tests {
let node = KitchensinkNode::new(&test_config().0); let node = KitchensinkNode::new(&test_config().0);
let result = node let result = node
.extract_balance_from_genesis_file(genesis_json) .extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap())
.unwrap(); .unwrap();
let result_map: std::collections::HashMap<_, _> = result.into_iter().collect(); let result_map: std::collections::HashMap<_, _> = result.into_iter().collect();
@@ -1192,7 +1214,7 @@ mod tests {
]; ];
for eth_addr in eth_addresses { for eth_addr in eth_addresses {
let ss58 = KitchensinkNode::eth_to_substrate_address(eth_addr).unwrap(); let ss58 = KitchensinkNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
println!("Ethereum: {eth_addr} -> Substrate SS58: {ss58}"); println!("Ethereum: {eth_addr} -> Substrate SS58: {ss58}");
} }
@@ -1220,7 +1242,7 @@ mod tests {
]; ];
for (eth_addr, expected_ss58) in cases { for (eth_addr, expected_ss58) in cases {
let result = KitchensinkNode::eth_to_substrate_address(eth_addr).unwrap(); let result = KitchensinkNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
assert_eq!( assert_eq!(
result, expected_ss58, result, expected_ss58,
"Mismatch for Ethereum address {eth_addr}" "Mismatch for Ethereum address {eth_addr}"
+6
View File
@@ -3,6 +3,8 @@
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
pub mod common;
pub mod constants;
pub mod geth; pub mod geth;
pub mod kitchensink; pub mod kitchensink;
pub mod pool; pub mod pool;
@@ -30,4 +32,8 @@ pub trait Node: EthereumNode {
/// Returns the node version. /// Returns the node version.
fn version(&self) -> anyhow::Result<String>; fn version(&self) -> anyhow::Result<String>;
/// Given a list of targets from the metadata file, this function determines if the metadata
/// file can be ran on this node or not.
fn matches_target(&self, targets: Option<&[String]>) -> bool;
} }
+1 -5
View File
@@ -33,9 +33,5 @@
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "0x00", "timestamp": "0x00",
"alloc": { "alloc": {}
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": {
"balance": "1000000000000000000"
}
}
} }