Compare commits

...

16 Commits

Author SHA1 Message Date
Omar Abdulla f9868617a4 Update the new geth test 2025-10-05 18:00:47 +03:00
Omar Abdulla 99b2618328 Ignore certain tests 2025-10-05 17:52:09 +03:00
Omar Abdulla 329440c05e Update the lighthouse ports 2025-10-05 16:54:41 +03:00
Omar Abdulla bd8ee26297 Cleanups 2025-10-05 16:26:03 +03:00
Omar Abdulla 970917ca43 Update the concurrency limit 2025-10-05 02:41:25 +03:00
Omar Abdulla 9ffc04bf13 Update the concurrency limit 2025-10-04 23:05:56 +03:00
Omar Abdulla 2bb9594857 Implement a global concurrency limit on provider requests 2025-10-04 15:47:56 +03:00
Omar Abdulla fe7eaae984 Fix issues in the benchmarks driver 2025-10-03 01:26:19 +03:00
Omar Abdulla ff368b8444 Remove some debug logging 2025-10-02 19:36:06 +03:00
Omar Abdulla ba90c60c1b Implement the core benchmarking components 2025-10-02 18:42:38 +03:00
Omar Abdulla 8762702560 Wire up the cleaned up driver implementation 2025-09-30 13:39:25 +03:00
Omar Abdulla c3c7203af8 Implement the platform driver 2025-09-29 21:10:37 +03:00
Omar Abdulla ca7d8e2ba4 refactor existing dt infra 2025-09-29 15:29:23 +03:00
Omar Abdulla bd422b633e Fix the lighthouse node tracing issue 2025-09-28 21:39:50 +03:00
Omar Abdulla 331807e2f0 Update the account pre-funding handling 2025-09-28 19:20:42 +03:00
Omar Abdulla ec96410546 Implement a solution for the pre-fund account limit 2025-09-28 16:52:43 +03:00
51 changed files with 4308 additions and 1990 deletions
Generated
+121 -66
View File
@@ -67,9 +67,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "alloy" name = "alloy"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad4eb51e7845257b70c51b38ef8d842d5e5e93196701fcbd757577971a043c6" checksum = "7f6cfe35f100bc496007c9a00f90b88bdf565f1421d4c707c9f07e0717e2aaad"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-contract", "alloy-contract",
@@ -88,6 +88,7 @@ dependencies = [
"alloy-transport-http", "alloy-transport-http",
"alloy-transport-ipc", "alloy-transport-ipc",
"alloy-transport-ws", "alloy-transport-ws",
"alloy-trie",
] ]
[[package]] [[package]]
@@ -103,9 +104,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-consensus" name = "alloy-consensus"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3b746060277f3d7f9c36903bb39b593a741cb7afcb0044164c28f0e9b673f0" checksum = "59094911f05dbff1cf5b29046a00ef26452eccc8d47136d50a47c0cf22f00c85"
dependencies = [ dependencies = [
"alloy-eips", "alloy-eips",
"alloy-primitives", "alloy-primitives",
@@ -122,15 +123,16 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"secp256k1 0.30.0", "secp256k1 0.30.0",
"serde", "serde",
"serde_json",
"serde_with", "serde_with",
"thiserror 2.0.12", "thiserror 2.0.12",
] ]
[[package]] [[package]]
name = "alloy-consensus-any" name = "alloy-consensus-any"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf98679329fa708fa809ea596db6d974da892b068ad45e48ac1956f582edf946" checksum = "903cb8f728107ca27c816546f15be38c688df3c381d7bd1a4a9f215effc1ddb4"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-eips", "alloy-eips",
@@ -142,9 +144,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-contract" name = "alloy-contract"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a10e47f5305ea08c37b1772086c1573e9a0a257227143996841172d37d3831bb" checksum = "03df5cb3b428ac96b386ad64c11d5c6e87a5505682cf1fbd6f8f773e9eda04f6"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-dyn-abi", "alloy-dyn-abi",
@@ -230,9 +232,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-eips" name = "alloy-eips"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f562a81278a3ed83290e68361f2d1c75d018ae3b8589a314faf9303883e18ec9" checksum = "ac7f1c9a1ccc7f3e03c36976455751a6166a4f0d2d2c530c3f87dfe7d0cdc836"
dependencies = [ dependencies = [
"alloy-eip2124", "alloy-eip2124",
"alloy-eip2930", "alloy-eip2930",
@@ -245,14 +247,16 @@ dependencies = [
"derive_more 2.0.1", "derive_more 2.0.1",
"either", "either",
"serde", "serde",
"serde_with",
"sha2 0.10.9", "sha2 0.10.9",
"thiserror 2.0.12",
] ]
[[package]] [[package]]
name = "alloy-genesis" name = "alloy-genesis"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc41384e9ab8c9b2fb387c52774d9d432656a28edcda1c2d4083e96051524518" checksum = "1421f6c9d15e5b86afbfe5865ca84dea3b9f77173a0963c1a2ee4e626320ada9"
dependencies = [ dependencies = [
"alloy-eips", "alloy-eips",
"alloy-primitives", "alloy-primitives",
@@ -276,9 +280,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-json-rpc" name = "alloy-json-rpc"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12c454fcfcd5d26ed3b8cae5933cbee9da5f0b05df19b46d4bd4446d1f082565" checksum = "65f763621707fa09cece30b73ecc607eb43fd7a72451fe3b46f645b905086926"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-sol-types", "alloy-sol-types",
@@ -291,9 +295,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-network" name = "alloy-network"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d6d39eabe5c7b3d8f23ac47b0b683b99faa4359797114636c66e0743103d05" checksum = "2f59a869fa4b4c3a7f08b1c8cb79aec61c29febe6e24a24fe0fcfded8a9b5703"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-consensus-any", "alloy-consensus-any",
@@ -317,9 +321,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-network-primitives" name = "alloy-network-primitives"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3704fa8b7ba9ba3f378d99b3d628c8bc8c2fc431b709947930f154e22a8368b6" checksum = "46e9374c667c95c41177602ebe6f6a2edd455193844f011d973d374b65501b38"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-eips", "alloy-eips",
@@ -358,9 +362,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-provider" name = "alloy-provider"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08800e8cbe70c19e2eb7cf3d7ff4b28bdd9b3933f8e1c8136c7d910617ba03bf" checksum = "77818b7348bd5486491a5297579dbfe5f706a81f8e1f5976393025f1e22a7c7d"
dependencies = [ dependencies = [
"alloy-chains", "alloy-chains",
"alloy-consensus", "alloy-consensus",
@@ -387,7 +391,6 @@ dependencies = [
"either", "either",
"futures", "futures",
"futures-utils-wasm", "futures-utils-wasm",
"http",
"lru", "lru",
"parking_lot", "parking_lot",
"pin-project", "pin-project",
@@ -403,13 +406,14 @@ dependencies = [
[[package]] [[package]]
name = "alloy-pubsub" name = "alloy-pubsub"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae68457a2c2ead6bd7d7acb5bf5f1623324b1962d4f8e7b0250657a3c3ab0a0b" checksum = "249b45103a66c9ad60ad8176b076106d03a2399a37f0ee7b0e03692e6b354cb9"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-primitives", "alloy-primitives",
"alloy-transport", "alloy-transport",
"auto_impl",
"bimap", "bimap",
"futures", "futures",
"parking_lot", "parking_lot",
@@ -446,9 +450,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-client" name = "alloy-rpc-client"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162301b5a57d4d8f000bf30f4dcb82f9f468f3e5e846eeb8598dd39e7886932c" checksum = "2430d5623e428dd012c6c2156ae40b7fe638d6fca255e3244e0fba51fa698e93"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-primitives", "alloy-primitives",
@@ -472,11 +476,12 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types" name = "alloy-rpc-types"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cd8ca94ae7e2b32cc3895d9981f3772aab0b4756aa60e9ed0bcfee50f0e1328" checksum = "e9e131624d08a25cfc40557041e7dc42e1182fa1153e7592d120f769a1edce56"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-rpc-types-debug",
"alloy-rpc-types-eth", "alloy-rpc-types-eth",
"alloy-rpc-types-trace", "alloy-rpc-types-trace",
"alloy-serde", "alloy-serde",
@@ -485,9 +490,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types-any" name = "alloy-rpc-types-any"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076b47e834b367d8618c52dd0a0d6a711ddf66154636df394805300af4923b8a" checksum = "07429a1099cd17227abcddb91b5e38c960aaeb02a6967467f5bb561fbe716ac6"
dependencies = [ dependencies = [
"alloy-consensus-any", "alloy-consensus-any",
"alloy-rpc-types-eth", "alloy-rpc-types-eth",
@@ -496,19 +501,21 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types-debug" name = "alloy-rpc-types-debug"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a2a86ad7b7d718c15e79d0779bd255561b6b22968dc5ed2e7c0fbc43bb55fe" checksum = "aeff305b7d10cc1c888456d023e7bb8a5ea82e9e42b951e37619b88cc1a1486d"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"derive_more 2.0.1",
"serde", "serde",
"serde_with",
] ]
[[package]] [[package]]
name = "alloy-rpc-types-eth" name = "alloy-rpc-types-eth"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c2f847e635ec0be819d06e2ada4bcc4e4204026a83c4bfd78ae8d550e027ae7" checksum = "db46b0901ee16bbb68d986003c66dcb74a12f9d9b3c44f8e85d51974f2458f0f"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-consensus-any", "alloy-consensus-any",
@@ -527,9 +534,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types-trace" name = "alloy-rpc-types-trace"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fc58180302a94c934d455eeedb3ecb99cdc93da1dbddcdbbdb79dd6fe618b2a" checksum = "36f10620724bd45f80c79668a8cdbacb6974f860686998abce28f6196ae79444"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-rpc-types-eth", "alloy-rpc-types-eth",
@@ -541,9 +548,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-serde" name = "alloy-serde"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae699248d02ade9db493bbdae61822277dc14ae0f82a5a4153203b60e34422a6" checksum = "5413814be7a22fbc81e0f04a2401fcc3eb25e56fd53b04683e8acecc6e1fe01b"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"serde", "serde",
@@ -552,9 +559,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-signer" name = "alloy-signer"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf7d793c813515e2b627b19a15693960b3ed06670f9f66759396d06ebe5747b" checksum = "53410a18a61916e2c073a6519499514e027b01e77eeaf96acd1df7cf96ef6bb2"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"async-trait", "async-trait",
@@ -567,9 +574,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-signer-local" name = "alloy-signer-local"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51a424bc5a11df0d898ce0fd15906b88ebe2a6e4f17a514b51bc93946bb756bd" checksum = "e6006c4cbfa5d08cadec1fcabea6cb56dc585a30a9fce40bcf81e307d6a71c8e"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-network", "alloy-network",
@@ -656,12 +663,13 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport" name = "alloy-transport"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f317d20f047b3de4d9728c556e2e9a92c9a507702d2016424cd8be13a74ca5e" checksum = "d94ee404368a3d9910dfe61b203e888c6b0e151a50e147f95da8baff9f9c7763"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-primitives", "alloy-primitives",
"auto_impl",
"base64 0.22.1", "base64 0.22.1",
"derive_more 2.0.1", "derive_more 2.0.1",
"futures", "futures",
@@ -679,9 +687,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport-http" name = "alloy-transport-http"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff084ac7b1f318c87b579d221f11b748341d68b9ddaa4ffca5e62ed2b8cfefb4" checksum = "a2f8a6338d594f6c6481292215ee8f2fd7b986c80aba23f3f44e761a8658de78"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-transport", "alloy-transport",
@@ -694,9 +702,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport-ipc" name = "alloy-transport-ipc"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb099cdad8ed2e6a80811cdf9bbf715ebf4e34c981b4a6e2d1f9daacbf8b218" checksum = "17a37a8ca18006fa0a58c7489645619ff58cfa073f2b29c4e052c9bd114b123a"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-pubsub", "alloy-pubsub",
@@ -714,9 +722,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport-ws" name = "alloy-transport-ws"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e915e1250dc129ad48d264573ccd08e4716fdda564a772fd217875b8459aff9" checksum = "679b0122b7bca9d4dc5eb2c0549677a3c53153f6e232f23f4b3ba5575f74ebde"
dependencies = [ dependencies = [
"alloy-pubsub", "alloy-pubsub",
"alloy-transport", "alloy-transport",
@@ -748,12 +756,12 @@ dependencies = [
[[package]] [[package]]
name = "alloy-tx-macros" name = "alloy-tx-macros"
version = "1.0.22" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1154c8187a5ff985c95a8b2daa2fedcf778b17d7668e5e50e556c4ff9c881154" checksum = "e64c09ec565a90ed8390d82aa08cd3b22e492321b96cb4a3d4f58414683c9e2f"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"darling", "darling 0.21.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.101", "syn 2.0.101",
@@ -2010,8 +2018,18 @@ version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.11",
"darling_macro", "darling_macro 0.20.11",
]
[[package]]
name = "darling"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core 0.21.3",
"darling_macro 0.21.3",
] ]
[[package]] [[package]]
@@ -2028,13 +2046,39 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "darling_core"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"serde",
"strsim",
"syn 2.0.101",
]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.20.11" version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.11",
"quote",
"syn 2.0.101",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core 0.21.3",
"quote", "quote",
"syn 2.0.101", "syn 2.0.101",
] ]
@@ -4516,7 +4560,6 @@ name = "revive-dt-common"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"alloy-primitives",
"anyhow", "anyhow",
"clap", "clap",
"moka", "moka",
@@ -4533,7 +4576,6 @@ name = "revive-dt-compiler"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"alloy-primitives",
"anyhow", "anyhow",
"dashmap", "dashmap",
"foundry-compilers-artifacts", "foundry-compilers-artifacts",
@@ -4597,8 +4639,6 @@ name = "revive-dt-format"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"alloy-primitives",
"alloy-sol-types",
"anyhow", "anyhow",
"futures", "futures",
"regex", "regex",
@@ -4618,6 +4658,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"anyhow", "anyhow",
"futures",
"revive-common", "revive-common",
"revive-dt-common", "revive-dt-common",
"revive-dt-config", "revive-dt-config",
@@ -4631,6 +4672,7 @@ dependencies = [
"sp-runtime", "sp-runtime",
"temp-dir", "temp-dir",
"tokio", "tokio",
"tower",
"tracing", "tracing",
] ]
@@ -4640,6 +4682,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"anyhow", "anyhow",
"futures",
"revive-common", "revive-common",
"revive-dt-format", "revive-dt-format",
] ]
@@ -4648,7 +4691,7 @@ dependencies = [
name = "revive-dt-report" name = "revive-dt-report"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alloy-primitives", "alloy",
"anyhow", "anyhow",
"indexmap 2.10.0", "indexmap 2.10.0",
"paste", "paste",
@@ -5114,10 +5157,11 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core",
"serde_derive", "serde_derive",
] ]
@@ -5131,10 +5175,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_core"
version = "1.0.219" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5222,7 +5275,7 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [ dependencies = [
"darling", "darling 0.20.11",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.101", "syn 2.0.101",
@@ -6218,8 +6271,10 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-util",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
+5 -4
View File
@@ -22,8 +22,6 @@ revive-dt-node-pool = { version = "0.1.0", path = "crates/node-pool" }
revive-dt-report = { version = "0.1.0", path = "crates/report" } revive-dt-report = { version = "0.1.0", path = "crates/report" }
revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" } revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
alloy-primitives = "1.2.1"
alloy-sol-types = "1.2.1"
anyhow = "1.0" anyhow = "1.0"
bson = { version = "2.15.0" } bson = { version = "2.15.0" }
cacache = { version = "13.1.0" } cacache = { version = "13.1.0" }
@@ -59,6 +57,7 @@ tokio = { version = "1.47.0", default-features = false, features = [
"process", "process",
"rt", "rt",
] } ] }
tower = { version = "0.5.2", features = ["limit"] }
uuid = { version = "1.8", features = ["v4"] } uuid = { version = "1.8", features = ["v4"] }
tracing = { version = "0.1.41" } tracing = { version = "0.1.41" }
tracing-appender = { version = "0.2.3" } tracing-appender = { version = "0.2.3" }
@@ -75,13 +74,14 @@ 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.22" version = "1.0.37"
default-features = false default-features = false
features = [ features = [
"json-abi", "json-abi",
"providers", "providers",
"provider-ipc",
"provider-ws", "provider-ws",
"provider-ipc",
"provider-http",
"provider-debug-api", "provider-debug-api",
"reqwest", "reqwest",
"rpc-types", "rpc-types",
@@ -91,6 +91,7 @@ features = [
"serde", "serde",
"rpc-types-eth", "rpc-types-eth",
"genesis", "genesis",
"sol-types",
] ]
[profile.bench] [profile.bench]
-1
View File
@@ -10,7 +10,6 @@ rust-version.workspace = true
[dependencies] [dependencies]
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
moka = { workspace = true, features = ["sync"] } moka = { workspace = true, features = ["sync"] }
+2
View File
@@ -1,9 +1,11 @@
mod identifiers; mod identifiers;
mod mode; mod mode;
mod private_key_allocator; mod private_key_allocator;
mod round_robin_pool;
mod version_or_requirement; mod version_or_requirement;
pub use identifiers::*; pub use identifiers::*;
pub use mode::*; pub use mode::*;
pub use private_key_allocator::*; pub use private_key_allocator::*;
pub use round_robin_pool::*;
pub use version_or_requirement::*; pub use version_or_requirement::*;
@@ -1,6 +1,6 @@
use alloy::primitives::U256;
use alloy::signers::local::PrivateKeySigner; use alloy::signers::local::PrivateKeySigner;
use alloy_primitives::U256; use anyhow::{Context, Result, bail};
use anyhow::{Result, bail};
/// This is a sequential private key allocator. When instantiated, it allocated private keys in /// This is a sequential private key allocator. When instantiated, it allocated private keys in
/// sequentially and in order until the maximum private key specified is reached. /// sequentially and in order until the maximum private key specified is reached.
@@ -10,25 +10,26 @@ pub struct PrivateKeyAllocator {
next_private_key: U256, next_private_key: U256,
/// The highest private key (exclusive) that can be returned by this allocator. /// The highest private key (exclusive) that can be returned by this allocator.
highest_private_key_exclusive: U256, highest_private_key_inclusive: U256,
} }
impl PrivateKeyAllocator { impl PrivateKeyAllocator {
/// Creates a new instance of the private key allocator. /// Creates a new instance of the private key allocator.
pub fn new(highest_private_key_exclusive: U256) -> Self { pub fn new(highest_private_key_inclusive: U256) -> Self {
Self { Self {
next_private_key: U256::ZERO, next_private_key: U256::ONE,
highest_private_key_exclusive, highest_private_key_inclusive,
} }
} }
/// Allocates a new private key and errors out if the maximum private key has been reached. /// Allocates a new private key and errors out if the maximum private key has been reached.
pub fn allocate(&mut self) -> Result<PrivateKeySigner> { pub fn allocate(&mut self) -> Result<PrivateKeySigner> {
if self.next_private_key >= self.highest_private_key_exclusive { if self.next_private_key > self.highest_private_key_inclusive {
bail!("Attempted to allocate a private key but failed since all have been allocated"); bail!("Attempted to allocate a private key but failed since all have been allocated");
}; };
let private_key = let private_key =
PrivateKeySigner::from_slice(self.next_private_key.to_be_bytes::<32>().as_slice())?; PrivateKeySigner::from_slice(self.next_private_key.to_be_bytes::<32>().as_slice())
.context("Failed to convert the private key digits into a private key")?;
self.next_private_key += U256::ONE; self.next_private_key += U256::ONE;
Ok(private_key) Ok(private_key)
} }
@@ -0,0 +1,24 @@
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct RoundRobinPool<T> {
next_index: AtomicUsize,
items: Vec<T>,
}
impl<T> RoundRobinPool<T> {
pub fn new(items: Vec<T>) -> Self {
Self {
next_index: Default::default(),
items,
}
}
pub fn round_robin(&self) -> &T {
let current = self.next_index.fetch_add(1, Ordering::SeqCst) % self.items.len();
self.items.get(current).unwrap()
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.items.iter()
}
}
-1
View File
@@ -16,7 +16,6 @@ revive-dt-solc-binaries = { workspace = true }
revive-common = { workspace = true } revive-common = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
dashmap = { workspace = true } dashmap = { workspace = true }
foundry-compilers-artifacts = { workspace = true } foundry-compilers-artifacts = { workspace = true }
+1 -1
View File
@@ -11,7 +11,7 @@ use std::{
}; };
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use alloy_primitives::Address; use alloy::primitives::Address;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use semver::Version; use semver::Version;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
+14
View File
@@ -16,6 +16,7 @@ use revive_solc_json_interface::{
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection, SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
SolcStandardJsonOutput, SolcStandardJsonOutput,
}; };
use tracing::{Span, field::display};
use crate::{ use crate::{
CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc, CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc,
@@ -80,6 +81,16 @@ impl SolidityCompiler for Resolc {
} }
#[tracing::instrument(level = "debug", ret)] #[tracing::instrument(level = "debug", ret)]
#[tracing::instrument(
level = "error",
skip_all,
fields(
resolc_version = %self.version(),
solc_version = %self.0.solc.version(),
json_in = tracing::field::Empty
),
err(Debug)
)]
fn build( fn build(
&self, &self,
CompilerInput { CompilerInput {
@@ -141,6 +152,7 @@ impl SolidityCompiler for Resolc {
polkavm: None, polkavm: None,
}, },
}; };
Span::current().record("json_in", display(serde_json::to_string(&input).unwrap()));
let path = &self.0.resolc_path; let path = &self.0.resolc_path;
let mut command = AsyncCommand::new(path); let mut command = AsyncCommand::new(path);
@@ -148,6 +160,8 @@ impl SolidityCompiler for Resolc {
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.arg("--solc")
.arg(self.0.solc.path())
.arg("--standard-json"); .arg("--standard-json");
if let Some(ref base_path) = base_path { if let Some(ref base_path) = base_path {
+19 -9
View File
@@ -10,8 +10,9 @@ use std::{
use dashmap::DashMap; use dashmap::DashMap;
use revive_dt_common::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration}; use revive_dt_config::{SolcConfiguration, WorkingDirectoryConfiguration};
use revive_dt_solc_binaries::download_solc; use revive_dt_solc_binaries::download_solc;
use tracing::{Span, field::display, info};
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler}; use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
@@ -39,9 +40,7 @@ struct SolcInner {
impl Solc { impl Solc {
pub async fn new( pub async fn new(
context: impl AsRef<SolcConfiguration> context: impl AsRef<SolcConfiguration> + AsRef<WorkingDirectoryConfiguration>,
+ AsRef<ResolcConfiguration>
+ AsRef<WorkingDirectoryConfiguration>,
version: impl Into<Option<VersionOrRequirement>>, version: impl Into<Option<VersionOrRequirement>>,
) -> Result<Self> { ) -> Result<Self> {
// This is a cache for the compiler objects so that whenever the same compiler version is // This is a cache for the compiler objects so that whenever the same compiler version is
@@ -69,6 +68,11 @@ impl Solc {
Ok(COMPILERS_CACHE Ok(COMPILERS_CACHE
.entry((path.clone(), version.clone())) .entry((path.clone(), version.clone()))
.or_insert_with(|| { .or_insert_with(|| {
info!(
solc_path = %path.display(),
solc_version = %version,
"Created a new solc compiler object"
);
Self(Arc::new(SolcInner { Self(Arc::new(SolcInner {
solc_path: path, solc_path: path,
solc_version: version, solc_version: version,
@@ -88,6 +92,12 @@ impl SolidityCompiler for Solc {
} }
#[tracing::instrument(level = "debug", ret)] #[tracing::instrument(level = "debug", ret)]
#[tracing::instrument(
level = "error",
skip_all,
fields(json_in = tracing::field::Empty),
err(Debug)
)]
fn build( fn build(
&self, &self,
CompilerInput { CompilerInput {
@@ -166,12 +176,14 @@ impl SolidityCompiler for Solc {
}, },
}; };
Span::current().record("json_in", display(serde_json::to_string(&input).unwrap()));
let path = &self.0.solc_path; let path = &self.0.solc_path;
let mut command = AsyncCommand::new(path); let mut command = AsyncCommand::new(path);
command command
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::null())
.arg("--standard-json"); .arg("--standard-json");
if let Some(ref base_path) = base_path { if let Some(ref base_path) = base_path {
@@ -205,20 +217,18 @@ impl SolidityCompiler for Solc {
if !output.status.success() { if !output.status.success() {
let json_in = serde_json::to_string_pretty(&input) let json_in = serde_json::to_string_pretty(&input)
.context("Failed to pretty-print Standard JSON input for logging")?; .context("Failed to pretty-print Standard JSON input for logging")?;
let message = String::from_utf8_lossy(&output.stderr);
tracing::error!( tracing::error!(
status = %output.status, status = %output.status,
message = %message,
json_input = json_in, json_input = json_in,
"Compilation using solc failed" "Compilation using solc failed"
); );
anyhow::bail!("Compilation failed with an error: {message}"); anyhow::bail!("Compilation failed");
} }
let parsed = serde_json::from_slice::<SolcOutput>(&output.stdout) let parsed = serde_json::from_slice::<SolcOutput>(&output.stdout)
.map_err(|e| { .map_err(|e| {
anyhow::anyhow!( anyhow::anyhow!(
"failed to parse resolc JSON output: {e}\nstderr: {}", "failed to parse resolc JSON output: {e}\nstdout: {}",
String::from_utf8_lossy(&output.stdout) String::from_utf8_lossy(&output.stdout)
) )
}) })
+225 -24
View File
@@ -28,7 +28,11 @@ use temp_dir::TempDir;
#[command(name = "retester")] #[command(name = "retester")]
pub enum Context { pub enum Context {
/// Executes tests in the MatterLabs format differentially on multiple targets concurrently. /// Executes tests in the MatterLabs format differentially on multiple targets concurrently.
ExecuteTests(Box<TestExecutionContext>), Test(Box<TestExecutionContext>),
/// Executes differential benchmarks on various platforms.
Benchmark(Box<BenchmarkingContext>),
/// Exports the JSON schema of the MatterLabs test format used by the tool. /// Exports the JSON schema of the MatterLabs test format used by the tool.
ExportJsonSchema, ExportJsonSchema,
} }
@@ -46,7 +50,18 @@ impl Context {
impl AsRef<WorkingDirectoryConfiguration> for Context { impl AsRef<WorkingDirectoryConfiguration> for Context {
fn as_ref(&self) -> &WorkingDirectoryConfiguration { fn as_ref(&self) -> &WorkingDirectoryConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
impl AsRef<CorpusConfiguration> for Context {
fn as_ref(&self) -> &CorpusConfiguration {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -55,7 +70,8 @@ impl AsRef<WorkingDirectoryConfiguration> for Context {
impl AsRef<SolcConfiguration> for Context { impl AsRef<SolcConfiguration> for Context {
fn as_ref(&self) -> &SolcConfiguration { fn as_ref(&self) -> &SolcConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -64,7 +80,8 @@ impl AsRef<SolcConfiguration> for Context {
impl AsRef<ResolcConfiguration> for Context { impl AsRef<ResolcConfiguration> for Context {
fn as_ref(&self) -> &ResolcConfiguration { fn as_ref(&self) -> &ResolcConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -73,7 +90,8 @@ impl AsRef<ResolcConfiguration> for Context {
impl AsRef<GethConfiguration> for Context { impl AsRef<GethConfiguration> for Context {
fn as_ref(&self) -> &GethConfiguration { fn as_ref(&self) -> &GethConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -82,7 +100,8 @@ impl AsRef<GethConfiguration> for Context {
impl AsRef<KurtosisConfiguration> for Context { impl AsRef<KurtosisConfiguration> for Context {
fn as_ref(&self) -> &KurtosisConfiguration { fn as_ref(&self) -> &KurtosisConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -91,7 +110,8 @@ impl AsRef<KurtosisConfiguration> for Context {
impl AsRef<KitchensinkConfiguration> for Context { impl AsRef<KitchensinkConfiguration> for Context {
fn as_ref(&self) -> &KitchensinkConfiguration { fn as_ref(&self) -> &KitchensinkConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -100,7 +120,8 @@ impl AsRef<KitchensinkConfiguration> for Context {
impl AsRef<ReviveDevNodeConfiguration> for Context { impl AsRef<ReviveDevNodeConfiguration> for Context {
fn as_ref(&self) -> &ReviveDevNodeConfiguration { fn as_ref(&self) -> &ReviveDevNodeConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -109,7 +130,8 @@ impl AsRef<ReviveDevNodeConfiguration> for Context {
impl AsRef<EthRpcConfiguration> for Context { impl AsRef<EthRpcConfiguration> for Context {
fn as_ref(&self) -> &EthRpcConfiguration { fn as_ref(&self) -> &EthRpcConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -118,7 +140,11 @@ impl AsRef<EthRpcConfiguration> for Context {
impl AsRef<GenesisConfiguration> for Context { impl AsRef<GenesisConfiguration> for Context {
fn as_ref(&self) -> &GenesisConfiguration { fn as_ref(&self) -> &GenesisConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(..) => {
static GENESIS: LazyLock<GenesisConfiguration> = LazyLock::new(Default::default);
&GENESIS
}
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -127,7 +153,8 @@ impl AsRef<GenesisConfiguration> for Context {
impl AsRef<WalletConfiguration> for Context { impl AsRef<WalletConfiguration> for Context {
fn as_ref(&self) -> &WalletConfiguration { fn as_ref(&self) -> &WalletConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -136,7 +163,8 @@ impl AsRef<WalletConfiguration> for Context {
impl AsRef<ConcurrencyConfiguration> for Context { impl AsRef<ConcurrencyConfiguration> for Context {
fn as_ref(&self) -> &ConcurrencyConfiguration { fn as_ref(&self) -> &ConcurrencyConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -145,7 +173,8 @@ impl AsRef<ConcurrencyConfiguration> for Context {
impl AsRef<CompilationConfiguration> for Context { impl AsRef<CompilationConfiguration> for Context {
fn as_ref(&self) -> &CompilationConfiguration { fn as_ref(&self) -> &CompilationConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -154,7 +183,8 @@ impl AsRef<CompilationConfiguration> for Context {
impl AsRef<ReportConfiguration> for Context { impl AsRef<ReportConfiguration> for Context {
fn as_ref(&self) -> &ReportConfiguration { fn as_ref(&self) -> &ReportConfiguration {
match self { match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(), Self::ExportJsonSchema => unreachable!(),
} }
} }
@@ -183,9 +213,9 @@ pub struct TestExecutionContext {
)] )]
pub platforms: Vec<PlatformIdentifier>, pub platforms: Vec<PlatformIdentifier>,
/// A list of test corpus JSON files to be tested. /// Configuration parameters for the corpus files to use.
#[arg(long = "corpus", short)] #[clap(flatten, next_help_heading = "Corpus Configuration")]
pub corpus: Vec<PathBuf>, pub corpus_configuration: CorpusConfiguration,
/// Configuration parameters for the solc compiler. /// Configuration parameters for the solc compiler.
#[clap(flatten, next_help_heading = "Solc Configuration")] #[clap(flatten, next_help_heading = "Solc Configuration")]
@@ -236,6 +266,83 @@ pub struct TestExecutionContext {
pub report_configuration: ReportConfiguration, pub report_configuration: ReportConfiguration,
} }
#[derive(Clone, Debug, Parser, Serialize)]
pub struct BenchmarkingContext {
/// The working directory that the program will use for all of the temporary artifacts needed at
/// runtime.
///
/// If not specified, then a temporary directory will be created and used by the program for all
/// temporary artifacts.
#[clap(
short,
long,
default_value = "",
value_hint = ValueHint::DirPath,
)]
pub working_directory: WorkingDirectoryConfiguration,
/// The set of platforms that the differential tests should run on.
#[arg(
short = 'p',
long = "platform",
default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"]
)]
pub platforms: Vec<PlatformIdentifier>,
/// The default repetition count for any workload specified but that doesn't contain a repeat
/// step.
#[arg(short = 'r', long = "default-repetition-count", default_value_t = 1000)]
pub default_repetition_count: usize,
/// Configuration parameters for the corpus files to use.
#[clap(flatten, next_help_heading = "Corpus Configuration")]
pub corpus_configuration: CorpusConfiguration,
/// Configuration parameters for the solc compiler.
#[clap(flatten, next_help_heading = "Solc Configuration")]
pub solc_configuration: SolcConfiguration,
/// Configuration parameters for the resolc compiler.
#[clap(flatten, next_help_heading = "Resolc Configuration")]
pub resolc_configuration: ResolcConfiguration,
/// Configuration parameters for the geth node.
#[clap(flatten, next_help_heading = "Geth Configuration")]
pub geth_configuration: GethConfiguration,
/// Configuration parameters for the lighthouse node.
#[clap(flatten, next_help_heading = "Lighthouse Configuration")]
pub lighthouse_configuration: KurtosisConfiguration,
/// Configuration parameters for the Kitchensink.
#[clap(flatten, next_help_heading = "Kitchensink Configuration")]
pub kitchensink_configuration: KitchensinkConfiguration,
/// Configuration parameters for the Revive Dev Node.
#[clap(flatten, next_help_heading = "Revive Dev Node Configuration")]
pub revive_dev_node_configuration: ReviveDevNodeConfiguration,
/// Configuration parameters for the Eth Rpc.
#[clap(flatten, next_help_heading = "Eth RPC Configuration")]
pub eth_rpc_configuration: EthRpcConfiguration,
/// Configuration parameters for the wallet.
#[clap(flatten, next_help_heading = "Wallet Configuration")]
pub wallet_configuration: WalletConfiguration,
/// Configuration parameters for concurrency.
#[clap(flatten, next_help_heading = "Concurrency Configuration")]
pub concurrency_configuration: ConcurrencyConfiguration,
/// Configuration parameters for the compilers and compilation.
#[clap(flatten, next_help_heading = "Compilation Configuration")]
pub compilation_configuration: CompilationConfiguration,
/// Configuration parameters for the report.
#[clap(flatten, next_help_heading = "Report Configuration")]
pub report_configuration: ReportConfiguration,
}
impl Default for TestExecutionContext { impl Default for TestExecutionContext {
fn default() -> Self { fn default() -> Self {
Self::parse_from(["execution-context"]) Self::parse_from(["execution-context"])
@@ -248,6 +355,12 @@ impl AsRef<WorkingDirectoryConfiguration> for TestExecutionContext {
} }
} }
impl AsRef<CorpusConfiguration> for TestExecutionContext {
fn as_ref(&self) -> &CorpusConfiguration {
&self.corpus_configuration
}
}
impl AsRef<SolcConfiguration> for TestExecutionContext { impl AsRef<SolcConfiguration> for TestExecutionContext {
fn as_ref(&self) -> &SolcConfiguration { fn as_ref(&self) -> &SolcConfiguration {
&self.solc_configuration &self.solc_configuration
@@ -320,6 +433,98 @@ impl AsRef<ReportConfiguration> for TestExecutionContext {
} }
} }
impl Default for BenchmarkingContext {
fn default() -> Self {
Self::parse_from(["execution-context"])
}
}
impl AsRef<WorkingDirectoryConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
&self.working_directory
}
}
impl AsRef<CorpusConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &CorpusConfiguration {
&self.corpus_configuration
}
}
impl AsRef<SolcConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &SolcConfiguration {
&self.solc_configuration
}
}
impl AsRef<ResolcConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &ResolcConfiguration {
&self.resolc_configuration
}
}
impl AsRef<GethConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &GethConfiguration {
&self.geth_configuration
}
}
impl AsRef<KurtosisConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &KurtosisConfiguration {
&self.lighthouse_configuration
}
}
impl AsRef<KitchensinkConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &KitchensinkConfiguration {
&self.kitchensink_configuration
}
}
impl AsRef<ReviveDevNodeConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
&self.revive_dev_node_configuration
}
}
impl AsRef<EthRpcConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &EthRpcConfiguration {
&self.eth_rpc_configuration
}
}
impl AsRef<WalletConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &WalletConfiguration {
&self.wallet_configuration
}
}
impl AsRef<ConcurrencyConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &ConcurrencyConfiguration {
&self.concurrency_configuration
}
}
impl AsRef<CompilationConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &CompilationConfiguration {
&self.compilation_configuration
}
}
impl AsRef<ReportConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &ReportConfiguration {
&self.report_configuration
}
}
/// A set of configuration parameters for the corpus files to use for the execution.
#[derive(Clone, Debug, Parser, Serialize)]
pub struct CorpusConfiguration {
/// A list of test corpus JSON files to be tested.
#[arg(short = 'c', long = "corpus")]
pub paths: Vec<PathBuf>,
}
/// A set of configuration parameters for Solc. /// A set of configuration parameters for Solc.
#[derive(Clone, Debug, Parser, Serialize)] #[derive(Clone, Debug, Parser, Serialize)]
pub struct SolcConfiguration { pub struct SolcConfiguration {
@@ -397,10 +602,6 @@ pub struct KitchensinkConfiguration {
value_parser = parse_duration value_parser = parse_duration
)] )]
pub start_timeout_ms: Duration, pub start_timeout_ms: Duration,
/// This configures the tool to use Kitchensink instead of using the revive-dev-node.
#[clap(long = "kitchensink.dont-use-dev-node")]
pub use_kitchensink: bool,
} }
/// A set of configuration parameters for the revive dev node. /// A set of configuration parameters for the revive dev node.
@@ -448,7 +649,7 @@ pub struct EthRpcConfiguration {
} }
/// A set of configuration parameters for the genesis. /// A set of configuration parameters for the genesis.
#[derive(Clone, Debug, Parser, Serialize)] #[derive(Clone, Debug, Default, Parser, Serialize)]
pub struct GenesisConfiguration { pub struct GenesisConfiguration {
/// Specifies the path of the genesis file to use for the nodes that are started. /// Specifies the path of the genesis file to use for the nodes that are started.
/// ///
@@ -499,8 +700,8 @@ pub struct WalletConfiguration {
/// This argument controls which private keys the nodes should have access to and be added to /// 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 /// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
/// of the node. /// of the node.
#[clap(long = "wallet.additional-keys", default_value_t = 200)] #[clap(long = "wallet.additional-keys", default_value_t = 100_000)]
additional_keys: usize, pub additional_keys: usize,
/// The wallet object that will be used. /// The wallet object that will be used.
#[clap(skip)] #[clap(skip)]
@@ -0,0 +1,770 @@
use std::{
collections::HashMap,
ops::ControlFlow,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use alloy::{
hex,
json_abi::JsonAbi,
network::{Ethereum, TransactionBuilder},
primitives::{Address, TxHash, U256},
rpc::types::{
TransactionReceipt, TransactionRequest,
trace::geth::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
GethDebugTracingOptions,
},
},
};
use anyhow::{Context as _, Result, bail};
use indexmap::IndexMap;
use revive_dt_common::{
futures::{PollingWaitBehavior, poll},
types::PrivateKeyAllocator,
};
use revive_dt_format::{
metadata::{ContractInstance, ContractPathAndIdent},
steps::{
AllocateAccountStep, BalanceAssertionStep, Calldata, EtherValue, FunctionCallStep, Method,
RepeatStep, Step, StepAddress, StepIdx, StepPath, StorageEmptyAssertionStep,
},
traits::{ResolutionContext, ResolverApi},
};
use tokio::sync::{Mutex, mpsc::UnboundedSender};
use tracing::{Instrument, Span, debug, error, field::display, info, info_span, instrument};
use crate::{
differential_benchmarks::{ExecutionState, WatcherEvent},
helpers::{CachedCompiler, TestDefinition, TestPlatformInformation},
};
static DRIVER_COUNT: AtomicUsize = AtomicUsize::new(0);
/// The differential tests driver for a single platform.
pub struct Driver<'a, I> {
/// The id of the driver.
driver_id: usize,
/// The information of the platform that this driver is for.
platform_information: &'a TestPlatformInformation<'a>,
/// The resolver of the platform.
resolver: Arc<dyn ResolverApi + 'a>,
/// The definition of the test that the driver is instructed to execute.
test_definition: &'a TestDefinition<'a>,
/// The private key allocator used by this driver and other drivers when account allocations are
/// needed.
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
/// The execution state associated with the platform.
execution_state: ExecutionState,
/// The send side of the watcher's unbounded channel associated with this driver.
watcher_tx: UnboundedSender<WatcherEvent>,
/// The number of steps that were executed on the driver.
steps_executed: usize,
/// This is the queue of steps that are to be executed by the driver for this test case. Each
/// time `execute_step` is called one of the steps is executed.
steps_iterator: I,
}
impl<'a, I> Driver<'a, I>
where
I: Iterator<Item = (StepPath, Step)>,
{
// region:Constructors & Initialization
pub async fn new(
platform_information: &'a TestPlatformInformation<'a>,
test_definition: &'a TestDefinition<'a>,
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
cached_compiler: &CachedCompiler<'a>,
watcher_tx: UnboundedSender<WatcherEvent>,
steps: I,
) -> Result<Self> {
let mut this = Driver {
driver_id: DRIVER_COUNT.fetch_add(1, Ordering::SeqCst),
platform_information,
resolver: platform_information
.node
.resolver()
.await
.context("Failed to create resolver")?,
test_definition,
private_key_allocator,
execution_state: ExecutionState::empty(),
steps_executed: 0,
steps_iterator: steps,
watcher_tx,
};
this.init_execution_state(cached_compiler)
.await
.context("Failed to initialize the execution state of the platform")?;
Ok(this)
}
async fn init_execution_state(&mut self, cached_compiler: &CachedCompiler<'a>) -> Result<()> {
let compiler_output = cached_compiler
.compile_contracts(
self.test_definition.metadata,
self.test_definition.metadata_file_path,
self.test_definition.mode.clone(),
None,
self.platform_information.compiler.as_ref(),
self.platform_information.platform,
&self.platform_information.reporter,
)
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Pre-linking compilation failed"
)
})
.context("Failed to produce the pre-linking compiled contracts")?;
let mut deployed_libraries = None::<HashMap<_, _>>;
let mut contract_sources = self
.test_definition
.metadata
.contract_sources()
.inspect_err(|err| {
error!(
?err,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Failed to retrieve contract sources from metadata"
)
})
.context("Failed to get the contract instances from the metadata file")?;
for library_instance in self
.test_definition
.metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
debug!(%library_instance, "Deploying Library Instance");
let ContractPathAndIdent {
contract_source_path: library_source_path,
contract_ident: library_ident,
} = contract_sources
.remove(library_instance)
.context("Failed to get the contract sources of the contract instance")?;
let (code, abi) = compiler_output
.contracts
.get(&library_source_path)
.and_then(|contracts| contracts.get(library_ident.as_str()))
.context("Failed to get the code and abi for the instance")?;
let code = alloy::hex::decode(code)?;
// Getting the deployer address from the cases themselves. This is to ensure
// that we're doing the deployments from different accounts and therefore we're
// not slowed down by the nonce.
let deployer_address = self
.test_definition
.case
.steps
.iter()
.filter_map(|step| match step {
Step::FunctionCall(input) => input.caller.as_address().copied(),
Step::BalanceAssertion(..) => None,
Step::StorageEmptyAssertion(..) => None,
Step::Repeat(..) => None,
Step::AllocateAccount(..) => None,
})
.next()
.unwrap_or(FunctionCallStep::default_caller_address());
let tx = TransactionBuilder::<Ethereum>::with_deploy_code(
TransactionRequest::default().from(deployer_address),
code,
);
let receipt = self.execute_transaction(tx).await.inspect_err(|err| {
error!(
?err,
%library_instance,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Failed to deploy the library"
)
})?;
debug!(
?library_instance,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Deployed library"
);
let library_address = receipt
.contract_address
.expect("Failed to deploy the library");
deployed_libraries.get_or_insert_default().insert(
library_instance.clone(),
(library_ident.clone(), library_address, abi.clone()),
);
}
let compiler_output = cached_compiler
.compile_contracts(
self.test_definition.metadata,
self.test_definition.metadata_file_path,
self.test_definition.mode.clone(),
deployed_libraries.as_ref(),
self.platform_information.compiler.as_ref(),
self.platform_information.platform,
&self.platform_information.reporter,
)
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Post-linking compilation failed"
)
})
.context("Failed to compile the post-link contracts")?;
self.execution_state = ExecutionState::new(
compiler_output.contracts,
deployed_libraries.unwrap_or_default(),
);
Ok(())
}
// endregion:Constructors & Initialization
// region:Step Handling
pub async fn execute_all(mut self) -> Result<usize> {
while let Some(result) = self.execute_next_step().await {
result?
}
Ok(self.steps_executed)
}
pub async fn execute_next_step(&mut self) -> Option<Result<()>> {
let (step_path, step) = self.steps_iterator.next()?;
info!(%step_path, "Executing Step");
Some(
self.execute_step(&step_path, &step)
.await
.inspect(|_| info!(%step_path, "Step execution succeeded"))
.inspect_err(|err| error!(%step_path, ?err, "Step execution failed")),
)
}
#[instrument(
level = "info",
skip_all,
fields(
driver_id = self.driver_id,
platform_identifier = %self.platform_information.platform.platform_identifier(),
%step_path,
),
err(Debug),
)]
async fn execute_step(&mut self, step_path: &StepPath, step: &Step) -> Result<()> {
let steps_executed = match step {
Step::FunctionCall(step) => self
.execute_function_call(step_path, step.as_ref())
.await
.context("Function call step Failed"),
Step::Repeat(step) => self
.execute_repeat_step(step_path, step.as_ref())
.await
.context("Repetition Step Failed"),
Step::AllocateAccount(step) => self
.execute_account_allocation(step_path, step.as_ref())
.await
.context("Account Allocation Step Failed"),
// The following steps are disabled in the benchmarking driver.
Step::BalanceAssertion(..) | Step::StorageEmptyAssertion(..) => Ok(0),
}?;
self.steps_executed += steps_executed;
Ok(())
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id))]
pub async fn execute_function_call(
&mut self,
_: &StepPath,
step: &FunctionCallStep,
) -> Result<usize> {
let deployment_receipts = self
.handle_function_call_contract_deployment(step)
.await
.context("Failed to deploy contracts for the function call step")?;
let execution_receipt = self
.handle_function_call_execution(step, deployment_receipts)
.await
.context("Failed to handle the function call execution")?;
let tracing_result = self
.handle_function_call_call_frame_tracing(execution_receipt.transaction_hash)
.await
.context("Failed to handle the function call call frame tracing")?;
self.handle_function_call_variable_assignment(step, &tracing_result)
.await
.context("Failed to handle function call variable assignment")?;
Ok(1)
}
async fn handle_function_call_contract_deployment(
&mut self,
step: &FunctionCallStep,
) -> Result<HashMap<ContractInstance, TransactionReceipt>> {
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in step.find_all_contract_instances().into_iter() {
if !self
.execution_state
.deployed_contracts
.contains_key(&instance)
{
instances_we_must_deploy.entry(instance).or_insert(false);
}
}
if let Method::Deployer = step.method {
instances_we_must_deploy.swap_remove(&step.instance);
instances_we_must_deploy.insert(step.instance.clone(), true);
}
let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
let calldata = deploy_with_constructor_arguments.then_some(&step.calldata);
let value = deploy_with_constructor_arguments
.then_some(step.value)
.flatten();
let caller = {
let context = self.default_resolution_context();
step.caller
.resolve_address(self.resolver.as_ref(), context)
.await?
};
if let (_, _, Some(receipt)) = self
.get_or_deploy_contract_instance(&instance, caller, calldata, value)
.await
.context("Failed to get or deploy contract instance during input execution")?
{
receipts.insert(instance.clone(), receipt);
}
}
Ok(receipts)
}
async fn handle_function_call_execution(
&mut self,
step: &FunctionCallStep,
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
) -> Result<TransactionReceipt> {
match step.method {
// This step was already executed when `handle_step` was called. We just need to
// lookup the transaction receipt in this case and continue on.
Method::Deployer => deployment_receipts
.remove(&step.instance)
.context("Failed to find deployment receipt for constructor call"),
Method::Fallback | Method::FunctionName(_) => {
let tx = step
.as_transaction(self.resolver.as_ref(), self.default_resolution_context())
.await?;
self.execute_transaction(tx).await
}
}
}
async fn handle_function_call_call_frame_tracing(
&mut self,
tx_hash: TxHash,
) -> Result<CallFrame> {
self.platform_information
.node
.trace_transaction(
tx_hash,
GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
)),
tracer_config: GethDebugTracerConfig(serde_json::json! {{
"onlyTopCall": true,
"withLog": false,
"withStorage": false,
"withMemory": false,
"withStack": false,
"withReturnData": true
}}),
..Default::default()
},
)
.await
.map(|trace| {
trace
.try_into_call_frame()
.expect("Impossible - we requested a callframe trace so we must get it back")
})
}
async fn handle_function_call_variable_assignment(
&mut self,
step: &FunctionCallStep,
tracing_result: &CallFrame,
) -> Result<()> {
let Some(ref assignments) = step.variable_assignments else {
return Ok(());
};
// Handling the return data variable assignments.
for (variable_name, output_word) in assignments.return_data.iter().zip(
tracing_result
.output
.as_ref()
.unwrap_or_default()
.to_vec()
.chunks(32),
) {
let value = U256::from_be_slice(output_word);
self.execution_state
.variables
.insert(variable_name.clone(), value);
tracing::info!(
variable_name,
variable_value = hex::encode(value.to_be_bytes::<32>()),
"Assigned variable"
);
}
Ok(())
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id))]
pub async fn execute_balance_assertion(
&mut self,
_: &StepPath,
_: &BalanceAssertionStep,
) -> anyhow::Result<usize> {
// Kept empty intentionally for the benchmark driver.
Ok(1)
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))]
async fn execute_storage_empty_assertion_step(
&mut self,
_: &StepPath,
_: &StorageEmptyAssertionStep,
) -> Result<usize> {
// Kept empty intentionally for the benchmark driver.
Ok(1)
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))]
async fn execute_repeat_step(
&mut self,
step_path: &StepPath,
step: &RepeatStep,
) -> Result<usize> {
let tasks = (0..step.repeat)
.map(|_| Driver {
driver_id: DRIVER_COUNT.fetch_add(1, Ordering::SeqCst),
platform_information: self.platform_information,
resolver: self.resolver.clone(),
test_definition: self.test_definition,
private_key_allocator: self.private_key_allocator.clone(),
execution_state: self.execution_state.clone(),
steps_executed: 0,
steps_iterator: {
let steps = step
.steps
.iter()
.cloned()
.enumerate()
.map(|(step_idx, step)| {
let step_idx = StepIdx::new(step_idx);
let step_path = step_path.append(step_idx);
(step_path, step)
})
.collect::<Vec<_>>();
steps.into_iter()
},
watcher_tx: self.watcher_tx.clone(),
})
.map(|driver| driver.execute_all());
// TODO: Determine how we want to know the `ignore_block_before` and if it's through the
// receipt and how this would impact the architecture and the possibility of us not waiting
// for receipts in the future.
self.watcher_tx
.send(WatcherEvent::RepetitionStartEvent {
ignore_block_before: 0,
})
.context("Failed to send message on the watcher's tx")?;
let res = futures::future::try_join_all(tasks)
.await
.context("Repetition execution failed")?;
Ok(res.into_iter().sum())
}
#[instrument(level = "info", fields(driver_id = self.driver_id), skip_all, err(Debug))]
pub async fn execute_account_allocation(
&mut self,
_: &StepPath,
step: &AllocateAccountStep,
) -> Result<usize> {
let Some(variable_name) = step.variable_name.strip_prefix("$VARIABLE:") else {
bail!("Account allocation must start with $VARIABLE:");
};
let private_key = self
.private_key_allocator
.lock()
.await
.allocate()
.context("Account allocation through the private key allocator failed")?;
let account = private_key.address();
let variable = U256::from_be_slice(account.0.as_slice());
self.execution_state
.variables
.insert(variable_name.to_string(), variable);
Ok(1)
}
// endregion:Step Handling
// region:Contract Deployment
#[instrument(
level = "info",
skip_all,
fields(
driver_id = self.driver_id,
platform_identifier = %self.platform_information.platform.platform_identifier(),
%contract_instance,
%deployer
),
err(Debug),
)]
async fn get_or_deploy_contract_instance(
&mut self,
contract_instance: &ContractInstance,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
) -> Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
if let Some((_, address, abi)) = self
.execution_state
.deployed_contracts
.get(contract_instance)
{
info!(
%address,
"Contract instance already deployed."
);
Ok((*address, abi.clone(), None))
} else {
info!("Contract instance requires deployment.");
let (address, abi, receipt) = self
.deploy_contract(contract_instance, deployer, calldata, value)
.await
.context("Failed to deploy contract")?;
info!(
%address,
"Contract instance has been deployed."
);
Ok((address, abi, Some(receipt)))
}
}
#[instrument(
level = "info",
skip_all,
fields(
driver_id = self.driver_id,
platform_identifier = %self.platform_information.platform.platform_identifier(),
%contract_instance,
%deployer
),
err(Debug),
)]
async fn deploy_contract(
&mut self,
contract_instance: &ContractInstance,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
) -> Result<(Address, JsonAbi, TransactionReceipt)> {
let Some(ContractPathAndIdent {
contract_source_path,
contract_ident,
}) = self
.test_definition
.metadata
.contract_sources()?
.remove(contract_instance)
else {
anyhow::bail!(
"Contract source not found for instance {:?}",
contract_instance
)
};
let Some((code, abi)) = self
.execution_state
.compiled_contracts
.get(&contract_source_path)
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
.cloned()
else {
anyhow::bail!(
"Failed to find information for contract {:?}",
contract_instance
)
};
let mut code = match alloy::hex::decode(&code) {
Ok(code) => code,
Err(error) => {
tracing::error!(
?error,
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
);
anyhow::bail!("Failed to hex-decode the byte code {}", error)
}
};
if let Some(calldata) = calldata {
let calldata = calldata
.calldata(self.resolver.as_ref(), self.default_resolution_context())
.await?;
code.extend(calldata);
}
let tx = {
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)
};
let receipt = match self.execute_transaction(tx).await {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(?error, "Contract deployment transaction failed.");
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?contract_instance,
instance_address = ?address,
"Deployed contract"
);
self.platform_information
.reporter
.report_contract_deployed_event(contract_instance.clone(), address)?;
self.execution_state.deployed_contracts.insert(
contract_instance.clone(),
(contract_ident, address, abi.clone()),
);
Ok((address, abi, receipt))
}
#[instrument(level = "info", fields(driver_id = self.driver_id), skip_all)]
async fn step_address_auto_deployment(
&mut self,
step_address: &StepAddress,
) -> Result<Address> {
match step_address {
StepAddress::Address(address) => Ok(*address),
StepAddress::ResolvableAddress(resolvable) => {
let Some(instance) = resolvable
.strip_suffix(".address")
.map(ContractInstance::new)
else {
bail!("Not an address variable");
};
self.get_or_deploy_contract_instance(
&instance,
FunctionCallStep::default_caller_address(),
None,
None,
)
.await
.map(|v| v.0)
}
}
}
// endregion:Contract Deployment
// region:Resolution & Resolver
fn default_resolution_context(&self) -> ResolutionContext<'_> {
ResolutionContext::default()
.with_deployed_contracts(&self.execution_state.deployed_contracts)
.with_variables(&self.execution_state.variables)
}
// endregion:Resolution & Resolver
// region:Transaction Execution
/// Executes the transaction on the driver's node with some custom waiting logic for the receipt
#[instrument(
level = "info",
skip_all,
fields(driver_id = self.driver_id, transaction_hash = tracing::field::Empty)
)]
async fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> anyhow::Result<TransactionReceipt> {
let node = self.platform_information.node;
let transaction_hash = node
.submit_transaction(transaction)
.await
.context("Failed to submit transaction")?;
Span::current().record("transaction_hash", display(transaction_hash));
info!("Submitted transaction");
self.watcher_tx
.send(WatcherEvent::SubmittedTransaction { transaction_hash })
.context("Failed to send the transaction hash to the watcher")?;
info!("Starting to poll for transaction receipt");
poll(
Duration::from_secs(30 * 60),
PollingWaitBehavior::Constant(Duration::from_secs(1)),
|| {
async move {
match node.get_receipt(transaction_hash).await {
Ok(receipt) => {
info!("Polling succeeded, receipt found");
Ok(ControlFlow::Break(receipt))
}
Err(_) => Ok(ControlFlow::Continue(())),
}
}
.instrument(info_span!("Polling for receipt"))
},
)
.await
}
// endregion:Transaction Execution
}
@@ -0,0 +1,177 @@
//! The main entry point for differential benchmarking.
use std::{collections::BTreeMap, sync::Arc};
use anyhow::Context as _;
use futures::{FutureExt, StreamExt};
use revive_dt_common::types::PrivateKeyAllocator;
use revive_dt_core::Platform;
use revive_dt_format::steps::{Step, StepIdx, StepPath};
use tokio::sync::Mutex;
use tracing::{error, info, info_span, instrument, warn};
use revive_dt_config::{BenchmarkingContext, Context};
use revive_dt_report::Reporter;
use crate::{
differential_benchmarks::{Driver, Watcher, WatcherEvent},
helpers::{CachedCompiler, NodePool, collect_metadata_files, create_test_definitions_stream},
};
/// Handles the differential testing executing it according to the information defined in the
/// context
#[instrument(level = "info", err(Debug), skip_all)]
pub async fn handle_differential_benchmarks(
mut context: BenchmarkingContext,
reporter: Reporter,
) -> anyhow::Result<()> {
// A bit of a hack but we need to override the number of nodes specified through the CLI since
// benchmarks can only be run on a single node. Perhaps in the future we'd have a cleaner way to
// do this. But, for the time being, we need to override the cli arguments.
if context.concurrency_configuration.number_of_nodes != 1 {
warn!(
specified_number_of_nodes = context.concurrency_configuration.number_of_nodes,
updated_number_of_nodes = 1,
"Invalid number of nodes specified through the CLI. Benchmarks can only be run on a single node. Updated the arguments."
);
context.concurrency_configuration.number_of_nodes = 1;
};
let full_context = Context::Benchmark(Box::new(context.clone()));
// Discover all of the metadata files that are defined in the context.
let metadata_files = collect_metadata_files(&context)
.context("Failed to collect metadata files for differential testing")?;
info!(len = metadata_files.len(), "Discovered metadata files");
// Discover the list of platforms that the tests should run on based on the context.
let platforms = context
.platforms
.iter()
.copied()
.map(Into::<&dyn Platform>::into)
.collect::<Vec<_>>();
// Starting the nodes of the various platforms specified in the context. Note that we use the
// node pool since it contains all of the code needed to spawn nodes from A to Z and therefore
// it's the preferred way for us to start nodes even when we're starting just a single node. The
// added overhead from it is quite small (performance wise) since it's involved only when we're
// creating the test definitions, but it might have other maintenance overhead as it obscures
// the fact that only a single node is spawned.
let platforms_and_nodes = {
let mut map = BTreeMap::new();
for platform in platforms.iter() {
let platform_identifier = platform.platform_identifier();
let node_pool = NodePool::new(full_context.clone(), *platform)
.await
.inspect_err(|err| {
error!(
?err,
%platform_identifier,
"Failed to initialize the node pool for the platform."
)
})
.context("Failed to initialize the node pool")?;
map.insert(platform_identifier, (*platform, node_pool));
}
map
};
info!("Spawned the platform nodes");
// Preparing test definitions for the execution.
let test_definitions = create_test_definitions_stream(
&full_context,
metadata_files.iter(),
&platforms_and_nodes,
reporter.clone(),
)
.await
.collect::<Vec<_>>()
.await;
info!(len = test_definitions.len(), "Created test definitions");
// Creating the objects that will be shared between the various runs. The cached compiler is the
// only one at the current moment of time that's safe to share between runs.
let cached_compiler = CachedCompiler::new(
context
.working_directory
.as_path()
.join("compilation_cache"),
context
.compilation_configuration
.invalidate_compilation_cache,
)
.await
.map(Arc::new)
.context("Failed to initialize cached compiler")?;
// Note: we do not want to run all of the workloads concurrently on all platforms. Rather, we'd
// like to run all of the workloads for one platform, and then the next sequentially as we'd
// like for the effect of concurrency to be minimized when we're doing the benchmarking.
for platform in platforms.iter() {
let platform_identifier = platform.platform_identifier();
let span = info_span!("Benchmarking for the platform", %platform_identifier);
let _guard = span.enter();
for test_definition in test_definitions.iter() {
let platform_information = &test_definition.platforms[&platform_identifier];
let span = info_span!(
"Executing workload",
metadata_file_path = %test_definition.metadata_file_path.display(),
case_idx = %test_definition.case_idx,
mode = %test_definition.mode,
);
let _guard = span.enter();
// Initializing all of the components requires to execute this particular workload.
let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
context.wallet_configuration.highest_private_key_exclusive(),
)));
let (watcher, watcher_tx) = Watcher::new(
platform_identifier,
platform_information
.node
.subscribe_to_full_blocks_information()
.await
.context("Failed to subscribe to full blocks information from the node")?,
);
let driver = Driver::new(
platform_information,
test_definition,
private_key_allocator,
cached_compiler.as_ref(),
watcher_tx.clone(),
test_definition
.case
.steps_iterator_for_benchmarks(context.default_repetition_count)
.enumerate()
.map(|(step_idx, step)| -> (StepPath, Step) {
(StepPath::new(vec![StepIdx::new(step_idx)]), step)
}),
)
.await
.context("Failed to create the benchmarks driver")?;
futures::future::try_join(
watcher.run(),
driver.execute_all().inspect(|_| {
info!("All transactions submitted - driver completed execution");
watcher_tx
.send(WatcherEvent::AllTransactionsSubmitted)
.unwrap()
}),
)
.await
.context("Failed to run the driver and executor")
.inspect(|(_, steps_executed)| info!(steps_executed, "Workload Execution Succeeded"))
.inspect_err(|err| error!(?err, "Workload Execution Failed"))?;
}
}
Ok(())
}
@@ -0,0 +1,43 @@
use std::{collections::HashMap, path::PathBuf};
use alloy::{
json_abi::JsonAbi,
primitives::{Address, U256},
};
use revive_dt_format::metadata::{ContractIdent, ContractInstance};
#[derive(Clone)]
/// The state associated with the test execution of one of the workloads.
pub struct ExecutionState {
/// The compiled contracts, these contracts have been compiled and have had the libraries linked
/// against them and therefore they're ready to be deployed on-demand.
pub compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// A map of all of the deployed contracts and information about them.
pub deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata file.
pub variables: HashMap<String, U256>,
}
impl ExecutionState {
pub fn new(
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
) -> Self {
Self {
compiled_contracts,
deployed_contracts,
variables: Default::default(),
}
}
pub fn empty() -> Self {
Self {
compiled_contracts: Default::default(),
deployed_contracts: Default::default(),
variables: Default::default(),
}
}
}
@@ -0,0 +1,9 @@
mod driver;
mod entry_point;
mod execution_state;
mod watcher;
pub use driver::*;
pub use entry_point::*;
pub use execution_state::*;
pub use watcher::*;
@@ -0,0 +1,207 @@
use std::{collections::HashSet, pin::Pin, sync::Arc};
use alloy::primitives::{BlockNumber, TxHash};
use anyhow::Result;
use futures::{Stream, StreamExt};
use revive_dt_common::types::PlatformIdentifier;
use revive_dt_node_interaction::MinedBlockInformation;
use tokio::sync::{
RwLock,
mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
};
use tracing::{info, instrument};
/// This struct defines the watcher used in the benchmarks. A watcher is only valid for 1 workload
/// and MUST NOT be re-used between workloads since it holds important internal state for a given
/// workload and is not designed for reuse.
pub struct Watcher {
/// The identifier of the platform that this watcher is for.
platform_identifier: PlatformIdentifier,
/// The receive side of the channel that all of the drivers and various other parts of the code
/// send events to the watcher on.
rx: UnboundedReceiver<WatcherEvent>,
/// This is a stream of the blocks that were mined by the node. This is for a single platform
/// and a single node from that platform.
blocks_stream: Pin<Box<dyn Stream<Item = MinedBlockInformation>>>,
}
impl Watcher {
pub fn new(
platform_identifier: PlatformIdentifier,
blocks_stream: Pin<Box<dyn Stream<Item = MinedBlockInformation>>>,
) -> (Self, UnboundedSender<WatcherEvent>) {
let (tx, rx) = unbounded_channel::<WatcherEvent>();
(
Self {
platform_identifier,
rx,
blocks_stream,
},
tx,
)
}
#[instrument(level = "info", skip_all)]
pub async fn run(mut self) -> Result<()> {
// The first event that the watcher receives must be a `RepetitionStartEvent` that informs
// the watcher of the last block number that it should ignore and what the block number is
// for the first important block that it should look for.
let ignore_block_before = loop {
let Some(WatcherEvent::RepetitionStartEvent {
ignore_block_before,
}) = self.rx.recv().await
else {
continue;
};
break ignore_block_before;
};
// This is the set of the transaction hashes that the watcher should be looking for and
// watch for them in the blocks. The watcher will keep watching for blocks until it sees
// that all of the transactions that it was watching for has been seen in the mined blocks.
let watch_for_transaction_hashes = Arc::new(RwLock::new(HashSet::<TxHash>::new()));
// A boolean that keeps track of whether all of the transactions were submitted or if more
// txs are expected to come through the receive side of the channel. We do not want to rely
// on the channel closing alone for the watcher to know that all of the transactions were
// submitted and for there to be an explicit event sent by the core orchestrator that
// informs the watcher that no further transactions are to be expected and that it can
// safely ignore the channel.
let all_transactions_submitted = Arc::new(RwLock::new(false));
let watcher_event_watching_task = {
let watch_for_transaction_hashes = watch_for_transaction_hashes.clone();
let all_transactions_submitted = all_transactions_submitted.clone();
async move {
while let Some(watcher_event) = self.rx.recv().await {
match watcher_event {
// Subsequent repetition starts are ignored since certain workloads can
// contain nested repetitions and therefore there's no use in doing any
// action if the repetitions are nested.
WatcherEvent::RepetitionStartEvent { .. } => {}
WatcherEvent::SubmittedTransaction { transaction_hash } => {
watch_for_transaction_hashes
.write()
.await
.insert(transaction_hash);
}
WatcherEvent::AllTransactionsSubmitted => {
*all_transactions_submitted.write().await = true;
self.rx.close();
info!("Watcher's Events Watching Task Finished");
break;
}
}
}
}
};
let block_information_watching_task = {
let watch_for_transaction_hashes = watch_for_transaction_hashes.clone();
let all_transactions_submitted = all_transactions_submitted.clone();
let mut blocks_information_stream = self.blocks_stream;
async move {
let mut mined_blocks_information = Vec::new();
while let Some(block) = blocks_information_stream.next().await {
// If the block number is equal to or less than the last block before the
// repetition then we ignore it and continue on to the next block.
if block.block_number <= ignore_block_before {
continue;
}
if *all_transactions_submitted.read().await
&& watch_for_transaction_hashes.read().await.is_empty()
{
break;
}
info!(
remaining_transactions = watch_for_transaction_hashes.read().await.len(),
block_tx_count = block.transaction_hashes.len(),
"Observed a block"
);
// Remove all of the transaction hashes observed in this block from the txs we
// are currently watching for.
let mut watch_for_transaction_hashes =
watch_for_transaction_hashes.write().await;
for tx_hash in block.transaction_hashes.iter() {
watch_for_transaction_hashes.remove(tx_hash);
}
mined_blocks_information.push(block);
}
info!("Watcher's Block Watching Task Finished");
mined_blocks_information
}
};
let (_, mined_blocks_information) =
futures::future::join(watcher_event_watching_task, block_information_watching_task)
.await;
// region:TEMPORARY
{
// TODO: The following core is TEMPORARY and will be removed once we have proper
// reporting in place and then it can be removed. This serves as as way of doing some
// very simple reporting for the time being.
use std::io::Write;
let mut stderr = std::io::stderr().lock();
writeln!(
stderr,
"Watcher information for {}",
self.platform_identifier
)?;
writeln!(
stderr,
"block_number,block_timestamp,mined_gas,block_gas_limit,tx_count"
)?;
for block in mined_blocks_information {
writeln!(
stderr,
"{},{},{},{},{}",
block.block_number,
block.block_timestamp,
block.mined_gas,
block.block_gas_limit,
block.transaction_hashes.len()
)?
}
}
// endregion:TEMPORARY
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum WatcherEvent {
/// Informs the watcher that it should begin watching for the blocks mined by the platforms.
/// Before the watcher receives this event it will not be watching for the mined blocks. The
/// reason behind this is that we do not want the initialization transactions (e.g., contract
/// deployments) to be included in the overall TPS and GPS measurements since these blocks will
/// most likely only contain a single transaction since they're just being used for
/// initialization.
RepetitionStartEvent {
/// This is the block number of the last block seen before the repetition started. This is
/// used to instruct the watcher to ignore all block prior to this block when it starts
/// streaming the blocks.
ignore_block_before: BlockNumber,
},
/// Informs the watcher that a transaction was submitted and that the watcher should watch for a
/// transaction with this hash in the blocks that it watches.
SubmittedTransaction {
/// The hash of the submitted transaction.
transaction_hash: TxHash,
},
/// Informs the watcher that all of the transactions of this benchmark have been submitted and
/// that it can expect to receive no further transaction hashes and not even watch the channel
/// any longer.
AllTransactionsSubmitted,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,240 @@
//! The main entry point into differential testing.
use std::{
collections::BTreeMap,
io::{BufWriter, Write, stderr},
sync::Arc,
time::Instant,
};
use anyhow::Context as _;
use futures::{FutureExt, StreamExt};
use revive_dt_common::types::PrivateKeyAllocator;
use revive_dt_core::Platform;
use tokio::sync::Mutex;
use tracing::{Instrument, error, info, info_span, instrument};
use revive_dt_config::{Context, TestExecutionContext};
use revive_dt_report::{Reporter, ReporterEvent, TestCaseStatus};
use crate::{
differential_tests::Driver,
helpers::{CachedCompiler, NodePool, collect_metadata_files, create_test_definitions_stream},
};
/// Handles the differential testing executing it according to the information defined in the
/// context
#[instrument(level = "info", err(Debug), skip_all)]
pub async fn handle_differential_tests(
context: TestExecutionContext,
reporter: Reporter,
) -> anyhow::Result<()> {
let reporter_clone = reporter.clone();
// Discover all of the metadata files that are defined in the context.
let metadata_files = collect_metadata_files(&context)
.context("Failed to collect metadata files for differential testing")?;
info!(len = metadata_files.len(), "Discovered metadata files");
// Discover the list of platforms that the tests should run on based on the context.
let platforms = context
.platforms
.iter()
.copied()
.map(Into::<&dyn Platform>::into)
.collect::<Vec<_>>();
// Starting the nodes of the various platforms specified in the context.
let platforms_and_nodes = {
let mut map = BTreeMap::new();
for platform in platforms.iter() {
let platform_identifier = platform.platform_identifier();
let context = Context::Test(Box::new(context.clone()));
let node_pool = NodePool::new(context, *platform)
.await
.inspect_err(|err| {
error!(
?err,
%platform_identifier,
"Failed to initialize the node pool for the platform."
)
})
.context("Failed to initialize the node pool")?;
map.insert(platform_identifier, (*platform, node_pool));
}
map
};
info!("Spawned the platform nodes");
// Preparing test definitions.
let full_context = Context::Test(Box::new(context.clone()));
let test_definitions = create_test_definitions_stream(
&full_context,
metadata_files.iter(),
&platforms_and_nodes,
reporter.clone(),
)
.await
.collect::<Vec<_>>()
.await;
info!(len = test_definitions.len(), "Created test definitions");
// Creating everything else required for the driver to run.
let cached_compiler = CachedCompiler::new(
context
.working_directory
.as_path()
.join("compilation_cache"),
context
.compilation_configuration
.invalidate_compilation_cache,
)
.await
.map(Arc::new)
.context("Failed to initialize cached compiler")?;
let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
context.wallet_configuration.highest_private_key_exclusive(),
)));
// Creating the driver and executing all of the steps.
let driver_task = futures::future::join_all(test_definitions.iter().map(|test_definition| {
let private_key_allocator = private_key_allocator.clone();
let cached_compiler = cached_compiler.clone();
let mode = test_definition.mode.clone();
let span = info_span!(
"Executing Test Case",
metadata_file_path = %test_definition.metadata_file_path.display(),
case_idx = %test_definition.case_idx,
mode = %mode
);
async move {
let driver =
match Driver::new_root(test_definition, private_key_allocator, &cached_compiler)
.await
{
Ok(driver) => driver,
Err(error) => {
test_definition
.reporter
.report_test_failed_event(format!("{error:#}"))
.expect("Can't fail");
error!("Test Case Failed");
return;
}
};
info!("Created the driver for the test case");
match driver.execute_all().await {
Ok(steps_executed) => test_definition
.reporter
.report_test_succeeded_event(steps_executed)
.expect("Can't fail"),
Err(error) => {
test_definition
.reporter
.report_test_failed_event(format!("{error:#}"))
.expect("Can't fail");
error!("Test Case Failed");
}
};
info!("Finished the execution of the test case")
}
.instrument(span)
}))
.inspect(|_| {
info!("Finished executing all test cases");
reporter_clone
.report_completion_event()
.expect("Can't fail")
});
let cli_reporting_task = start_cli_reporting_task(reporter);
futures::future::join(driver_task, cli_reporting_task).await;
Ok(())
}
#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)]
async fn start_cli_reporting_task(reporter: Reporter) {
let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail");
drop(reporter);
let start = Instant::now();
const GREEN: &str = "\x1B[32m";
const RED: &str = "\x1B[31m";
const GREY: &str = "\x1B[90m";
const COLOR_RESET: &str = "\x1B[0m";
const BOLD: &str = "\x1B[1m";
const BOLD_RESET: &str = "\x1B[22m";
let mut number_of_successes = 0;
let mut number_of_failures = 0;
let mut buf = BufWriter::new(stderr());
while let Ok(event) = aggregator_events_rx.recv().await {
let ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
metadata_file_path,
mode,
case_status,
} = event
else {
continue;
};
let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display());
for (case_idx, case_status) in case_status.into_iter() {
let _ = write!(buf, "\tCase Index {case_idx:>3}: ");
let _ = match case_status {
TestCaseStatus::Succeeded { steps_executed } => {
number_of_successes += 1;
writeln!(
buf,
"{}{}Case Succeeded{} - Steps Executed: {}{}",
GREEN, BOLD, BOLD_RESET, steps_executed, COLOR_RESET
)
}
TestCaseStatus::Failed { reason } => {
number_of_failures += 1;
writeln!(
buf,
"{}{}Case Failed{} - Reason: {}{}",
RED,
BOLD,
BOLD_RESET,
reason.trim(),
COLOR_RESET,
)
}
TestCaseStatus::Ignored { reason, .. } => writeln!(
buf,
"{}{}Case Ignored{} - Reason: {}{}",
GREY,
BOLD,
BOLD_RESET,
reason.trim(),
COLOR_RESET,
),
};
}
let _ = writeln!(buf);
}
// Summary at the end.
let _ = writeln!(
buf,
"{} cases: {}{}{} cases succeeded, {}{}{} cases failed in {} seconds",
number_of_successes + number_of_failures,
GREEN,
number_of_successes,
COLOR_RESET,
RED,
number_of_failures,
COLOR_RESET,
start.elapsed().as_secs()
);
}
@@ -0,0 +1,35 @@
use std::{collections::HashMap, path::PathBuf};
use alloy::{
json_abi::JsonAbi,
primitives::{Address, U256},
};
use revive_dt_format::metadata::{ContractIdent, ContractInstance};
#[derive(Clone)]
/// The state associated with the test execution of one of the tests.
pub struct ExecutionState {
/// The compiled contracts, these contracts have been compiled and have had the libraries linked
/// against them and therefore they're ready to be deployed on-demand.
pub compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// A map of all of the deployed contracts and information about them.
pub deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata file.
pub variables: HashMap<String, U256>,
}
impl ExecutionState {
pub fn new(
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
) -> Self {
Self {
compiled_contracts,
deployed_contracts,
variables: Default::default(),
}
}
}
+11
View File
@@ -0,0 +1,11 @@
//! This module contains all of the code responsible for performing differential tests including the
//! driver implementation, state implementation, and the core logic that allows for tests to be
//! executed.
mod driver;
mod entry_point;
mod execution_state;
pub use driver::*;
pub use entry_point::*;
pub use execution_state::*;
-900
View File
@@ -1,900 +0,0 @@
//! The test driver handles the compilation and execution of the test cases.
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use alloy::consensus::EMPTY_ROOT_HASH;
use alloy::hex;
use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder};
use alloy::primitives::{TxHash, U256};
use alloy::rpc::types::TransactionReceipt;
use alloy::rpc::types::trace::geth::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
GethDebugTracingOptions, GethTrace, PreStateConfig,
};
use alloy::{
primitives::Address,
rpc::types::{TransactionRequest, trace::geth::DiffMode},
};
use anyhow::{Context as _, bail};
use futures::{TryStreamExt, future::try_join_all};
use indexmap::IndexMap;
use revive_dt_common::types::{PlatformIdentifier, PrivateKeyAllocator};
use revive_dt_format::traits::{ResolutionContext, ResolverApi};
use revive_dt_report::ExecutionSpecificReporter;
use semver::Version;
use revive_dt_format::case::Case;
use revive_dt_format::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent};
use revive_dt_format::steps::{
BalanceAssertionStep, Calldata, EtherValue, Expected, ExpectedOutput, FunctionCallStep, Method,
StepIdx, StepPath, StorageEmptyAssertionStep,
};
use revive_dt_format::{metadata::Metadata, steps::Step};
use revive_dt_node_interaction::EthereumNode;
use tokio::sync::Mutex;
use tokio::try_join;
use tracing::{Instrument, info, info_span, instrument};
#[derive(Clone)]
pub struct CaseState {
/// A map of all of the compiled contracts for the given metadata file.
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// This map stores the contracts deployments for this case.
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata
/// file.
variables: HashMap<String, U256>,
/// Stores the version used for the current case.
compiler_version: Version,
/// The execution reporter.
execution_reporter: ExecutionSpecificReporter,
/// The private key allocator used for this case state. This is an Arc Mutex to allow for the
/// state to be cloned and for all of the clones to refer to the same allocator.
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
}
impl CaseState {
pub fn new(
compiler_version: Version,
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
execution_reporter: ExecutionSpecificReporter,
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
) -> Self {
Self {
compiled_contracts,
deployed_contracts,
variables: Default::default(),
compiler_version,
execution_reporter,
private_key_allocator,
}
}
pub async fn handle_step(
&mut self,
metadata: &Metadata,
step: &Step,
step_path: &StepPath,
node: &dyn EthereumNode,
) -> anyhow::Result<StepOutput> {
match step {
Step::FunctionCall(input) => {
let (receipt, geth_trace, diff_mode) = self
.handle_input(metadata, input, node)
.await
.context("Failed to handle function call step")?;
Ok(StepOutput::FunctionCall(receipt, geth_trace, diff_mode))
}
Step::BalanceAssertion(balance_assertion) => {
self.handle_balance_assertion(metadata, balance_assertion, node)
.await
.context("Failed to handle balance assertion step")?;
Ok(StepOutput::BalanceAssertion)
}
Step::StorageEmptyAssertion(storage_empty) => {
self.handle_storage_empty(metadata, storage_empty, node)
.await
.context("Failed to handle storage empty assertion step")?;
Ok(StepOutput::StorageEmptyAssertion)
}
Step::Repeat(repetition_step) => {
self.handle_repeat(
metadata,
repetition_step.repeat,
&repetition_step.steps,
step_path,
node,
)
.await
.context("Failed to handle the repetition step")?;
Ok(StepOutput::Repetition)
}
Step::AllocateAccount(account_allocation) => {
self.handle_account_allocation(account_allocation.variable_name.as_str())
.await
.context("Failed to allocate account")?;
Ok(StepOutput::AccountAllocation)
}
}
.inspect(|_| info!("Step Succeeded"))
}
#[instrument(level = "info", name = "Handling Input", skip_all)]
pub async fn handle_input(
&mut self,
metadata: &Metadata,
input: &FunctionCallStep,
node: &dyn EthereumNode,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let resolver = node.resolver().await?;
let deployment_receipts = self
.handle_input_contract_deployment(metadata, input, node)
.await
.context("Failed during contract deployment phase of input handling")?;
let execution_receipt = self
.handle_input_execution(input, deployment_receipts, node)
.await
.context("Failed during transaction execution phase of input handling")?;
let tracing_result = self
.handle_input_call_frame_tracing(execution_receipt.transaction_hash, node)
.await
.context("Failed during callframe tracing phase of input handling")?;
self.handle_input_variable_assignment(input, &tracing_result)
.context("Failed to assign variables from callframe output")?;
let (_, (geth_trace, diff_mode)) = try_join!(
self.handle_input_expectations(
input,
&execution_receipt,
resolver.as_ref(),
&tracing_result
),
self.handle_input_diff(execution_receipt.transaction_hash, node)
)
.context("Failed while evaluating expectations and diffs in parallel")?;
Ok((execution_receipt, geth_trace, diff_mode))
}
#[instrument(level = "info", name = "Handling Balance Assertion", skip_all)]
pub async fn handle_balance_assertion(
&mut self,
metadata: &Metadata,
balance_assertion: &BalanceAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node)
.await
.context("Failed to deploy contract for balance assertion")?;
self.handle_balance_assertion_execution(balance_assertion, node)
.await
.context("Failed to execute balance assertion")?;
Ok(())
}
#[instrument(level = "info", name = "Handling Storage Assertion", skip_all)]
pub async fn handle_storage_empty(
&mut self,
metadata: &Metadata,
storage_empty: &StorageEmptyAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node)
.await
.context("Failed to deploy contract for storage empty assertion")?;
self.handle_storage_empty_assertion_execution(storage_empty, node)
.await
.context("Failed to execute storage empty assertion")?;
Ok(())
}
#[instrument(level = "info", name = "Handling Repetition", skip_all)]
pub async fn handle_repeat(
&mut self,
metadata: &Metadata,
repetitions: usize,
steps: &[Step],
step_path: &StepPath,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let tasks = (0..repetitions).map(|_| {
let mut state = self.clone();
async move {
for (step_idx, step) in steps.iter().enumerate() {
let step_path = step_path.append(step_idx);
state.handle_step(metadata, step, &step_path, node).await?;
}
Ok::<(), anyhow::Error>(())
}
});
try_join_all(tasks).await?;
Ok(())
}
#[instrument(level = "info", name = "Handling Account Allocation", skip_all)]
pub async fn handle_account_allocation(&mut self, variable_name: &str) -> anyhow::Result<()> {
let Some(variable_name) = variable_name.strip_prefix("$VARIABLE:") else {
bail!("Account allocation must start with $VARIABLE:");
};
let private_key = self.private_key_allocator.lock().await.allocate()?;
let account = private_key.address();
let variable = U256::from_be_slice(account.0.as_slice());
self.variables.insert(variable_name.to_string(), variable);
Ok(())
}
/// Handles the contract deployment for a given input performing it if it needs to be performed.
#[instrument(level = "info", skip_all)]
async fn handle_input_contract_deployment(
&mut self,
metadata: &Metadata,
input: &FunctionCallStep,
node: &dyn EthereumNode,
) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> {
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in input.find_all_contract_instances().into_iter() {
if !self.deployed_contracts.contains_key(&instance) {
instances_we_must_deploy.entry(instance).or_insert(false);
}
}
if let Method::Deployer = input.method {
instances_we_must_deploy.swap_remove(&input.instance);
instances_we_must_deploy.insert(input.instance.clone(), true);
}
let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
let calldata = deploy_with_constructor_arguments.then_some(&input.calldata);
let value = deploy_with_constructor_arguments
.then_some(input.value)
.flatten();
let caller = {
let context = self.default_resolution_context();
let resolver = node.resolver().await?;
input
.caller
.resolve_address(resolver.as_ref(), context)
.await?
};
if let (_, _, Some(receipt)) = self
.get_or_deploy_contract_instance(&instance, metadata, caller, calldata, value, node)
.await
.context("Failed to get or deploy contract instance during input execution")?
{
receipts.insert(instance.clone(), receipt);
}
}
Ok(receipts)
}
/// Handles the execution of the input in terms of the calls that need to be made.
#[instrument(level = "info", skip_all)]
async fn handle_input_execution(
&mut self,
input: &FunctionCallStep,
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
node: &dyn EthereumNode,
) -> 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 for constructor call"),
Method::Fallback | Method::FunctionName(_) => {
let resolver = node.resolver().await?;
let tx = match input
.legacy_transaction(resolver.as_ref(), self.default_resolution_context())
.await
{
Ok(tx) => tx,
Err(err) => {
return Err(err);
}
};
match node.execute_transaction(tx).await {
Ok(receipt) => Ok(receipt),
Err(err) => Err(err),
}
}
}
}
#[instrument(level = "info", skip_all)]
async fn handle_input_call_frame_tracing(
&self,
tx_hash: TxHash,
node: &dyn EthereumNode,
) -> anyhow::Result<CallFrame> {
node.trace_transaction(
tx_hash,
GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
)),
tracer_config: GethDebugTracerConfig(serde_json::json! {{
"onlyTopCall": true,
"withLog": false,
"withStorage": false,
"withMemory": false,
"withStack": false,
"withReturnData": true
}}),
..Default::default()
},
)
.await
.map(|trace| {
trace
.try_into_call_frame()
.expect("Impossible - we requested a callframe trace so we must get it back")
})
}
#[instrument(level = "info", skip_all)]
fn handle_input_variable_assignment(
&mut self,
input: &FunctionCallStep,
tracing_result: &CallFrame,
) -> anyhow::Result<()> {
let Some(ref assignments) = input.variable_assignments else {
return Ok(());
};
// Handling the return data variable assignments.
for (variable_name, output_word) in assignments.return_data.iter().zip(
tracing_result
.output
.as_ref()
.unwrap_or_default()
.to_vec()
.chunks(32),
) {
let value = U256::from_be_slice(output_word);
self.variables.insert(variable_name.clone(), value);
tracing::info!(
variable_name,
variable_value = hex::encode(value.to_be_bytes::<32>()),
"Assigned variable"
);
}
Ok(())
}
#[instrument(level = "info", skip_all)]
async fn handle_input_expectations(
&self,
input: &FunctionCallStep,
execution_receipt: &TransactionReceipt,
resolver: &(impl ResolverApi + ?Sized),
tracing_result: &CallFrame,
) -> anyhow::Result<()> {
// Resolving the `input.expected` into a series of expectations that we can then assert on.
let mut expectations = match input {
FunctionCallStep {
expected: Some(Expected::Calldata(calldata)),
..
} => vec![ExpectedOutput::new().with_calldata(calldata.clone())],
FunctionCallStep {
expected: Some(Expected::Expected(expected)),
..
} => vec![expected.clone()],
FunctionCallStep {
expected: Some(Expected::ExpectedMany(expected)),
..
} => expected.clone(),
FunctionCallStep { 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;
}
}
futures::stream::iter(expectations.into_iter().map(Ok))
.try_for_each_concurrent(None, |expectation| async move {
self.handle_input_expectation_item(
execution_receipt,
resolver,
expectation,
tracing_result,
)
.await
})
.await
}
#[instrument(level = "info", skip_all)]
async fn handle_input_expectation_item(
&self,
execution_receipt: &TransactionReceipt,
resolver: &(impl ResolverApi + ?Sized),
expectation: ExpectedOutput,
tracing_result: &CallFrame,
) -> anyhow::Result<()> {
if let Some(ref version_requirement) = expectation.compiler_version {
if !version_requirement.matches(&self.compiler_version) {
return Ok(());
}
}
let resolution_context = self
.default_resolution_context()
.with_block_number(execution_receipt.block_number.as_ref())
.with_transaction_hash(&execution_receipt.transaction_hash);
// Handling the receipt state assertion.
let expected = !expectation.exception;
let actual = execution_receipt.status();
if actual != expected {
tracing::error!(
expected,
actual,
?execution_receipt,
?tracing_result,
"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, resolver, resolution_context)
.await
.context("Failed to resolve calldata equivalence for return data assertion")?
{
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 (event_idx, (expected_event, actual_event)) in expected_events
.iter()
.zip(execution_receipt.logs())
.enumerate()
{
// Handling the emitter assertion.
if let Some(ref expected_address) = expected_event.address {
let expected = expected_address
.resolve_address(resolver, resolution_context)
.await?;
let actual = actual_event.address();
if actual != expected {
tracing::error!(
event_idx,
%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, resolver, resolution_context)
.await
.context("Failed to resolve event topic equivalence")?
{
tracing::error!(
event_idx,
?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, resolver, resolution_context)
.await
.context("Failed to resolve event value equivalence")?
{
tracing::error!(
event_idx,
?execution_receipt,
?expected,
?actual,
"Event value assertion failed",
);
anyhow::bail!(
"Event value assertion failed - Expected {expected:?} but got {actual:?}",
);
}
}
}
Ok(())
}
#[instrument(level = "info", skip_all)]
async fn handle_input_diff(
&self,
tx_hash: TxHash,
node: &dyn EthereumNode,
) -> anyhow::Result<(GethTrace, DiffMode)> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let trace = node
.trace_transaction(tx_hash, trace_options)
.await
.context("Failed to obtain geth prestate tracer output")?;
let diff = node
.state_diff(tx_hash)
.await
.context("Failed to obtain state diff for transaction")?;
Ok((trace, diff))
}
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_contract_deployment(
&mut self,
metadata: &Metadata,
balance_assertion: &BalanceAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let Some(address) = balance_assertion.address.as_resolvable_address() else {
return Ok(());
};
let Some(instance) = address.strip_suffix(".address").map(ContractInstance::new) else {
return Ok(());
};
self.get_or_deploy_contract_instance(
&instance,
metadata,
FunctionCallStep::default_caller_address(),
None,
None,
node,
)
.await?;
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_execution(
&mut self,
BalanceAssertionStep {
address,
expected_balance: amount,
..
}: &BalanceAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let resolver = node.resolver().await?;
let address = address
.resolve_address(resolver.as_ref(), self.default_resolution_context())
.await?;
let balance = node.balance_of(address).await?;
let expected = *amount;
let actual = balance;
if expected != actual {
tracing::error!(%expected, %actual, %address, "Balance assertion failed");
anyhow::bail!(
"Balance assertion failed - Expected {} but got {} for {} resolved to {}",
expected,
actual,
address,
address,
)
}
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_contract_deployment(
&mut self,
metadata: &Metadata,
storage_empty_assertion: &StorageEmptyAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let Some(address) = storage_empty_assertion.address.as_resolvable_address() else {
return Ok(());
};
let Some(instance) = address.strip_suffix(".address").map(ContractInstance::new) else {
return Ok(());
};
self.get_or_deploy_contract_instance(
&instance,
metadata,
FunctionCallStep::default_caller_address(),
None,
None,
node,
)
.await?;
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_execution(
&mut self,
StorageEmptyAssertionStep {
address,
is_storage_empty,
..
}: &StorageEmptyAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let resolver = node.resolver().await?;
let address = address
.resolve_address(resolver.as_ref(), self.default_resolution_context())
.await?;
let storage = node.latest_state_proof(address, Default::default()).await?;
let is_empty = storage.storage_hash == EMPTY_ROOT_HASH;
let expected = is_storage_empty;
let actual = is_empty;
if *expected != actual {
tracing::error!(%expected, %actual, %address, "Storage Empty Assertion failed");
anyhow::bail!(
"Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}",
expected,
actual,
address,
address,
)
};
Ok(())
}
/// 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 async fn get_or_deploy_contract_instance(
&mut self,
contract_instance: &ContractInstance,
metadata: &Metadata,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
node: &dyn EthereumNode,
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
if let Some((_, address, abi)) = self.deployed_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 {
anyhow::bail!(
"Contract source not found for instance {:?}",
contract_instance
)
};
let Some((code, abi)) = self
.compiled_contracts
.get(&contract_source_path)
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
.cloned()
else {
anyhow::bail!(
"Failed to find information for contract {:?}",
contract_instance
)
};
let mut code = match alloy::hex::decode(&code) {
Ok(code) => code,
Err(error) => {
tracing::error!(
?error,
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
);
anyhow::bail!("Failed to hex-decode the byte code {}", error)
}
};
if let Some(calldata) = calldata {
let resolver = node.resolver().await?;
let calldata = calldata
.calldata(resolver.as_ref(), self.default_resolution_context())
.await?;
code.extend(calldata);
}
let tx = {
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)
};
let receipt = match node.execute_transaction(tx).await {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(?error, "Contract deployment transaction failed.");
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?contract_instance,
instance_address = ?address,
"Deployed contract"
);
self.execution_reporter
.report_contract_deployed_event(contract_instance.clone(), address)?;
self.deployed_contracts.insert(
contract_instance.clone(),
(contract_ident, address, abi.clone()),
);
Ok((address, abi, Some(receipt)))
}
fn default_resolution_context(&self) -> ResolutionContext<'_> {
ResolutionContext::default()
.with_deployed_contracts(&self.deployed_contracts)
.with_variables(&self.variables)
}
}
pub struct CaseDriver<'a> {
metadata: &'a Metadata,
case: &'a Case,
platform_state: Vec<(&'a dyn EthereumNode, PlatformIdentifier, CaseState)>,
}
impl<'a> CaseDriver<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
metadata: &'a Metadata,
case: &'a Case,
platform_state: Vec<(&'a dyn EthereumNode, PlatformIdentifier, CaseState)>,
) -> CaseDriver<'a> {
Self {
metadata,
case,
platform_state,
}
}
#[instrument(level = "info", name = "Executing Case", skip_all)]
pub async fn execute(&mut self) -> anyhow::Result<usize> {
let mut steps_executed = 0;
for (step_idx, step) in self
.case
.steps_iterator()
.enumerate()
.map(|(idx, v)| (StepIdx::new(idx), v))
{
let metadata = self.metadata;
let step_futures =
self.platform_state
.iter_mut()
.map(|(node, platform_id, case_state)| {
let platform_id = *platform_id;
let node_ref = *node;
let step = step.clone();
let span = info_span!(
"Handling Step",
%step_idx,
platform = %platform_id,
);
async move {
let step_path = StepPath::from_iterator([step_idx]);
case_state
.handle_step(metadata, &step, &step_path, node_ref)
.await
.map_err(|e| (platform_id, e))
}
.instrument(span)
});
match try_join_all(step_futures).await {
Ok(_outputs) => {
steps_executed += 1;
}
Err((platform_id, error)) => {
tracing::error!(
%step_idx,
platform = %platform_id,
?error,
"Step failed on platform",
);
return Err(error);
}
}
}
Ok(steps_executed)
}
}
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum StepOutput {
FunctionCall(TransactionReceipt, GethTrace, DiffMode),
BalanceAssertion,
StorageEmptyAssertion,
Repetition,
AccountAllocation,
}
@@ -5,7 +5,7 @@ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::{Arc, LazyLock},
}; };
use futures::FutureExt; use futures::FutureExt;
@@ -19,7 +19,7 @@ use anyhow::{Context as _, Error, Result};
use revive_dt_report::ExecutionSpecificReporter; use revive_dt_report::ExecutionSpecificReporter;
use semver::Version; use semver::Version;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock, Semaphore};
use tracing::{Instrument, debug, debug_span, instrument}; use tracing::{Instrument, debug, debug_span, instrument};
pub struct CachedCompiler<'a> { pub struct CachedCompiler<'a> {
@@ -165,10 +165,22 @@ impl<'a> CachedCompiler<'a> {
cache_value.compiler_output cache_value.compiler_output
} }
None => { None => {
compilation_callback() let compiler_output = compilation_callback()
.await .await
.context("Compilation callback failed (cache miss path)")? .context("Compilation callback failed (cache miss path)")?
.compiler_output .compiler_output;
self.artifacts_cache
.insert(
&cache_key,
&CacheValue {
compiler_output: compiler_output.clone(),
},
)
.await
.context(
"Failed to write the cached value of the compilation artifacts",
)?;
compiler_output
} }
} }
} }
@@ -186,6 +198,12 @@ async fn compile_contracts(
compiler: &dyn SolidityCompiler, compiler: &dyn SolidityCompiler,
reporter: &ExecutionSpecificReporter, reporter: &ExecutionSpecificReporter,
) -> Result<CompilerOutput> { ) -> Result<CompilerOutput> {
// Puts a limit on how many compilations we can perform at any given instance which helps us
// with some of the errors we've been seeing with high concurrency on MacOS (we have not tried
// it on Linux so we don't know if these issues also persist there or not.)
static SPAWN_GATE: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(100));
let _permit = SPAWN_GATE.acquire().await?;
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref()) let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
.with_allowed_extension("sol") .with_allowed_extension("sol")
.with_use_cached_fs(true) .with_use_cached_fs(true)
+33
View File
@@ -0,0 +1,33 @@
use revive_dt_config::CorpusConfiguration;
use revive_dt_format::{corpus::Corpus, metadata::MetadataFile};
use tracing::{info, info_span, instrument};
/// Given an object that implements [`AsRef<CorpusConfiguration>`], this function finds all of the
/// corpus files and produces a map containing all of the [`MetadataFile`]s discovered.
#[instrument(level = "debug", name = "Collecting Corpora", skip_all)]
pub fn collect_metadata_files(
context: impl AsRef<CorpusConfiguration>,
) -> anyhow::Result<Vec<MetadataFile>> {
let mut metadata_files = Vec::new();
let corpus_configuration = AsRef::<CorpusConfiguration>::as_ref(&context);
for path in &corpus_configuration.paths {
let span = info_span!("Processing corpus file", path = %path.display());
let _guard = span.enter();
let corpus = Corpus::try_from_path(path)?;
info!(
name = corpus.name(),
number_of_contained_paths = corpus.path_count(),
"Deserialized corpus file"
);
metadata_files.extend(corpus.enumerate_tests());
}
// There's a possibility that there are certain paths that all lead to the same metadata files
// and therefore it's important that we sort them and then deduplicate them.
metadata_files.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
metadata_files.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
Ok(metadata_files)
}
+9
View File
@@ -0,0 +1,9 @@
mod cached_compiler;
mod metadata;
mod pool;
mod test;
pub use cached_compiler::*;
pub use metadata::*;
pub use pool::*;
pub use test::*;
@@ -16,7 +16,7 @@ pub struct NodePool {
impl NodePool { impl NodePool {
/// Create a new Pool. This will start as many nodes as there are workers in `config`. /// Create a new Pool. This will start as many nodes as there are workers in `config`.
pub fn new(context: Context, platform: &dyn Platform) -> anyhow::Result<Self> { pub async fn new(context: Context, platform: &dyn Platform) -> anyhow::Result<Self> {
let concurrency_configuration = AsRef::<ConcurrencyConfiguration>::as_ref(&context); let concurrency_configuration = AsRef::<ConcurrencyConfiguration>::as_ref(&context);
let nodes = concurrency_configuration.number_of_nodes; let nodes = concurrency_configuration.number_of_nodes;
@@ -33,11 +33,18 @@ impl NodePool {
.join() .join()
.map_err(|error| anyhow::anyhow!("failed to spawn node: {:?}", error)) .map_err(|error| anyhow::anyhow!("failed to spawn node: {:?}", error))
.context("Failed to join node spawn thread")? .context("Failed to join node spawn thread")?
.map_err(|error| anyhow::anyhow!("node failed to spawn: {error}"))
.context("Node failed to spawn")?, .context("Node failed to spawn")?,
); );
} }
let pre_transactions_tasks = nodes
.iter_mut()
.map(|node| node.pre_transactions())
.collect::<Vec<_>>();
futures::future::try_join_all(pre_transactions_tasks)
.await
.context("Failed to run the pre-transactions task")?;
Ok(Self { Ok(Self {
nodes, nodes,
next: Default::default(), next: Default::default(),
+325
View File
@@ -0,0 +1,325 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{borrow::Cow, path::Path};
use futures::{Stream, StreamExt, stream};
use indexmap::{IndexMap, indexmap};
use revive_dt_common::iterators::EitherIter;
use revive_dt_common::types::PlatformIdentifier;
use revive_dt_config::Context;
use revive_dt_format::mode::ParsedMode;
use serde_json::{Value, json};
use revive_dt_compiler::Mode;
use revive_dt_compiler::SolidityCompiler;
use revive_dt_format::{
case::{Case, CaseIdx},
metadata::MetadataFile,
};
use revive_dt_node_interaction::EthereumNode;
use revive_dt_report::{ExecutionSpecificReporter, Reporter};
use revive_dt_report::{TestSpecificReporter, TestSpecifier};
use tracing::{debug, error, info};
use crate::Platform;
use crate::helpers::NodePool;
pub async fn create_test_definitions_stream<'a>(
// This is only required for creating the compiler objects and is not used anywhere else in the
// function.
context: &Context,
metadata_files: impl IntoIterator<Item = &'a MetadataFile>,
platforms_and_nodes: &'a BTreeMap<PlatformIdentifier, (&dyn Platform, NodePool)>,
reporter: Reporter,
) -> impl Stream<Item = TestDefinition<'a>> {
stream::iter(
metadata_files
.into_iter()
// Flatten over the cases.
.flat_map(|metadata_file| {
metadata_file
.cases
.iter()
.enumerate()
.map(move |(case_idx, case)| (metadata_file, case_idx, case))
})
// Flatten over the modes, prefer the case modes over the metadata file modes.
.flat_map(move |(metadata_file, case_idx, case)| {
let reporter = reporter.clone();
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
let modes = match modes {
Some(modes) => EitherIter::A(
ParsedMode::many_to_modes(modes.iter()).map(Cow::<'static, _>::Owned),
),
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
};
modes.into_iter().map(move |mode| {
(
metadata_file,
case_idx,
case,
mode.clone(),
reporter.test_specific_reporter(Arc::new(TestSpecifier {
solc_mode: mode.as_ref().clone(),
metadata_file_path: metadata_file.metadata_file_path.clone(),
case_idx: CaseIdx::new(case_idx),
})),
)
})
})
// Inform the reporter of each one of the test cases that were discovered which we expect to
// run.
.inspect(|(_, _, _, _, reporter)| {
reporter
.report_test_case_discovery_event()
.expect("Can't fail");
}),
)
// Creating the Test Definition objects from all of the various objects we have and creating
// their required dependencies (e.g., compiler).
.filter_map(
move |(metadata_file, case_idx, case, mode, reporter)| async move {
let mut platforms = BTreeMap::new();
for (platform, node_pool) in platforms_and_nodes.values() {
let node = node_pool.round_robbin();
let compiler = platform
.new_compiler(context.clone(), mode.version.clone().map(Into::into))
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Failed to instantiate the compiler"
)
})
.ok()?;
reporter
.report_node_assigned_event(
node.id(),
platform.platform_identifier(),
node.connection_string(),
)
.expect("Can't fail");
let reporter =
reporter.execution_specific_reporter(node.id(), platform.platform_identifier());
platforms.insert(
platform.platform_identifier(),
TestPlatformInformation {
platform: *platform,
node,
compiler,
reporter,
},
);
}
Some(TestDefinition {
/* Metadata file information */
metadata: metadata_file,
metadata_file_path: metadata_file.metadata_file_path.as_path(),
/* Mode Information */
mode: mode.clone(),
/* Case Information */
case_idx: CaseIdx::new(case_idx),
case,
/* Platform and Node Assignment Information */
platforms,
/* Reporter */
reporter,
})
},
)
// Filter out the test cases which are incompatible or that can't run in the current setup.
.filter_map(move |test| async move {
match test.check_compatibility() {
Ok(()) => Some(test),
Err((reason, additional_information)) => {
debug!(
metadata_file_path = %test.metadata.metadata_file_path.display(),
case_idx = %test.case_idx,
mode = %test.mode,
reason,
additional_information =
serde_json::to_string(&additional_information).unwrap(),
"Ignoring Test Case"
);
test.reporter
.report_test_ignored_event(
reason.to_string(),
additional_information
.into_iter()
.map(|(k, v)| (k.into(), v))
.collect::<IndexMap<_, _>>(),
)
.expect("Can't fail");
None
}
}
})
.inspect(|test| {
info!(
metadata_file_path = %test.metadata_file_path.display(),
case_idx = %test.case_idx,
mode = %test.mode,
"Created a test case definition"
);
})
}
/// This is a full description of a differential test to run alongside the full metadata file, the
/// specific case to be tested, the platforms that the tests should run on, the specific nodes of
/// these platforms that they should run on, the compilers to use, and everything else needed making
/// it a complete description.
pub struct TestDefinition<'a> {
/* Metadata file information */
pub metadata: &'a MetadataFile,
pub metadata_file_path: &'a Path,
/* Mode Information */
pub mode: Cow<'a, Mode>,
/* Case Information */
pub case_idx: CaseIdx,
pub case: &'a Case,
/* Platform and Node Assignment Information */
pub platforms: BTreeMap<PlatformIdentifier, TestPlatformInformation<'a>>,
/* Reporter */
pub reporter: TestSpecificReporter,
}
impl<'a> TestDefinition<'a> {
/// Checks if this test can be ran with the current configuration.
pub fn check_compatibility(&self) -> TestCheckFunctionResult {
self.check_metadata_file_ignored()?;
self.check_case_file_ignored()?;
self.check_target_compatibility()?;
self.check_evm_version_compatibility()?;
self.check_compiler_compatibility()?;
Ok(())
}
/// Checks if the metadata file is ignored or not.
fn check_metadata_file_ignored(&self) -> TestCheckFunctionResult {
if self.metadata.ignore.is_some_and(|ignore| ignore) {
Err(("Metadata file is ignored.", indexmap! {}))
} else {
Ok(())
}
}
/// Checks if the case file is ignored or not.
fn check_case_file_ignored(&self) -> TestCheckFunctionResult {
if self.case.ignore.is_some_and(|ignore| ignore) {
Err(("Case is ignored.", indexmap! {}))
} else {
Ok(())
}
}
/// Checks if the platforms all support the desired targets in the metadata file.
fn check_target_compatibility(&self) -> TestCheckFunctionResult {
let mut error_map = indexmap! {
"test_desired_targets" => json!(self.metadata.targets.as_ref()),
};
let mut is_allowed = true;
for (_, platform_information) in self.platforms.iter() {
let is_allowed_for_platform = match self.metadata.targets.as_ref() {
None => true,
Some(required_vm_identifiers) => {
required_vm_identifiers.contains(&platform_information.platform.vm_identifier())
}
};
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform_information.platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"One of the platforms do do not support the targets allowed by the test.",
error_map,
))
}
}
// Checks for the compatibility of the EVM version with the platforms specified.
fn check_evm_version_compatibility(&self) -> TestCheckFunctionResult {
let Some(evm_version_requirement) = self.metadata.required_evm_version else {
return Ok(());
};
let mut error_map = indexmap! {
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
};
let mut is_allowed = true;
for (_, platform_information) in self.platforms.iter() {
let is_allowed_for_platform =
evm_version_requirement.matches(&platform_information.node.evm_version());
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform_information.platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"EVM version is incompatible for the platforms specified",
error_map,
))
}
}
/// Checks if the platforms compilers support the mode that the test is for.
fn check_compiler_compatibility(&self) -> TestCheckFunctionResult {
let mut error_map = indexmap! {
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
};
let mut is_allowed = true;
for (_, platform_information) in self.platforms.iter() {
let is_allowed_for_platform = platform_information
.compiler
.supports_mode(self.mode.optimize_setting, self.mode.pipeline);
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform_information.platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"Compilers do not support this mode either for the provided platforms.",
error_map,
))
}
}
}
pub struct TestPlatformInformation<'a> {
pub platform: &'a dyn Platform,
pub node: &'a dyn EthereumNode,
pub compiler: Box<dyn SolidityCompiler>,
pub reporter: ExecutionSpecificReporter,
}
type TestCheckFunctionResult = Result<(), (&'static str, IndexMap<&'static str, Value>)>;
+3 -3
View File
@@ -14,13 +14,13 @@ use revive_dt_common::types::*;
use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc}; use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc};
use revive_dt_config::*; use revive_dt_config::*;
use revive_dt_node::{ use revive_dt_node::{
Node, geth::GethNode, lighthouse_geth::LighthouseGethNode, substrate::SubstrateNode, Node, node_implementations::geth::GethNode,
node_implementations::lighthouse_geth::LighthouseGethNode,
node_implementations::substrate::SubstrateNode,
}; };
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use tracing::info; use tracing::info;
pub mod driver;
/// A trait that describes the interface for the platforms that are supported by the tool. /// A trait that describes the interface for the platforms that are supported by the tool.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub trait Platform { pub trait Platform {
+38 -740
View File
@@ -1,55 +1,21 @@
mod cached_compiler; mod differential_benchmarks;
mod pool; mod differential_tests;
mod helpers;
use std::{
borrow::Cow,
collections::{BTreeSet, HashMap},
io::{BufWriter, Write, stderr},
path::Path,
sync::Arc,
time::Instant,
};
use alloy::{
network::{Ethereum, TransactionBuilder},
rpc::types::TransactionRequest,
};
use anyhow::Context as _;
use clap::Parser; use clap::Parser;
use futures::stream; use revive_dt_report::ReportAggregator;
use futures::{Stream, StreamExt};
use indexmap::{IndexMap, indexmap};
use revive_dt_node_interaction::EthereumNode;
use revive_dt_report::{
ExecutionSpecificReporter, ReportAggregator, Reporter, ReporterEvent, TestCaseStatus,
TestSpecificReporter, TestSpecifier,
};
use schemars::schema_for; use schemars::schema_for;
use serde_json::{Value, json}; use tracing::info;
use tokio::sync::Mutex;
use tracing::{debug, error, info, info_span, instrument};
use tracing_subscriber::{EnvFilter, FmtSubscriber}; use tracing_subscriber::{EnvFilter, FmtSubscriber};
use revive_dt_common::{ use revive_dt_config::Context;
iterators::EitherIter, use revive_dt_core::Platform;
types::{Mode, PrivateKeyAllocator}, use revive_dt_format::metadata::Metadata;
};
use revive_dt_compiler::SolidityCompiler;
use revive_dt_config::{Context, *};
use revive_dt_core::{
Platform,
driver::{CaseDriver, CaseState},
};
use revive_dt_format::{
case::{Case, CaseIdx},
corpus::Corpus,
metadata::{ContractPathAndIdent, Metadata, MetadataFile},
mode::ParsedMode,
steps::{FunctionCallStep, Step},
};
use crate::cached_compiler::CachedCompiler; use crate::{
use crate::pool::NodePool; differential_benchmarks::handle_differential_benchmarks,
differential_tests::handle_differential_tests,
};
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let (writer, _guard) = tracing_appender::non_blocking::NonBlockingBuilder::default() let (writer, _guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
@@ -75,37 +41,37 @@ fn main() -> anyhow::Result<()> {
let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task(); let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task();
match context { match context {
Context::ExecuteTests(context) => { Context::Test(context) => tokio::runtime::Builder::new_multi_thread()
let tests = collect_corpora(&context)
.context("Failed to collect corpus files from provided arguments")?
.into_iter()
.inspect(|(corpus, _)| {
reporter
.report_corpus_file_discovery_event(corpus.clone())
.expect("Can't fail")
})
.flat_map(|(_, files)| files.into_iter())
.inspect(|metadata_file| {
reporter
.report_metadata_file_discovery_event(
metadata_file.metadata_file_path.clone(),
metadata_file.content.clone(),
)
.expect("Can't fail")
})
.collect::<Vec<_>>();
tokio::runtime::Builder::new_multi_thread()
.worker_threads(context.concurrency_configuration.number_of_threads) .worker_threads(context.concurrency_configuration.number_of_threads)
.enable_all() .enable_all()
.build() .build()
.expect("Failed building the Runtime") .expect("Failed building the Runtime")
.block_on(async move { .block_on(async move {
execute_corpus(*context, &tests, reporter, report_aggregator_task) let differential_tests_handling_task =
.await handle_differential_tests(*context, reporter);
.context("Failed to execute corpus")
}) futures::future::try_join(differential_tests_handling_task, report_aggregator_task)
} .await?;
Ok(())
}),
Context::Benchmark(context) => tokio::runtime::Builder::new_multi_thread()
.worker_threads(context.concurrency_configuration.number_of_threads)
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(async move {
let differential_benchmarks_handling_task =
handle_differential_benchmarks(*context, reporter);
futures::future::try_join(
differential_benchmarks_handling_task,
report_aggregator_task,
)
.await?;
Ok(())
}),
Context::ExportJsonSchema => { Context::ExportJsonSchema => {
let schema = schema_for!(Metadata); let schema = schema_for!(Metadata);
println!("{}", serde_json::to_string_pretty(&schema).unwrap()); println!("{}", serde_json::to_string_pretty(&schema).unwrap());
@@ -113,671 +79,3 @@ fn main() -> anyhow::Result<()> {
} }
} }
} }
#[instrument(level = "debug", name = "Collecting Corpora", skip_all)]
fn collect_corpora(
context: &TestExecutionContext,
) -> anyhow::Result<HashMap<Corpus, Vec<MetadataFile>>> {
let mut corpora = HashMap::new();
for path in &context.corpus {
let span = info_span!("Processing corpus file", path = %path.display());
let _guard = span.enter();
let corpus = Corpus::try_from_path(path)?;
info!(
name = corpus.name(),
number_of_contained_paths = corpus.path_count(),
"Deserialized corpus file"
);
let tests = corpus.enumerate_tests();
corpora.insert(corpus, tests);
}
Ok(corpora)
}
async fn run_driver(
context: TestExecutionContext,
metadata_files: &[MetadataFile],
reporter: Reporter,
report_aggregator_task: impl Future<Output = anyhow::Result<()>>,
platforms: Vec<&dyn Platform>,
) -> anyhow::Result<()> {
let mut nodes = Vec::<(&dyn Platform, NodePool)>::new();
for platform in platforms.into_iter() {
let pool = NodePool::new(Context::ExecuteTests(Box::new(context.clone())), platform)
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Failed to initialize the node pool for the platform."
)
})
.context("Failed to initialize the node pool")?;
nodes.push((platform, pool));
}
let tests_stream = tests_stream(
&context,
metadata_files.iter(),
nodes.as_slice(),
reporter.clone(),
)
.await;
let driver_task = start_driver_task(&context, tests_stream)
.await
.context("Failed to start driver task")?;
let cli_reporting_task = start_cli_reporting_task(reporter);
let (_, _, rtn) = tokio::join!(cli_reporting_task, driver_task, report_aggregator_task);
rtn?;
Ok(())
}
async fn tests_stream<'a>(
args: &TestExecutionContext,
metadata_files: impl IntoIterator<Item = &'a MetadataFile> + Clone,
nodes: &'a [(&dyn Platform, NodePool)],
reporter: Reporter,
) -> impl Stream<Item = Test<'a>> {
let tests = metadata_files
.into_iter()
.flat_map(|metadata_file| {
metadata_file
.cases
.iter()
.enumerate()
.map(move |(case_idx, case)| (metadata_file, case_idx, case))
})
// Flatten over the modes, prefer the case modes over the metadata file modes.
.flat_map(|(metadata_file, case_idx, case)| {
let reporter = reporter.clone();
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
let modes = match modes {
Some(modes) => EitherIter::A(
ParsedMode::many_to_modes(modes.iter()).map(Cow::<'static, _>::Owned),
),
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
};
modes.into_iter().map(move |mode| {
(
metadata_file,
case_idx,
case,
mode.clone(),
reporter.test_specific_reporter(Arc::new(TestSpecifier {
solc_mode: mode.as_ref().clone(),
metadata_file_path: metadata_file.metadata_file_path.clone(),
case_idx: CaseIdx::new(case_idx),
})),
)
})
})
.collect::<Vec<_>>();
// Note: before we do any kind of filtering or process the iterator in any way, we need to
// inform the report aggregator of all of the cases that were found as it keeps a state of the
// test cases for its internal use.
for (_, _, _, _, reporter) in tests.iter() {
reporter
.report_test_case_discovery_event()
.expect("Can't fail")
}
stream::iter(tests.into_iter())
.filter_map(
move |(metadata_file, case_idx, case, mode, reporter)| async move {
let mut platforms = Vec::new();
for (platform, node_pool) in nodes.iter() {
let node = node_pool.round_robbin();
let compiler = platform
.new_compiler(
Context::ExecuteTests(Box::new(args.clone())),
mode.version.clone().map(Into::into),
)
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Failed to instantiate the compiler"
)
})
.ok()?;
let reporter = reporter
.execution_specific_reporter(node.id(), platform.platform_identifier());
platforms.push((*platform, node, compiler, reporter));
}
Some(Test {
metadata: metadata_file,
metadata_file_path: metadata_file.metadata_file_path.as_path(),
mode: mode.clone(),
case_idx: CaseIdx::new(case_idx),
case,
platforms,
reporter,
})
},
)
.filter_map(move |test| async move {
match test.check_compatibility() {
Ok(()) => Some(test),
Err((reason, additional_information)) => {
debug!(
metadata_file_path = %test.metadata.metadata_file_path.display(),
case_idx = %test.case_idx,
mode = %test.mode,
reason,
additional_information =
serde_json::to_string(&additional_information).unwrap(),
"Ignoring Test Case"
);
test.reporter
.report_test_ignored_event(
reason.to_string(),
additional_information
.into_iter()
.map(|(k, v)| (k.into(), v))
.collect::<IndexMap<_, _>>(),
)
.expect("Can't fail");
None
}
}
})
}
async fn start_driver_task<'a>(
context: &TestExecutionContext,
tests: impl Stream<Item = Test<'a>>,
) -> anyhow::Result<impl Future<Output = ()>> {
info!("Starting driver task");
let cached_compiler = Arc::new(
CachedCompiler::new(
context
.working_directory
.as_path()
.join("compilation_cache"),
context
.compilation_configuration
.invalidate_compilation_cache,
)
.await
.context("Failed to initialize cached compiler")?,
);
Ok(tests.for_each_concurrent(
context.concurrency_configuration.concurrency_limit(),
move |test| {
let cached_compiler = cached_compiler.clone();
async move {
for (platform, node, _, _) in test.platforms.iter() {
test.reporter
.report_node_assigned_event(
node.id(),
platform.platform_identifier(),
node.connection_string(),
)
.expect("Can't fail");
}
let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
context.wallet_configuration.highest_private_key_exclusive(),
)));
let reporter = test.reporter.clone();
let result =
handle_case_driver(&test, cached_compiler, private_key_allocator).await;
match result {
Ok(steps_executed) => reporter
.report_test_succeeded_event(steps_executed)
.expect("Can't fail"),
Err(error) => reporter
.report_test_failed_event(format!("{error:#}"))
.expect("Can't fail"),
}
}
},
))
}
#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)]
async fn start_cli_reporting_task(reporter: Reporter) {
let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail");
drop(reporter);
let start = Instant::now();
const GREEN: &str = "\x1B[32m";
const RED: &str = "\x1B[31m";
const GREY: &str = "\x1B[90m";
const COLOR_RESET: &str = "\x1B[0m";
const BOLD: &str = "\x1B[1m";
const BOLD_RESET: &str = "\x1B[22m";
let mut number_of_successes = 0;
let mut number_of_failures = 0;
let mut buf = BufWriter::new(stderr());
while let Ok(event) = aggregator_events_rx.recv().await {
let ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
metadata_file_path,
mode,
case_status,
} = event
else {
continue;
};
let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display());
for (case_idx, case_status) in case_status.into_iter() {
let _ = write!(buf, "\tCase Index {case_idx:>3}: ");
let _ = match case_status {
TestCaseStatus::Succeeded { steps_executed } => {
number_of_successes += 1;
writeln!(
buf,
"{}{}Case Succeeded{} - Steps Executed: {}{}",
GREEN, BOLD, BOLD_RESET, steps_executed, COLOR_RESET
)
}
TestCaseStatus::Failed { reason } => {
number_of_failures += 1;
writeln!(
buf,
"{}{}Case Failed{} - Reason: {}{}",
RED,
BOLD,
BOLD_RESET,
reason.trim(),
COLOR_RESET,
)
}
TestCaseStatus::Ignored { reason, .. } => writeln!(
buf,
"{}{}Case Ignored{} - Reason: {}{}",
GREY,
BOLD,
BOLD_RESET,
reason.trim(),
COLOR_RESET,
),
};
}
let _ = writeln!(buf);
}
// Summary at the end.
let _ = writeln!(
buf,
"{} cases: {}{}{} cases succeeded, {}{}{} cases failed in {} seconds",
number_of_successes + number_of_failures,
GREEN,
number_of_successes,
COLOR_RESET,
RED,
number_of_failures,
COLOR_RESET,
start.elapsed().as_secs()
);
}
#[allow(clippy::too_many_arguments)]
#[instrument(
level = "info",
name = "Handling Case"
skip_all,
fields(
metadata_file_path = %test.metadata.relative_path().display(),
mode = %test.mode,
case_idx = %test.case_idx,
case_name = test.case.name.as_deref().unwrap_or("Unnamed Case"),
)
)]
async fn handle_case_driver<'a>(
test: &Test<'a>,
cached_compiler: Arc<CachedCompiler<'a>>,
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
) -> anyhow::Result<usize> {
let platform_state = stream::iter(test.platforms.iter())
// Compiling the pre-link contracts.
.filter_map(|(platform, node, compiler, reporter)| {
let cached_compiler = cached_compiler.clone();
async move {
let compiler_output = cached_compiler
.compile_contracts(
test.metadata,
test.metadata_file_path,
test.mode.clone(),
None,
compiler.as_ref(),
*platform,
reporter,
)
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Pre-linking compilation failed"
)
})
.ok()?;
Some((test, platform, node, compiler, reporter, compiler_output))
}
})
// Deploying the libraries for the platform.
.filter_map(
|(test, platform, node, compiler, reporter, compiler_output)| async move {
let mut deployed_libraries = None::<HashMap<_, _>>;
let mut contract_sources = test
.metadata
.contract_sources()
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Failed to retrieve contract sources from metadata"
)
})
.ok()?;
for library_instance in test
.metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
debug!(%library_instance, "Deploying Library Instance");
let ContractPathAndIdent {
contract_source_path: library_source_path,
contract_ident: library_ident,
} = contract_sources.remove(library_instance)?;
let (code, abi) = compiler_output
.contracts
.get(&library_source_path)
.and_then(|contracts| contracts.get(library_ident.as_str()))?;
let code = alloy::hex::decode(code).ok()?;
// Getting the deployer address from the cases themselves. This is to ensure
// that we're doing the deployments from different accounts and therefore we're
// not slowed down by the nonce.
let deployer_address = test
.case
.steps
.iter()
.filter_map(|step| match step {
Step::FunctionCall(input) => input.caller.as_address().copied(),
Step::BalanceAssertion(..) => None,
Step::StorageEmptyAssertion(..) => None,
Step::Repeat(..) => None,
Step::AllocateAccount(..) => None,
})
.next()
.unwrap_or(FunctionCallStep::default_caller_address());
let tx = TransactionBuilder::<Ethereum>::with_deploy_code(
TransactionRequest::default().from(deployer_address),
code,
);
let receipt = node
.execute_transaction(tx)
.await
.inspect_err(|err| {
error!(
?err,
%library_instance,
platform_identifier = %platform.platform_identifier(),
"Failed to deploy the library"
)
})
.ok()?;
debug!(
?library_instance,
platform_identifier = %platform.platform_identifier(),
"Deployed library"
);
let library_address = receipt.contract_address?;
deployed_libraries.get_or_insert_default().insert(
library_instance.clone(),
(library_ident.clone(), library_address, abi.clone()),
);
}
Some((
test,
platform,
node,
compiler,
reporter,
compiler_output,
deployed_libraries,
))
},
)
// Compiling the post-link contracts.
.filter_map(
|(test, platform, node, compiler, reporter, _, deployed_libraries)| {
let cached_compiler = cached_compiler.clone();
let private_key_allocator = private_key_allocator.clone();
async move {
let compiler_output = cached_compiler
.compile_contracts(
test.metadata,
test.metadata_file_path,
test.mode.clone(),
deployed_libraries.as_ref(),
compiler.as_ref(),
*platform,
reporter,
)
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Pre-linking compilation failed"
)
})
.ok()?;
let case_state = CaseState::new(
compiler.version().clone(),
compiler_output.contracts,
deployed_libraries.unwrap_or_default(),
reporter.clone(),
private_key_allocator,
);
Some((*node, platform.platform_identifier(), case_state))
}
},
)
// Collect
.collect::<Vec<_>>()
.await;
let mut driver = CaseDriver::new(test.metadata, test.case, platform_state);
driver
.execute()
.await
.inspect(|steps_executed| info!(steps_executed, "Case succeeded"))
}
async fn execute_corpus(
context: TestExecutionContext,
tests: &[MetadataFile],
reporter: Reporter,
report_aggregator_task: impl Future<Output = anyhow::Result<()>>,
) -> anyhow::Result<()> {
let platforms = context
.platforms
.iter()
.copied()
.collect::<BTreeSet<_>>()
.into_iter()
.map(Into::<&dyn Platform>::into)
.collect::<Vec<_>>();
run_driver(context, tests, reporter, report_aggregator_task, platforms).await?;
Ok(())
}
/// this represents a single "test"; a mode, path and collection of cases.
#[allow(clippy::type_complexity)]
struct Test<'a> {
metadata: &'a MetadataFile,
metadata_file_path: &'a Path,
mode: Cow<'a, Mode>,
case_idx: CaseIdx,
case: &'a Case,
platforms: Vec<(
&'a dyn Platform,
&'a dyn EthereumNode,
Box<dyn SolidityCompiler>,
ExecutionSpecificReporter,
)>,
reporter: TestSpecificReporter,
}
impl<'a> Test<'a> {
/// Checks if this test can be ran with the current configuration.
pub fn check_compatibility(&self) -> TestCheckFunctionResult {
self.check_metadata_file_ignored()?;
self.check_case_file_ignored()?;
self.check_target_compatibility()?;
self.check_evm_version_compatibility()?;
self.check_compiler_compatibility()?;
Ok(())
}
/// Checks if the metadata file is ignored or not.
fn check_metadata_file_ignored(&self) -> TestCheckFunctionResult {
if self.metadata.ignore.is_some_and(|ignore| ignore) {
Err(("Metadata file is ignored.", indexmap! {}))
} else {
Ok(())
}
}
/// Checks if the case file is ignored or not.
fn check_case_file_ignored(&self) -> TestCheckFunctionResult {
if self.case.ignore.is_some_and(|ignore| ignore) {
Err(("Case is ignored.", indexmap! {}))
} else {
Ok(())
}
}
/// Checks if the platforms all support the desired targets in the metadata file.
fn check_target_compatibility(&self) -> TestCheckFunctionResult {
let mut error_map = indexmap! {
"test_desired_targets" => json!(self.metadata.targets.as_ref()),
};
let mut is_allowed = true;
for (platform, ..) in self.platforms.iter() {
let is_allowed_for_platform = match self.metadata.targets.as_ref() {
None => true,
Some(targets) => {
let mut target_matches = false;
for target in targets.iter() {
if &platform.vm_identifier() == target {
target_matches = true;
break;
}
}
target_matches
}
};
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"One of the platforms do do not support the targets allowed by the test.",
error_map,
))
}
}
// Checks for the compatibility of the EVM version with the platforms specified.
fn check_evm_version_compatibility(&self) -> TestCheckFunctionResult {
let Some(evm_version_requirement) = self.metadata.required_evm_version else {
return Ok(());
};
let mut error_map = indexmap! {
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
};
let mut is_allowed = true;
for (platform, node, ..) in self.platforms.iter() {
let is_allowed_for_platform = evm_version_requirement.matches(&node.evm_version());
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"EVM version is incompatible for the platforms specified",
error_map,
))
}
}
/// Checks if the platforms compilers support the mode that the test is for.
fn check_compiler_compatibility(&self) -> TestCheckFunctionResult {
let mut error_map = indexmap! {
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
};
let mut is_allowed = true;
for (platform, _, compiler, ..) in self.platforms.iter() {
let is_allowed_for_platform =
compiler.supports_mode(self.mode.optimize_setting, self.mode.pipeline);
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"Compilers do not support this mode either for the provided platforms.",
error_map,
))
}
}
}
type TestCheckFunctionResult = Result<(), (&'static str, IndexMap<&'static str, Value>)>;
-2
View File
@@ -14,8 +14,6 @@ revive-dt-common = { workspace = true }
revive-common = { workspace = true } revive-common = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
+1 -4
View File
@@ -3,10 +3,7 @@ use serde::{Deserialize, Serialize};
use revive_dt_common::{macros::define_wrapper_type, types::Mode}; use revive_dt_common::{macros::define_wrapper_type, types::Mode};
use crate::{ use crate::{mode::ParsedMode, steps::*};
mode::ParsedMode,
steps::{Expected, RepeatStep, Step},
};
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)] #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
pub struct Case { pub struct Case {
+6 -6
View File
@@ -1,5 +1,6 @@
use std::{collections::HashMap, fmt::Display, str::FromStr}; use std::{collections::HashMap, fmt::Display, str::FromStr};
use alloy::primitives::{FixedBytes, utils::parse_units};
use alloy::{ use alloy::{
eips::BlockNumberOrTag, eips::BlockNumberOrTag,
json_abi::Function, json_abi::Function,
@@ -7,7 +8,6 @@ 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 as _; use anyhow::Context as _;
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream}; use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
use schemars::JsonSchema; use schemars::JsonSchema;
@@ -537,7 +537,7 @@ impl FunctionCallStep {
} }
/// Parse this input into a legacy transaction. /// Parse this input into a legacy transaction.
pub async fn legacy_transaction( pub async fn as_transaction(
&self, &self,
resolver: &(impl ResolverApi + ?Sized), resolver: &(impl ResolverApi + ?Sized),
context: ResolutionContext<'_>, context: ResolutionContext<'_>,
@@ -959,9 +959,9 @@ impl<'de> Deserialize<'de> for EtherValue {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use alloy::primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
use alloy::sol_types::SolValue;
use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi}; use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi};
use alloy_primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
use alloy_sol_types::SolValue;
use std::{collections::HashMap, pin::Pin}; use std::{collections::HashMap, pin::Pin};
use super::*; use super::*;
@@ -1115,7 +1115,7 @@ mod tests {
let encoded = input.encoded_input(&resolver, context).await.unwrap(); let encoded = input.encoded_input(&resolver, context).await.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy::primitives::Address,);
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap(); let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
assert_eq!( assert_eq!(
decoded.0, decoded.0,
@@ -1162,7 +1162,7 @@ mod tests {
let encoded = input.encoded_input(&resolver, context).await.unwrap(); let encoded = input.encoded_input(&resolver, context).await.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy::primitives::Address,);
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap(); let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
assert_eq!( assert_eq!(
decoded.0, decoded.0,
+1 -1
View File
@@ -3,8 +3,8 @@ use std::pin::Pin;
use alloy::eips::BlockNumberOrTag; use alloy::eips::BlockNumberOrTag;
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use alloy::primitives::TxHash;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256}; use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy_primitives::TxHash;
use anyhow::Result; use anyhow::Result;
use crate::metadata::{ContractIdent, ContractInstance}; use crate::metadata::{ContractIdent, ContractInstance};
+1
View File
@@ -15,6 +15,7 @@ revive-dt-format = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
futures = { workspace = true }
[lints] [lints]
workspace = true workspace = true
+43 -1
View File
@@ -3,22 +3,36 @@
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use alloy::primitives::{Address, StorageKey, TxHash, U256}; use alloy::primitives::{Address, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace}; use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest}; use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
use anyhow::Result; use anyhow::Result;
use futures::Stream;
use revive_common::EVMVersion; use revive_common::EVMVersion;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
/// An interface for all interactions with Ethereum compatible nodes. /// An interface for all interactions with Ethereum compatible nodes.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub trait EthereumNode { pub trait EthereumNode {
/// A function to run post spawning the nodes and before any transactions are run on the node.
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>>;
fn id(&self) -> usize; fn id(&self) -> usize;
/// Returns the nodes connection string. /// Returns the nodes connection string.
fn connection_string(&self) -> &str; fn connection_string(&self) -> &str;
fn submit_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = Result<TxHash>> + '_>>;
fn get_receipt(
&self,
tx_hash: TxHash,
) -> Pin<Box<dyn Future<Output = Result<TransactionReceipt>> + '_>>;
/// Execute the [TransactionRequest] and return a [TransactionReceipt]. /// Execute the [TransactionRequest] and return a [TransactionReceipt].
fn execute_transaction( fn execute_transaction(
&self, &self,
@@ -50,4 +64,32 @@ pub trait EthereumNode {
/// Returns the EVM version of the node. /// Returns the EVM version of the node.
fn evm_version(&self) -> EVMVersion; fn evm_version(&self) -> EVMVersion;
/// Returns a stream of the blocks that were mined by the node.
fn subscribe_to_full_blocks_information(
&self,
) -> Pin<
Box<
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
+ '_,
>,
>;
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MinedBlockInformation {
/// The block number.
pub block_number: BlockNumber,
/// The block timestamp.
pub block_timestamp: BlockTimestamp,
/// The amount of gas mined in the block.
pub mined_gas: u128,
/// The gas limit of the block.
pub block_gas_limit: u128,
/// The hashes of the transactions that were mined as part of the block.
pub transaction_hashes: Vec<TxHash>,
} }
+2
View File
@@ -11,7 +11,9 @@ rust-version.workspace = true
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tower = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
revive-common = { workspace = true } revive-common = { workspace = true }
+5
View File
@@ -1,5 +1,10 @@
use alloy::primitives::ChainId;
/// This constant defines how much Wei accounts are pre-seeded with in genesis. /// This constant defines how much Wei accounts are pre-seeded with in genesis.
/// ///
/// Note: After changing this number, check that the tests for substrate work as we encountered /// Note: After changing this number, check that the tests for substrate work as we encountered
/// some issues with different values of the initial balance on substrate. /// some issues with different values of the initial balance on substrate.
pub const INITIAL_BALANCE: u128 = 10u128.pow(37); pub const INITIAL_BALANCE: u128 = 10u128.pow(37);
/// The chain id used for all of the chains spawned by the framework.
pub const CHAIN_ID: ChainId = 420420420;
+3
View File
@@ -0,0 +1,3 @@
mod process;
pub use process::*;
@@ -110,8 +110,11 @@ impl Process {
} }
let check_result = let check_result =
check_function(stdout_line.as_deref(), stderr_line.as_deref()) check_function(stdout_line.as_deref(), stderr_line.as_deref()).context(
.context("Failed to wait for the process to be ready")?; format!(
"Failed to wait for the process to be ready - {stdout} - {stderr}"
),
)?;
if check_result { if check_result {
break; break;
@@ -127,10 +130,10 @@ impl Process {
ProcessReadinessWaitBehavior::WaitForCommandToExit => { ProcessReadinessWaitBehavior::WaitForCommandToExit => {
if !child if !child
.wait() .wait()
.context("Failed waiting for kurtosis run process to finish")? .context("Failed waiting for process to finish")?
.success() .success()
{ {
anyhow::bail!("Failed to initialize kurtosis network",); anyhow::bail!("Failed to spawn command");
} }
} }
} }
+3 -5
View File
@@ -3,12 +3,10 @@
use alloy::genesis::Genesis; use alloy::genesis::Genesis;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
pub mod common;
pub mod constants; pub mod constants;
pub mod geth; pub mod helpers;
pub mod lighthouse_geth; pub mod node_implementations;
pub mod process; pub mod provider_utils;
pub mod substrate;
/// An abstract interface for testing nodes. /// An abstract interface for testing nodes.
pub trait Node: EthereumNode { pub trait Node: EthereumNode {
@@ -20,18 +20,22 @@ use alloy::{
network::{Ethereum, EthereumWallet, NetworkWallet}, network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256}, primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
providers::{ providers::{
Provider, ProviderBuilder, Provider,
ext::DebugApi, ext::DebugApi,
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, NonceFiller},
}, },
rpc::types::{ rpc::types::{
EIP1186AccountProofResponse, TransactionRequest, EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{
DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame,
},
}, },
}; };
use anyhow::Context as _; use anyhow::Context as _;
use futures::{Stream, StreamExt};
use revive_common::EVMVersion; use revive_common::EVMVersion;
use tracing::{Instrument, instrument}; use tokio::sync::OnceCell;
use tracing::{Instrument, error, instrument};
use revive_dt_common::{ use revive_dt_common::{
fs::clear_directory, fs::clear_directory,
@@ -39,13 +43,13 @@ use revive_dt_common::{
}; };
use revive_dt_config::*; use revive_dt_config::*;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
use crate::{ use crate::{
Node, Node,
common::FallbackGasFiller, constants::{CHAIN_ID, INITIAL_BALANCE},
constants::INITIAL_BALANCE, helpers::{Process, ProcessReadinessWaitBehavior},
process::{Process, ProcessReadinessWaitBehavior}, provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
}; };
static NODE_COUNT: AtomicU32 = AtomicU32::new(0); static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -70,7 +74,7 @@ pub struct GethNode {
start_timeout: Duration, start_timeout: Duration,
wallet: Arc<EthereumWallet>, wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager, nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller, provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
} }
impl GethNode { impl GethNode {
@@ -119,8 +123,8 @@ impl GethNode {
handle: None, handle: None,
start_timeout: geth_configuration.start_timeout_ms, start_timeout: geth_configuration.start_timeout_ms,
wallet: wallet.clone(), wallet: wallet.clone(),
chain_id_filler: Default::default(),
nonce_manager: Default::default(), nonce_manager: Default::default(),
provider: Default::default(),
} }
} }
@@ -235,7 +239,7 @@ impl GethNode {
match process { match process {
Ok(process) => self.handle = Some(process), Ok(process) => self.handle = Some(process),
Err(err) => { Err(err) => {
tracing::error!(?err, "Failed to start geth, shutting down gracefully"); error!(?err, "Failed to start geth, shutting down gracefully");
self.shutdown() self.shutdown()
.context("Failed to gracefully shutdown after geth start error")?; .context("Failed to gracefully shutdown after geth start error")?;
return Err(err); return Err(err);
@@ -245,27 +249,29 @@ impl GethNode {
Ok(self) Ok(self)
} }
async fn provider( async fn provider(&self) -> anyhow::Result<ConcreteProvider<Ethereum, Arc<EthereumWallet>>> {
&self, self.provider
) -> anyhow::Result<FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>> .get_or_try_init(|| async move {
{ construct_concurrency_limited_provider::<Ethereum, _>(
ProviderBuilder::new() self.connection_string.as_str(),
.disable_recommended_fillers() FallbackGasFiller::default(),
.filler(FallbackGasFiller::new( ChainIdFiller::new(Some(CHAIN_ID)),
25_000_000, NonceFiller::new(self.nonce_manager.clone()),
1_000_000_000, self.wallet.clone(),
1_000_000_000, )
))
.filler(self.chain_id_filler.clone())
.filler(NonceFiller::new(self.nonce_manager.clone()))
.wallet(self.wallet.clone())
.connect(&self.connection_string)
.await .await
.map_err(Into::into) .context("Failed to construct the provider")
})
.await
.cloned()
} }
} }
impl EthereumNode for GethNode { impl EthereumNode for GethNode {
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>> {
Box::pin(async move { Ok(()) })
}
fn id(&self) -> usize { fn id(&self) -> usize {
self.id as _ self.id as _
} }
@@ -274,6 +280,50 @@ impl EthereumNode for GethNode {
&self.connection_string &self.connection_string
} }
#[instrument(
level = "info",
skip_all,
fields(geth_node_id = self.id, connection_string = self.connection_string),
err,
)]
fn submit_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TxHash>> + '_>> {
Box::pin(async move {
let provider = self
.provider()
.await
.context("Failed to create the provider for transaction submission")?;
let pending_transaction = provider
.send_transaction(transaction)
.await
.context("Failed to submit the transaction through the provider")?;
Ok(*pending_transaction.tx_hash())
})
}
#[instrument(
level = "info",
skip_all,
fields(geth_node_id = self.id, connection_string = self.connection_string),
err,
)]
fn get_receipt(
&self,
tx_hash: TxHash,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move {
self.provider()
.await
.context("Failed to create provider for getting the receipt")?
.get_transaction_receipt(tx_hash)
.await
.context("Failed to get the receipt of the transaction")?
.context("Failed to get the receipt of the transaction")
})
}
#[instrument( #[instrument(
level = "info", level = "info",
skip_all, skip_all,
@@ -283,8 +333,7 @@ impl EthereumNode for GethNode {
fn execute_transaction( fn execute_transaction(
&self, &self,
transaction: TransactionRequest, transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::TransactionReceipt>> + '_>> ) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
{
Box::pin(async move { Box::pin(async move {
let provider = self let provider = self
.provider() .provider()
@@ -295,7 +344,7 @@ impl EthereumNode for GethNode {
.send_transaction(transaction) .send_transaction(transaction)
.await .await
.inspect_err( .inspect_err(
|err| tracing::error!(%err, "Encountered an error when submitting the transaction"), |err| error!(%err, "Encountered an error when submitting the transaction"),
) )
.context("Failed to submit transaction to geth node")?; .context("Failed to submit transaction to geth node")?;
let transaction_hash = *pending_transaction.tx_hash(); let transaction_hash = *pending_transaction.tx_hash();
@@ -317,7 +366,6 @@ impl EthereumNode for GethNode {
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for // allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential // a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
// backoff each time we attempt to get the receipt and find that it's not available. // backoff each time we attempt to get the receipt and find that it's not available.
let provider = Arc::new(provider);
poll( poll(
Self::RECEIPT_POLLING_DURATION, Self::RECEIPT_POLLING_DURATION,
PollingWaitBehavior::Constant(Duration::from_millis(200)), PollingWaitBehavior::Constant(Duration::from_millis(200)),
@@ -351,14 +399,12 @@ impl EthereumNode for GethNode {
&self, &self,
tx_hash: TxHash, tx_hash: TxHash,
trace_options: GethDebugTracingOptions, trace_options: GethDebugTracingOptions,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::trace::geth::GethTrace>> + '_>> ) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
{
Box::pin(async move { Box::pin(async move {
let provider = Arc::new( let provider = self
self.provider() .provider()
.await .await
.context("Failed to create provider for tracing")?, .context("Failed to create provider for tracing")?;
);
poll( poll(
Self::TRACE_POLLING_DURATION, Self::TRACE_POLLING_DURATION,
PollingWaitBehavior::Constant(Duration::from_millis(200)), PollingWaitBehavior::Constant(Duration::from_millis(200)),
@@ -456,14 +502,54 @@ impl EthereumNode for GethNode {
fn evm_version(&self) -> EVMVersion { fn evm_version(&self) -> EVMVersion {
EVMVersion::Cancun EVMVersion::Cancun
} }
fn subscribe_to_full_blocks_information(
&self,
) -> Pin<
Box<
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
+ '_,
>,
> {
Box::pin(async move {
let provider = self
.provider()
.await
.context("Failed to create the provider for block subscription")?;
let block_subscription = provider.subscribe_full_blocks();
let block_stream = block_subscription
.into_stream()
.await
.context("Failed to create the block stream")?;
let mined_block_information_stream = block_stream.filter_map(|block| async {
let block = block.ok()?;
Some(MinedBlockInformation {
block_number: block.number(),
block_timestamp: block.header.timestamp,
mined_gas: block.header.gas_used as _,
block_gas_limit: block.header.gas_limit as _,
transaction_hashes: block
.transactions
.into_hashes()
.as_hashes()
.expect("Must be hashes")
.to_vec(),
})
});
Ok(Box::pin(mined_block_information_stream)
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
})
}
} }
pub struct GethNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereum>> { pub struct GethNodeResolver {
id: u32, id: u32,
provider: FillProvider<F, P, Ethereum>, provider: ConcreteProvider<Ethereum, Arc<EthereumWallet>>,
} }
impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi for GethNodeResolver<F, P> { impl ResolverApi for GethNodeResolver {
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn chain_id( fn chain_id(
&self, &self,
@@ -648,12 +734,38 @@ mod tests {
(context, node) (context, node)
} }
fn shared_state() -> &'static (TestExecutionContext, GethNode) {
static STATE: LazyLock<(TestExecutionContext, GethNode)> = LazyLock::new(new_node);
&STATE
}
fn shared_node() -> &'static GethNode { fn shared_node() -> &'static GethNode {
static NODE: LazyLock<(TestExecutionContext, GethNode)> = LazyLock::new(new_node); &shared_state().1
&NODE.1 }
#[tokio::test]
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange
let (context, node) = shared_state();
let account_address = context
.wallet_configuration
.wallet()
.default_signer()
.address();
let transaction = TransactionRequest::default()
.to(account_address)
.value(U256::from(100_000_000_000_000u128));
// Act
let receipt = node.execute_transaction(transaction).await;
// Assert
let _ = receipt.expect("Failed to get the receipt for the transfer");
} }
#[test] #[test]
#[ignore = "Ignored since they take a long time to run"]
fn version_works() { fn version_works() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -670,6 +782,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_chain_id_from_node() { async fn can_get_chain_id_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -683,6 +796,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_gas_limit_from_node() { async fn can_get_gas_limit_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -700,6 +814,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_coinbase_from_node() { async fn can_get_coinbase_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -717,6 +832,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_difficulty_from_node() { async fn can_get_block_difficulty_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -734,6 +850,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_hash_from_node() { async fn can_get_block_hash_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -751,6 +868,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_timestamp_from_node() { async fn can_get_block_timestamp_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -768,6 +886,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_number_from_node() { async fn can_get_block_number_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -9,7 +9,7 @@
//! that the tool has. //! that the tool has.
use std::{ use std::{
collections::BTreeMap, collections::{BTreeMap, HashSet},
fs::{File, create_dir_all}, fs::{File, create_dir_all},
io::Read, io::Read,
ops::ControlFlow, ops::ControlFlow,
@@ -17,7 +17,7 @@ use std::{
pin::Pin, pin::Pin,
process::{Command, Stdio}, process::{Command, Stdio},
sync::{ sync::{
Arc, Arc, LazyLock,
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
}, },
time::{Duration, SystemTime, UNIX_EPOCH}, time::{Duration, SystemTime, UNIX_EPOCH},
@@ -31,20 +31,24 @@ use alloy::{
Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256, address, Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256, address,
}, },
providers::{ providers::{
Provider, ProviderBuilder, Provider,
ext::DebugApi, ext::DebugApi,
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
}, },
rpc::types::{ rpc::types::{
EIP1186AccountProofResponse, TransactionRequest, EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{
DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame,
},
}, },
}; };
use anyhow::Context as _; use anyhow::Context as _;
use futures::{Stream, StreamExt};
use revive_common::EVMVersion; use revive_common::EVMVersion;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::serde_as; use serde_with::serde_as;
use tracing::{Instrument, instrument}; use tokio::sync::{OnceCell, Semaphore};
use tracing::{Instrument, info, instrument};
use revive_dt_common::{ use revive_dt_common::{
fs::clear_directory, fs::clear_directory,
@@ -52,13 +56,13 @@ use revive_dt_common::{
}; };
use revive_dt_config::*; use revive_dt_config::*;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
use crate::{ use crate::{
Node, Node,
common::FallbackGasFiller, constants::{CHAIN_ID, INITIAL_BALANCE},
constants::INITIAL_BALANCE, helpers::{Process, ProcessReadinessWaitBehavior},
process::{Process, ProcessReadinessWaitBehavior}, provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
}; };
static NODE_COUNT: AtomicU32 = AtomicU32::new(0); static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -75,7 +79,8 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
pub struct LighthouseGethNode { pub struct LighthouseGethNode {
/* Node Identifier */ /* Node Identifier */
id: u32, id: u32,
connection_string: String, ws_connection_string: String,
http_connection_string: String,
enclave_name: String, enclave_name: String,
/* Directory Paths */ /* Directory Paths */
@@ -91,17 +96,22 @@ pub struct LighthouseGethNode {
/* Spawned Processes */ /* Spawned Processes */
process: Option<Process>, process: Option<Process>,
/* Prefunded Account Information */
prefunded_account_address: Address,
/* Provider Related Fields */ /* Provider Related Fields */
wallet: Arc<EthereumWallet>, wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager, nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
persistent_http_provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
persistent_ws_provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
http_provider_requests_semaphore: LazyLock<Semaphore>,
} }
impl LighthouseGethNode { impl LighthouseGethNode {
const BASE_DIRECTORY: &str = "lighthouse"; const BASE_DIRECTORY: &str = "lighthouse";
const LOGS_DIRECTORY: &str = "logs"; const LOGS_DIRECTORY: &str = "logs";
const IPC_FILE_NAME: &str = "geth.ipc";
const CONFIG_FILE_NAME: &str = "config.yaml"; const CONFIG_FILE_NAME: &str = "config.yaml";
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress"; const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
@@ -134,10 +144,8 @@ impl LighthouseGethNode {
Self { Self {
/* Node Identifier */ /* Node Identifier */
id, id,
connection_string: base_directory ws_connection_string: String::default(),
.join(Self::IPC_FILE_NAME) http_connection_string: String::default(),
.display()
.to_string(),
enclave_name: format!( enclave_name: format!(
"enclave-{}-{}", "enclave-{}-{}",
SystemTime::now() SystemTime::now()
@@ -160,15 +168,20 @@ impl LighthouseGethNode {
/* Spawned Processes */ /* Spawned Processes */
process: None, process: None,
/* Prefunded Account Information */
prefunded_account_address: wallet.default_signer().address(),
/* Provider Related Fields */ /* Provider Related Fields */
wallet: wallet.clone(), wallet: wallet.clone(),
nonce_manager: Default::default(), nonce_manager: Default::default(),
chain_id_filler: Default::default(), persistent_http_provider: OnceCell::const_new(),
persistent_ws_provider: OnceCell::const_new(),
http_provider_requests_semaphore: LazyLock::new(|| Semaphore::const_new(500)),
} }
} }
/// Create the node directory and call `geth init` to configure the genesis. /// Create the node directory and call `geth init` to configure the genesis.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn init(&mut self, _: Genesis) -> anyhow::Result<&mut Self> { fn init(&mut self, _: Genesis) -> anyhow::Result<&mut Self> {
self.init_directories() self.init_directories()
.context("Failed to initialize the directories of the Lighthouse Geth node.")?; .context("Failed to initialize the directories of the Lighthouse Geth node.")?;
@@ -198,10 +211,12 @@ impl LighthouseGethNode {
execution_layer_extra_parameters: vec![ execution_layer_extra_parameters: vec![
"--nodiscover".to_string(), "--nodiscover".to_string(),
"--cache=4096".to_string(), "--cache=4096".to_string(),
"--txpool.globalslots=100000".to_string(), "--txlookuplimit=0".to_string(),
"--txpool.globalqueue=100000".to_string(), "--gcmode=archive".to_string(),
"--txpool.accountslots=128".to_string(), "--txpool.globalslots=500000".to_string(),
"--txpool.accountqueue=1024".to_string(), "--txpool.globalqueue=500000".to_string(),
"--txpool.accountslots=32768".to_string(),
"--txpool.accountqueue=32768".to_string(),
"--http.api=admin,engine,net,eth,web3,debug,txpool".to_string(), "--http.api=admin,engine,net,eth,web3,debug,txpool".to_string(),
"--http.addr=0.0.0.0".to_string(), "--http.addr=0.0.0.0".to_string(),
"--ws".to_string(), "--ws".to_string(),
@@ -211,13 +226,14 @@ impl LighthouseGethNode {
"--ws.origins=*".to_string(), "--ws.origins=*".to_string(),
], ],
consensus_layer_extra_parameters: vec![ consensus_layer_extra_parameters: vec![
"--disable-quic".to_string(),
"--disable-deposit-contract-sync".to_string(), "--disable-deposit-contract-sync".to_string(),
], ],
}], }],
network_parameters: NetworkParameters { network_parameters: NetworkParameters {
preset: NetworkPreset::Mainnet, preset: NetworkPreset::Mainnet,
seconds_per_slot: 12, seconds_per_slot: 12,
network_id: 420420420, network_id: CHAIN_ID,
deposit_contract_address: address!("0x00000000219ab540356cBB839Cbe05303d7705Fa"), deposit_contract_address: address!("0x00000000219ab540356cBB839Cbe05303d7705Fa"),
altair_fork_epoch: 0, altair_fork_epoch: 0,
bellatrix_fork_epoch: 0, bellatrix_fork_epoch: 0,
@@ -228,14 +244,8 @@ impl LighthouseGethNode {
num_validator_keys_per_node: 64, num_validator_keys_per_node: 64,
genesis_delay: 10, genesis_delay: 10,
prefunded_accounts: { prefunded_accounts: {
let map = NetworkWallet::<Ethereum>::signer_addresses(&self.wallet) let map = std::iter::once(self.prefunded_account_address)
.map(|address| { .map(|address| (address, GenesisAccount::default().with_balance(U256::MAX)))
(
address,
GenesisAccount::default()
.with_balance(INITIAL_BALANCE.try_into().unwrap()),
)
})
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
serde_json::to_string(&map).unwrap() serde_json::to_string(&map).unwrap()
}, },
@@ -248,7 +258,12 @@ impl LighthouseGethNode {
public_port_start: Some(32000 + self.id as u16 * 1000), public_port_start: Some(32000 + self.id as u16 * 1000),
}, },
), ),
consensus_layer_port_publisher_parameters: Default::default(), consensus_layer_port_publisher_parameters: Some(
PortPublisherSingleItemParameters {
enabled: Some(true),
public_port_start: Some(59010 + self.id as u16 * 50),
},
),
}), }),
}; };
@@ -261,7 +276,7 @@ impl LighthouseGethNode {
} }
/// Spawn the go-ethereum node child process. /// Spawn the go-ethereum node child process.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn spawn_process(&mut self) -> anyhow::Result<&mut Self> { fn spawn_process(&mut self) -> anyhow::Result<&mut Self> {
let process = Process::new( let process = Process::new(
None, None,
@@ -292,6 +307,7 @@ impl LighthouseGethNode {
}), }),
}, },
) )
.context("Failed to spawn the kurtosis enclave")
.inspect_err(|err| { .inspect_err(|err| {
tracing::error!(?err, "Failed to spawn Kurtosis"); tracing::error!(?err, "Failed to spawn Kurtosis");
self.shutdown().expect("Failed to shutdown kurtosis"); self.shutdown().expect("Failed to shutdown kurtosis");
@@ -316,64 +332,157 @@ impl LighthouseGethNode {
stdout stdout
}; };
self.connection_string = stdout self.http_connection_string = stdout
.split("el-1-geth-lighthouse")
.nth(1)
.and_then(|str| str.split(" rpc").nth(1))
.and_then(|str| str.split("->").nth(1))
.and_then(|str| str.split("\n").next())
.and_then(|str| str.trim().split(" ").next())
.map(|str| format!("http://{}", str.trim()))
.context("Failed to find the HTTP connection string of Kurtosis")?;
self.ws_connection_string = stdout
.split("el-1-geth-lighthouse") .split("el-1-geth-lighthouse")
.nth(1) .nth(1)
.and_then(|str| str.split("ws").nth(1)) .and_then(|str| str.split("ws").nth(1))
.and_then(|str| str.split("->").nth(1)) .and_then(|str| str.split("->").nth(1))
.and_then(|str| str.split("\n").next()) .and_then(|str| str.split("\n").next())
.and_then(|str| str.trim().split(" ").next())
.map(|str| format!("ws://{}", str.trim())) .map(|str| format!("ws://{}", str.trim()))
.context("Failed to find the WS connection string of Kurtosis")?; .context("Failed to find the WS connection string of Kurtosis")?;
info!(
http_connection_string = self.http_connection_string,
ws_connection_string = self.ws_connection_string,
"Discovered the connection strings for the node"
);
Ok(self) Ok(self)
} }
async fn provider(
&self,
) -> anyhow::Result<FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>>
{
ProviderBuilder::new()
.disable_recommended_fillers()
.filler(FallbackGasFiller::new(
25_000_000,
1_000_000_000,
1_000_000_000,
))
.filler(self.chain_id_filler.clone())
.filler(NonceFiller::new(self.nonce_manager.clone()))
.wallet(self.wallet.clone())
.connect(&self.connection_string)
.await
.context("Failed to create the provider for Kurtosis")
}
}
impl EthereumNode for LighthouseGethNode {
fn id(&self) -> usize {
self.id as _
}
fn connection_string(&self) -> &str {
&self.connection_string
}
#[instrument( #[instrument(
level = "info", level = "info",
skip_all, skip_all,
fields(geth_node_id = self.id, connection_string = self.connection_string), fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string),
err, err(Debug),
)] )]
fn execute_transaction( #[allow(clippy::type_complexity)]
&self, async fn ws_provider(&self) -> anyhow::Result<ConcreteProvider<Ethereum, Arc<EthereumWallet>>> {
transaction: TransactionRequest, self.persistent_ws_provider
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::TransactionReceipt>> + '_>> .get_or_try_init(|| async move {
{ construct_concurrency_limited_provider::<Ethereum, _>(
Box::pin(async move { self.ws_connection_string.as_str(),
let provider = self FallbackGasFiller::default(),
.provider() ChainIdFiller::new(Some(CHAIN_ID)),
NonceFiller::new(self.nonce_manager.clone()),
self.wallet.clone(),
)
.await .await
.context("Failed to create provider for transaction submission")?; .context("Failed to construct the provider")
})
.await
.cloned()
}
#[instrument(
level = "info",
skip_all,
fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string),
err(Debug),
)]
#[allow(clippy::type_complexity)]
async fn http_provider(
&self,
) -> anyhow::Result<ConcreteProvider<Ethereum, Arc<EthereumWallet>>> {
self.persistent_http_provider
.get_or_try_init(|| async move {
construct_concurrency_limited_provider::<Ethereum, _>(
self.http_connection_string.as_str(),
FallbackGasFiller::default(),
ChainIdFiller::new(Some(CHAIN_ID)),
NonceFiller::new(self.nonce_manager.clone()),
self.wallet.clone(),
)
.await
.context("Failed to construct the provider")
})
.await
.cloned()
}
/// Funds all of the accounts in the Ethereum wallet from the initially funded account.
#[instrument(
level = "info",
skip_all,
fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string),
err(Debug),
)]
async fn fund_all_accounts(&self) -> anyhow::Result<()> {
let mut full_block_subscriber = self
.ws_provider()
.await
.context("Failed to create the WS provider")?
.subscribe_full_blocks()
.into_stream()
.await
.context("Full block subscriber")?;
let mut tx_hashes = futures::future::try_join_all(
NetworkWallet::<Ethereum>::signer_addresses(self.wallet.as_ref())
.enumerate()
.map(|(nonce, address)| async move {
let mut transaction = TransactionRequest::default()
.from(self.prefunded_account_address)
.to(address)
.nonce(nonce as _)
.value(INITIAL_BALANCE.try_into().unwrap());
transaction.chain_id = Some(CHAIN_ID);
self.submit_transaction(transaction).await
}),
)
.await
.context("Failed to submit all transactions")?
.into_iter()
.collect::<HashSet<_>>();
while let Some(block) = full_block_subscriber.next().await {
let Ok(block) = block else {
continue;
};
let block_number = block.number();
let block_timestamp = block.header.timestamp;
let block_transaction_count = block.transactions.len();
for hash in block.transactions.into_hashes().as_hashes().unwrap() {
tx_hashes.remove(hash);
}
info!(
block.number = block_number,
block.timestamp = block_timestamp,
block.transaction_count = block_transaction_count,
remaining_transactions = tx_hashes.len(),
"Discovered new block when funding accounts"
);
if tx_hashes.is_empty() {
break;
}
}
Ok(())
}
fn internal_execute_transaction<'a>(
transaction: TransactionRequest,
provider: FillProvider<
impl TxFiller<Ethereum> + 'a,
impl Provider<Ethereum> + Clone + 'a,
Ethereum,
>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + 'a>> {
Box::pin(async move {
let pending_transaction = provider let pending_transaction = provider
.send_transaction(transaction) .send_transaction(transaction)
.await .await
@@ -404,10 +513,9 @@ impl EthereumNode for LighthouseGethNode {
// allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting // allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting
// with exponential backoff each time we attempt to get the receipt and find that it's // with exponential backoff each time we attempt to get the receipt and find that it's
// not available. // not available.
let provider = Arc::new(provider);
poll( poll(
Self::RECEIPT_POLLING_DURATION, Self::RECEIPT_POLLING_DURATION,
PollingWaitBehavior::Constant(Duration::from_millis(200)), PollingWaitBehavior::Constant(Duration::from_millis(500)),
move || { move || {
let provider = provider.clone(); let provider = provider.clone();
async move { async move {
@@ -432,17 +540,94 @@ impl EthereumNode for LighthouseGethNode {
.await .await
}) })
} }
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] impl EthereumNode for LighthouseGethNode {
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>> {
Box::pin(async move { self.fund_all_accounts().await })
}
fn id(&self) -> usize {
self.id as _
}
fn connection_string(&self) -> &str {
&self.ws_connection_string
}
#[instrument(
level = "info",
skip_all,
fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string),
err,
)]
fn submit_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TxHash>> + '_>> {
Box::pin(async move {
let _permit = self.http_provider_requests_semaphore.acquire().await;
let provider = self
.http_provider()
.await
.context("Failed to create the provider for transaction submission")?;
let pending_transaction = provider
.send_transaction(transaction)
.await
.context("Failed to submit the transaction through the provider")?;
Ok(*pending_transaction.tx_hash())
})
}
#[instrument(
level = "info",
skip_all,
fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string),
)]
fn get_receipt(
&self,
tx_hash: TxHash,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move {
self.ws_provider()
.await
.context("Failed to create provider for getting the receipt")?
.get_transaction_receipt(tx_hash)
.await
.context("Failed to get the receipt of the transaction")?
.context("Failed to get the receipt of the transaction")
})
}
#[instrument(
level = "info",
skip_all,
fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string),
err,
)]
fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move {
let provider = self
.http_provider()
.await
.context("Failed to create provider for transaction execution")?;
Self::internal_execute_transaction(transaction, provider).await
})
}
#[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn trace_transaction( fn trace_transaction(
&self, &self,
tx_hash: TxHash, tx_hash: TxHash,
trace_options: GethDebugTracingOptions, trace_options: GethDebugTracingOptions,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::trace::geth::GethTrace>> + '_>> ) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
{
Box::pin(async move { Box::pin(async move {
let provider = Arc::new( let provider = Arc::new(
self.provider() self.http_provider()
.await .await
.context("Failed to create provider for tracing")?, .context("Failed to create provider for tracing")?,
); );
@@ -473,7 +658,7 @@ impl EthereumNode for LighthouseGethNode {
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn state_diff( fn state_diff(
&self, &self,
tx_hash: TxHash, tx_hash: TxHash,
@@ -497,13 +682,13 @@ impl EthereumNode for LighthouseGethNode {
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn balance_of( fn balance_of(
&self, &self,
address: Address, address: Address,
) -> Pin<Box<dyn Future<Output = anyhow::Result<U256>> + '_>> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<U256>> + '_>> {
Box::pin(async move { Box::pin(async move {
self.provider() self.ws_provider()
.await .await
.context("Failed to get the Geth provider")? .context("Failed to get the Geth provider")?
.get_balance(address) .get_balance(address)
@@ -512,14 +697,14 @@ impl EthereumNode for LighthouseGethNode {
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn latest_state_proof( fn latest_state_proof(
&self, &self,
address: Address, address: Address,
keys: Vec<StorageKey>, keys: Vec<StorageKey>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<EIP1186AccountProofResponse>> + '_>> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<EIP1186AccountProofResponse>> + '_>> {
Box::pin(async move { Box::pin(async move {
self.provider() self.ws_provider()
.await .await
.context("Failed to get the Geth provider")? .context("Failed to get the Geth provider")?
.get_proof(address, keys) .get_proof(address, keys)
@@ -529,13 +714,13 @@ impl EthereumNode for LighthouseGethNode {
}) })
} }
// #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn resolver( fn resolver(
&self, &self,
) -> Pin<Box<dyn Future<Output = anyhow::Result<Arc<dyn ResolverApi + '_>>> + '_>> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<Arc<dyn ResolverApi + '_>>> + '_>> {
Box::pin(async move { Box::pin(async move {
let id = self.id; let id = self.id;
let provider = self.provider().await?; let provider = self.ws_provider().await?;
Ok(Arc::new(LighthouseGethNodeResolver { id, provider }) as Arc<dyn ResolverApi>) Ok(Arc::new(LighthouseGethNodeResolver { id, provider }) as Arc<dyn ResolverApi>)
}) })
} }
@@ -543,6 +728,43 @@ impl EthereumNode for LighthouseGethNode {
fn evm_version(&self) -> EVMVersion { fn evm_version(&self) -> EVMVersion {
EVMVersion::Cancun EVMVersion::Cancun
} }
fn subscribe_to_full_blocks_information(
&self,
) -> Pin<
Box<
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
+ '_,
>,
> {
Box::pin(async move {
let provider = self.ws_provider().await?;
let block_subscription = provider.subscribe_full_blocks().channel_size(1024);
let block_stream = block_subscription
.into_stream()
.await
.context("Failed to create the block stream")?;
let mined_block_information_stream = block_stream.filter_map(|block| async {
let block = block.ok()?;
Some(MinedBlockInformation {
block_number: block.number(),
block_timestamp: block.header.timestamp,
mined_gas: block.header.gas_used as _,
block_gas_limit: block.header.gas_limit as _,
transaction_hashes: block
.transactions
.into_hashes()
.as_hashes()
.expect("Must be hashes")
.to_vec(),
})
});
Ok(Box::pin(mined_block_information_stream)
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
})
}
} }
pub struct LighthouseGethNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereum>> { pub struct LighthouseGethNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereum>> {
@@ -553,14 +775,14 @@ pub struct LighthouseGethNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereu
impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
for LighthouseGethNodeResolver<F, P> for LighthouseGethNodeResolver<F, P>
{ {
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn chain_id( fn chain_id(
&self, &self,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::primitives::ChainId>> + '_>> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::primitives::ChainId>> + '_>> {
Box::pin(async move { self.provider.get_chain_id().await.map_err(Into::into) }) Box::pin(async move { self.provider.get_chain_id().await.map_err(Into::into) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn transaction_gas_price( fn transaction_gas_price(
&self, &self,
tx_hash: TxHash, tx_hash: TxHash,
@@ -574,7 +796,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn block_gas_limit( fn block_gas_limit(
&self, &self,
number: BlockNumberOrTag, number: BlockNumberOrTag,
@@ -589,7 +811,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn block_coinbase( fn block_coinbase(
&self, &self,
number: BlockNumberOrTag, number: BlockNumberOrTag,
@@ -604,7 +826,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn block_difficulty( fn block_difficulty(
&self, &self,
number: BlockNumberOrTag, number: BlockNumberOrTag,
@@ -619,7 +841,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn block_base_fee( fn block_base_fee(
&self, &self,
number: BlockNumberOrTag, number: BlockNumberOrTag,
@@ -639,7 +861,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn block_hash( fn block_hash(
&self, &self,
number: BlockNumberOrTag, number: BlockNumberOrTag,
@@ -654,7 +876,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn block_timestamp( fn block_timestamp(
&self, &self,
number: BlockNumberOrTag, number: BlockNumberOrTag,
@@ -669,29 +891,55 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
}) })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn last_block_number(&self) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockNumber>> + '_>> { fn last_block_number(&self) -> Pin<Box<dyn Future<Output = anyhow::Result<BlockNumber>> + '_>> {
Box::pin(async move { self.provider.get_block_number().await.map_err(Into::into) }) Box::pin(async move { self.provider.get_block_number().await.map_err(Into::into) })
} }
} }
impl Node for LighthouseGethNode { impl Node for LighthouseGethNode {
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn shutdown(&mut self) -> anyhow::Result<()> { fn shutdown(&mut self) -> anyhow::Result<()> {
if !Command::new(self.kurtosis_binary_path.as_path()) let mut child = Command::new(self.kurtosis_binary_path.as_path())
.arg("enclave") .arg("enclave")
.arg("rm") .arg("rm")
.arg("-f") .arg("-f")
.arg(self.enclave_name.as_str()) .arg(self.enclave_name.as_str())
.stdout(Stdio::null()) .stdout(Stdio::piped())
.stderr(Stdio::null()) .stderr(Stdio::piped())
.spawn() .spawn()
.expect("Failed to spawn the enclave kill command") .expect("Failed to spawn the enclave kill command");
if !child
.wait() .wait()
.expect("Failed to wait for the enclave kill command") .expect("Failed to wait for the enclave kill command")
.success() .success()
{ {
panic!("Failed to shut down the enclave {}", self.enclave_name) let stdout = {
let mut stdout = String::default();
child
.stdout
.take()
.expect("Should be piped")
.read_to_string(&mut stdout)
.context("Failed to read stdout of kurtosis inspect to string")?;
stdout
};
let stderr = {
let mut stderr = String::default();
child
.stderr
.take()
.expect("Should be piped")
.read_to_string(&mut stderr)
.context("Failed to read stderr of kurtosis inspect to string")?;
stderr
};
panic!(
"Failed to shut down the enclave {} - stdout: {stdout}, stderr: {stderr}",
self.enclave_name
)
} }
drop(self.process.take()); drop(self.process.take());
@@ -699,13 +947,13 @@ impl Node for LighthouseGethNode {
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> { fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> {
self.init(genesis)?.spawn_process()?; self.init(genesis)?.spawn_process()?;
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn version(&self) -> anyhow::Result<String> { fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.kurtosis_binary_path) let output = Command::new(&self.kurtosis_binary_path)
.arg("version") .arg("version")
@@ -722,7 +970,7 @@ impl Node for LighthouseGethNode {
} }
impl Drop for LighthouseGethNode { impl Drop for LighthouseGethNode {
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))]
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown().expect("Failed to shutdown") self.shutdown().expect("Failed to shutdown")
} }
@@ -853,7 +1101,9 @@ mod tests {
use super::*; use super::*;
fn test_config() -> TestExecutionContext { fn test_config() -> TestExecutionContext {
TestExecutionContext::default() let mut config = TestExecutionContext::default();
config.wallet_configuration.additional_keys = 100;
config
} }
fn new_node() -> (TestExecutionContext, LighthouseGethNode) { fn new_node() -> (TestExecutionContext, LighthouseGethNode) {
@@ -888,6 +1138,7 @@ mod tests {
async fn node_mines_simple_transfer_transaction_and_returns_receipt() { async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange // Arrange
let (context, node) = new_node(); let (context, node) = new_node();
node.fund_all_accounts().await.expect("Failed");
let account_address = context let account_address = context
.wallet_configuration .wallet_configuration
@@ -906,6 +1157,7 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Ignored since they take a long time to run"]
fn version_works() { fn version_works() {
// Arrange // Arrange
let (_context, node) = new_node(); let (_context, node) = new_node();
@@ -922,6 +1174,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_chain_id_from_node() { async fn can_get_chain_id_from_node() {
// Arrange // Arrange
let (_context, node) = new_node(); let (_context, node) = new_node();
@@ -935,6 +1188,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_gas_limit_from_node() { async fn can_get_gas_limit_from_node() {
// Arrange // Arrange
let (_context, node) = new_node(); let (_context, node) = new_node();
@@ -952,6 +1206,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_coinbase_from_node() { async fn can_get_coinbase_from_node() {
// Arrange // Arrange
let (_context, node) = new_node(); let (_context, node) = new_node();
@@ -969,6 +1224,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_difficulty_from_node() { async fn can_get_block_difficulty_from_node() {
// Arrange // Arrange
let (_context, node) = new_node(); let (_context, node) = new_node();
@@ -986,6 +1242,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_hash_from_node() { async fn can_get_block_hash_from_node() {
// Arrange // Arrange
let (_context, node) = new_node(); let (_context, node) = new_node();
@@ -1003,6 +1260,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_timestamp_from_node() { async fn can_get_block_timestamp_from_node() {
// Arrange // Arrange
let (_context, node) = new_node(); let (_context, node) = new_node();
@@ -1020,6 +1278,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_number_from_node() { async fn can_get_block_number_from_node() {
// Arrange // Arrange
let (_context, node) = new_node(); let (_context, node) = new_node();
@@ -0,0 +1,3 @@
pub mod geth;
pub mod lighthouse_geth;
pub mod substrate;
@@ -23,17 +23,20 @@ use alloy::{
TxHash, U256, TxHash, U256,
}, },
providers::{ providers::{
Provider, ProviderBuilder, Provider,
ext::DebugApi, ext::DebugApi,
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, NonceFiller},
}, },
rpc::types::{ rpc::types::{
EIP1186AccountProofResponse, TransactionReceipt, EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
eth::{Block, Header, Transaction}, eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{
DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame,
},
}, },
}; };
use anyhow::Context as _; use anyhow::Context as _;
use futures::{Stream, StreamExt};
use revive_common::EVMVersion; use revive_common::EVMVersion;
use revive_dt_common::fs::clear_directory; use revive_dt_common::fs::clear_directory;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
@@ -43,14 +46,15 @@ use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32; use sp_runtime::AccountId32;
use revive_dt_config::*; use revive_dt_config::*;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
use tokio::sync::OnceCell;
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::{
Node, Node,
common::FallbackGasFiller, constants::{CHAIN_ID, INITIAL_BALANCE},
constants::INITIAL_BALANCE, helpers::{Process, ProcessReadinessWaitBehavior},
process::{Process, ProcessReadinessWaitBehavior}, provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
}; };
static NODE_COUNT: AtomicU32 = AtomicU32::new(0); static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -59,6 +63,7 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
/// or the revive-dev-node which is done by changing the path and some of the other arguments passed /// or the revive-dev-node which is done by changing the path and some of the other arguments passed
/// to the command. /// to the command.
#[derive(Debug)] #[derive(Debug)]
pub struct SubstrateNode { pub struct SubstrateNode {
id: u32, id: u32,
node_binary: PathBuf, node_binary: PathBuf,
@@ -71,7 +76,7 @@ pub struct SubstrateNode {
eth_proxy_process: Option<Process>, eth_proxy_process: Option<Process>,
wallet: Arc<EthereumWallet>, wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager, nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller, provider: OnceCell<ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>>,
} }
impl SubstrateNode { impl SubstrateNode {
@@ -121,8 +126,8 @@ impl SubstrateNode {
substrate_process: None, substrate_process: None,
eth_proxy_process: None, eth_proxy_process: None,
wallet: wallet.clone(), wallet: wallet.clone(),
chain_id_filler: Default::default(),
nonce_manager: Default::default(), nonce_manager: Default::default(),
provider: Default::default(),
} }
} }
@@ -144,6 +149,7 @@ impl SubstrateNode {
.arg(self.export_chainspec_command.as_str()) .arg(self.export_chainspec_command.as_str())
.arg("--chain") .arg("--chain")
.arg("dev") .arg("dev")
.env_remove("RUST_LOG")
.output() .output()
.context("Failed to export the chain-spec")?; .context("Failed to export the chain-spec")?;
@@ -335,27 +341,29 @@ impl SubstrateNode {
async fn provider( async fn provider(
&self, &self,
) -> anyhow::Result< ) -> anyhow::Result<ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>> {
FillProvider<impl TxFiller<ReviveNetwork>, impl Provider<ReviveNetwork>, ReviveNetwork>, self.provider
> { .get_or_try_init(|| async move {
ProviderBuilder::new() construct_concurrency_limited_provider::<ReviveNetwork, _>(
.disable_recommended_fillers() self.rpc_url.as_str(),
.network::<ReviveNetwork>() FallbackGasFiller::new(250_000_000, 5_000_000_000, 1_000_000_000),
.filler(FallbackGasFiller::new( ChainIdFiller::new(Some(CHAIN_ID)),
25_000_000, NonceFiller::new(self.nonce_manager.clone()),
1_000_000_000, self.wallet.clone(),
1_000_000_000, )
))
.filler(self.chain_id_filler.clone())
.filler(NonceFiller::new(self.nonce_manager.clone()))
.wallet(self.wallet.clone())
.connect(&self.rpc_url)
.await .await
.map_err(Into::into) .context("Failed to construct the provider")
})
.await
.cloned()
} }
} }
impl EthereumNode for SubstrateNode { impl EthereumNode for SubstrateNode {
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>> {
Box::pin(async move { Ok(()) })
}
fn id(&self) -> usize { fn id(&self) -> usize {
self.id as _ self.id as _
} }
@@ -364,11 +372,48 @@ impl EthereumNode for SubstrateNode {
&self.rpc_url &self.rpc_url
} }
fn execute_transaction( fn submit_transaction(
&self, &self,
transaction: alloy::rpc::types::TransactionRequest, transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TxHash>> + '_>> {
Box::pin(async move {
let provider = self
.provider()
.await
.context("Failed to create the provider for transaction submission")?;
let pending_transaction = provider
.send_transaction(transaction)
.await
.context("Failed to submit the transaction through the provider")?;
Ok(*pending_transaction.tx_hash())
})
}
fn get_receipt(
&self,
tx_hash: TxHash,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move { Box::pin(async move {
self.provider()
.await
.context("Failed to create provider for getting the receipt")?
.get_transaction_receipt(tx_hash)
.await
.context("Failed to get the receipt of the transaction")?
.context("Failed to get the receipt of the transaction")
})
}
fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
static SEMAPHORE: std::sync::LazyLock<tokio::sync::Semaphore> =
std::sync::LazyLock::new(|| tokio::sync::Semaphore::new(500));
Box::pin(async move {
let _permit = SEMAPHORE.acquire().await?;
let receipt = self let receipt = self
.provider() .provider()
.await .await
@@ -387,8 +432,7 @@ impl EthereumNode for SubstrateNode {
&self, &self,
tx_hash: TxHash, tx_hash: TxHash,
trace_options: GethDebugTracingOptions, trace_options: GethDebugTracingOptions,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::trace::geth::GethTrace>> + '_>> ) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
{
Box::pin(async move { Box::pin(async move {
self.provider() self.provider()
.await .await
@@ -463,16 +507,56 @@ impl EthereumNode for SubstrateNode {
fn evm_version(&self) -> EVMVersion { fn evm_version(&self) -> EVMVersion {
EVMVersion::Cancun EVMVersion::Cancun
} }
fn subscribe_to_full_blocks_information(
&self,
) -> Pin<
Box<
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
+ '_,
>,
> {
Box::pin(async move {
let provider = self
.provider()
.await
.context("Failed to create the provider for block subscription")?;
let mut block_subscription = provider
.watch_full_blocks()
.await
.context("Failed to create the blocks stream")?;
block_subscription.set_channel_size(0xFFFF);
block_subscription.set_poll_interval(Duration::from_secs(1));
let block_stream = block_subscription.into_stream();
let mined_block_information_stream = block_stream.filter_map(|block| async {
let block = block.ok()?;
Some(MinedBlockInformation {
block_number: block.number(),
block_timestamp: block.header.timestamp,
mined_gas: block.header.gas_used as _,
block_gas_limit: block.header.gas_limit,
transaction_hashes: block
.transactions
.into_hashes()
.as_hashes()
.expect("Must be hashes")
.to_vec(),
})
});
Ok(Box::pin(mined_block_information_stream)
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
})
}
} }
pub struct SubstrateNodeResolver<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> { pub struct SubstrateNodeResolver {
id: u32, id: u32,
provider: FillProvider<F, P, ReviveNetwork>, provider: ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>,
} }
impl<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> ResolverApi impl ResolverApi for SubstrateNodeResolver {
for SubstrateNodeResolver<F, P>
{
#[instrument(level = "info", skip_all, fields(substrate_node_id = self.id))] #[instrument(level = "info", skip_all, fields(substrate_node_id = self.id))]
fn chain_id( fn chain_id(
&self, &self,
@@ -1068,9 +1152,7 @@ mod tests {
use crate::Node; use crate::Node;
fn test_config() -> TestExecutionContext { fn test_config() -> TestExecutionContext {
let mut context = TestExecutionContext::default(); TestExecutionContext::default()
context.kitchensink_configuration.use_kitchensink = true;
context
} }
fn new_node() -> (TestExecutionContext, SubstrateNode) { fn new_node() -> (TestExecutionContext, SubstrateNode) {
@@ -1142,6 +1224,7 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Ignored since they take a long time to run"]
fn test_init_generates_chainspec_with_balances() { fn test_init_generates_chainspec_with_balances() {
let genesis_content = r#" let genesis_content = r#"
{ {
@@ -1195,6 +1278,7 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Ignored since they take a long time to run"]
fn test_parse_genesis_alloc() { fn test_parse_genesis_alloc() {
// Create test genesis file // Create test genesis file
let genesis_json = r#" let genesis_json = r#"
@@ -1237,6 +1321,7 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Ignored since they take a long time to run"]
fn print_eth_to_substrate_mappings() { fn print_eth_to_substrate_mappings() {
let eth_addresses = vec![ let eth_addresses = vec![
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
@@ -1252,6 +1337,7 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Ignored since they take a long time to run"]
fn test_eth_to_substrate_address() { fn test_eth_to_substrate_address() {
let cases = vec![ let cases = vec![
( (
@@ -1282,6 +1368,7 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Ignored since they take a long time to run"]
fn version_works() { fn version_works() {
let node = shared_node(); let node = shared_node();
@@ -1294,6 +1381,7 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Ignored since they take a long time to run"]
fn eth_rpc_version_works() { fn eth_rpc_version_works() {
let node = shared_node(); let node = shared_node();
@@ -1306,6 +1394,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_chain_id_from_node() { async fn can_get_chain_id_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -1319,6 +1408,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_gas_limit_from_node() { async fn can_get_gas_limit_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -1336,6 +1426,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_coinbase_from_node() { async fn can_get_coinbase_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -1353,6 +1444,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_difficulty_from_node() { async fn can_get_block_difficulty_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -1370,6 +1462,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_hash_from_node() { async fn can_get_block_hash_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -1387,6 +1480,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_timestamp_from_node() { async fn can_get_block_timestamp_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -1404,6 +1498,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_number_from_node() { async fn can_get_block_number_from_node() {
// Arrange // Arrange
let node = shared_node(); let node = shared_node();
@@ -0,0 +1,69 @@
use std::sync::Arc;
use alloy::transports::BoxFuture;
use tokio::sync::Semaphore;
use tower::{Layer, Service};
#[derive(Clone, Debug)]
pub struct ConcurrencyLimiterLayer {
semaphore: Arc<Semaphore>,
}
impl ConcurrencyLimiterLayer {
pub fn new(permit_count: usize) -> Self {
Self {
semaphore: Arc::new(Semaphore::new(permit_count)),
}
}
}
impl<S> Layer<S> for ConcurrencyLimiterLayer {
type Service = ConcurrencyLimiterService<S>;
fn layer(&self, inner: S) -> Self::Service {
ConcurrencyLimiterService {
service: inner,
semaphore: self.semaphore.clone(),
}
}
}
#[derive(Clone)]
pub struct ConcurrencyLimiterService<S> {
service: S,
semaphore: Arc<Semaphore>,
}
impl<S, Request> Service<Request> for ConcurrencyLimiterService<S>
where
S: Service<Request> + Send,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
let semaphore = self.semaphore.clone();
let future = self.service.call(req);
Box::pin(async move {
let _permit = semaphore
.acquire()
.await
.expect("Semaphore has been closed");
tracing::debug!(
available_permits = semaphore.available_permits(),
"Acquired Semaphore Permit"
);
future.await
})
}
}
@@ -30,6 +30,12 @@ impl FallbackGasFiller {
} }
} }
impl Default for FallbackGasFiller {
fn default() -> Self {
FallbackGasFiller::new(25_000_000, 1_000_000_000, 1_000_000_000)
}
}
impl<N> TxFiller<N> for FallbackGasFiller impl<N> TxFiller<N> for FallbackGasFiller
where where
N: Network, N: Network,
+7
View File
@@ -0,0 +1,7 @@
mod concurrency_limiter;
mod fallback_gas_provider;
mod provider;
pub use concurrency_limiter::*;
pub use fallback_gas_provider::*;
pub use provider::*;
@@ -0,0 +1,63 @@
use std::sync::LazyLock;
use alloy::{
network::{Network, NetworkWallet, TransactionBuilder4844},
providers::{
Identity, ProviderBuilder, RootProvider,
fillers::{ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, WalletFiller},
},
rpc::client::ClientBuilder,
};
use anyhow::{Context, Result};
use crate::provider_utils::{ConcurrencyLimiterLayer, FallbackGasFiller};
pub type ConcreteProvider<N, W> = FillProvider<
JoinFill<
JoinFill<JoinFill<JoinFill<Identity, FallbackGasFiller>, ChainIdFiller>, NonceFiller>,
WalletFiller<W>,
>,
RootProvider<N>,
N,
>;
pub async fn construct_concurrency_limited_provider<N, W>(
rpc_url: &str,
fallback_gas_filler: FallbackGasFiller,
chain_id_filler: ChainIdFiller,
nonce_filler: NonceFiller,
wallet: W,
) -> Result<ConcreteProvider<N, W>>
where
N: Network<TransactionRequest: TransactionBuilder4844>,
W: NetworkWallet<N>,
Identity: TxFiller<N>,
FallbackGasFiller: TxFiller<N>,
ChainIdFiller: TxFiller<N>,
NonceFiller: TxFiller<N>,
WalletFiller<W>: TxFiller<N>,
{
// This is a global limit on the RPC concurrency that applies to all of the providers created
// by the framework. With this limit, it means that we can have a maximum of N concurrent
// requests at any point of time and no more than that. This is done in an effort to stabilize
// the framework from some of the interment issues that we've been seeing related to RPC calls.
static GLOBAL_CONCURRENCY_LIMITER_LAYER: LazyLock<ConcurrencyLimiterLayer> =
LazyLock::new(|| ConcurrencyLimiterLayer::new(10));
let client = ClientBuilder::default()
.layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone())
.connect(rpc_url)
.await
.context("Failed to construct the RPC client")?;
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.network::<N>()
.filler(fallback_gas_filler)
.filler(chain_id_filler)
.filler(nonce_filler)
.wallet(wallet)
.connect_client(client);
Ok(provider)
}
+1 -1
View File
@@ -13,7 +13,7 @@ revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true } revive-dt-format = { workspace = true }
revive-dt-compiler = { workspace = true } revive-dt-compiler = { workspace = true }
alloy-primitives = { workspace = true } alloy = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
indexmap = { workspace = true, features = ["serde"] } indexmap = { workspace = true, features = ["serde"] }
+9 -1
View File
@@ -8,7 +8,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use alloy_primitives::Address; use alloy::primitives::Address;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use indexmap::IndexMap; use indexmap::IndexMap;
use revive_dt_common::types::PlatformIdentifier; use revive_dt_common::types::PlatformIdentifier;
@@ -106,6 +106,10 @@ impl ReportAggregator {
RunnerEvent::ContractDeployed(event) => { RunnerEvent::ContractDeployed(event) => {
self.handle_contract_deployed_event(*event); self.handle_contract_deployed_event(*event);
} }
RunnerEvent::Completion(event) => {
self.handle_completion(*event);
break;
}
} }
} }
debug!("Report aggregation completed"); debug!("Report aggregation completed");
@@ -382,6 +386,10 @@ impl ReportAggregator {
.insert(event.contract_instance, event.address); .insert(event.contract_instance, event.address);
} }
fn handle_completion(&mut self, _: CompletionEvent) {
self.runner_rx.close();
}
fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut TestCaseReport { fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut TestCaseReport {
self.report self.report
.test_case_information .test_case_information
+3 -1
View File
@@ -3,7 +3,7 @@
use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use alloy_primitives::Address; use alloy::primitives::Address;
use anyhow::Context as _; use anyhow::Context as _;
use indexmap::IndexMap; use indexmap::IndexMap;
use revive_dt_common::types::PlatformIdentifier; use revive_dt_common::types::PlatformIdentifier;
@@ -613,6 +613,8 @@ define_event! {
/// The address of the contract. /// The address of the contract.
address: Address address: Address
}, },
/// Reports the completion of the run.
Completion {}
} }
} }
+9 -3
View File
@@ -75,7 +75,11 @@ ABSOLUTE_PATH=$(realpath "$TEST_REPO_DIR/fixtures/solidity/")
cat > "$CORPUS_FILE" << EOF cat > "$CORPUS_FILE" << EOF
{ {
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests", "name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
"path": "$ABSOLUTE_PATH" "paths": [
"$(realpath "$TEST_REPO_DIR/fixtures/solidity/translated_semantic_tests")",
"$(realpath "$TEST_REPO_DIR/fixtures/solidity/complex")",
"$(realpath "$TEST_REPO_DIR/fixtures/solidity/simple")"
]
} }
EOF EOF
@@ -89,12 +93,14 @@ echo "This may take a while..."
echo "" echo ""
# Run the tool # Run the tool
RUST_LOG="info" cargo run --release -- execute-tests \ cargo build --release;
RUST_LOG="info,alloy_pubsub::service=error" ./target/release/retester test \
--platform geth-evm-solc \ --platform geth-evm-solc \
--platform revive-dev-node-polkavm-resolc \ --platform revive-dev-node-revm-solc \
--corpus "$CORPUS_FILE" \ --corpus "$CORPUS_FILE" \
--working-directory "$WORKDIR" \ --working-directory "$WORKDIR" \
--concurrency.number-of-nodes 5 \ --concurrency.number-of-nodes 5 \
--wallet.additional-keys 100000 \
--kitchensink.path "$SUBSTRATE_NODE_BIN" \ --kitchensink.path "$SUBSTRATE_NODE_BIN" \
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \ --revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
--eth-rpc.path "$ETH_RPC_BIN" \ --eth-rpc.path "$ETH_RPC_BIN" \