diff --git a/Cargo.lock b/Cargo.lock index 97233fa..d2ae2af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,9 +67,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad4eb51e7845257b70c51b38ef8d842d5e5e93196701fcbd757577971a043c6" +checksum = "7f6cfe35f100bc496007c9a00f90b88bdf565f1421d4c707c9f07e0717e2aaad" dependencies = [ "alloy-consensus", "alloy-contract", @@ -88,6 +88,7 @@ dependencies = [ "alloy-transport-http", "alloy-transport-ipc", "alloy-transport-ws", + "alloy-trie", ] [[package]] @@ -103,9 +104,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3b746060277f3d7f9c36903bb39b593a741cb7afcb0044164c28f0e9b673f0" +checksum = "59094911f05dbff1cf5b29046a00ef26452eccc8d47136d50a47c0cf22f00c85" dependencies = [ "alloy-eips", "alloy-primitives", @@ -122,15 +123,16 @@ dependencies = [ "rand 0.8.5", "secp256k1 0.30.0", "serde", + "serde_json", "serde_with", "thiserror 2.0.12", ] [[package]] name = "alloy-consensus-any" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf98679329fa708fa809ea596db6d974da892b068ad45e48ac1956f582edf946" +checksum = "903cb8f728107ca27c816546f15be38c688df3c381d7bd1a4a9f215effc1ddb4" dependencies = [ "alloy-consensus", "alloy-eips", @@ -142,9 +144,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a10e47f5305ea08c37b1772086c1573e9a0a257227143996841172d37d3831bb" +checksum = "03df5cb3b428ac96b386ad64c11d5c6e87a5505682cf1fbd6f8f773e9eda04f6" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -230,9 +232,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f562a81278a3ed83290e68361f2d1c75d018ae3b8589a314faf9303883e18ec9" +checksum = "ac7f1c9a1ccc7f3e03c36976455751a6166a4f0d2d2c530c3f87dfe7d0cdc836" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -245,14 +247,16 @@ dependencies = [ "derive_more 2.0.1", "either", "serde", + "serde_with", "sha2 0.10.9", + "thiserror 2.0.12", ] [[package]] name = "alloy-genesis" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc41384e9ab8c9b2fb387c52774d9d432656a28edcda1c2d4083e96051524518" +checksum = "1421f6c9d15e5b86afbfe5865ca84dea3b9f77173a0963c1a2ee4e626320ada9" dependencies = [ "alloy-eips", "alloy-primitives", @@ -276,9 +280,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12c454fcfcd5d26ed3b8cae5933cbee9da5f0b05df19b46d4bd4446d1f082565" +checksum = "65f763621707fa09cece30b73ecc607eb43fd7a72451fe3b46f645b905086926" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -291,9 +295,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d6d39eabe5c7b3d8f23ac47b0b683b99faa4359797114636c66e0743103d05" +checksum = "2f59a869fa4b4c3a7f08b1c8cb79aec61c29febe6e24a24fe0fcfded8a9b5703" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -317,9 +321,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3704fa8b7ba9ba3f378d99b3d628c8bc8c2fc431b709947930f154e22a8368b6" +checksum = "46e9374c667c95c41177602ebe6f6a2edd455193844f011d973d374b65501b38" dependencies = [ "alloy-consensus", "alloy-eips", @@ -358,9 +362,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08800e8cbe70c19e2eb7cf3d7ff4b28bdd9b3933f8e1c8136c7d910617ba03bf" +checksum = "77818b7348bd5486491a5297579dbfe5f706a81f8e1f5976393025f1e22a7c7d" dependencies = [ "alloy-chains", "alloy-consensus", @@ -387,7 +391,6 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "http", "lru", "parking_lot", "pin-project", @@ -403,13 +406,14 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae68457a2c2ead6bd7d7acb5bf5f1623324b1962d4f8e7b0250657a3c3ab0a0b" +checksum = "249b45103a66c9ad60ad8176b076106d03a2399a37f0ee7b0e03692e6b354cb9" dependencies = [ "alloy-json-rpc", "alloy-primitives", "alloy-transport", + "auto_impl", "bimap", "futures", "parking_lot", @@ -446,9 +450,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162301b5a57d4d8f000bf30f4dcb82f9f468f3e5e846eeb8598dd39e7886932c" +checksum = "2430d5623e428dd012c6c2156ae40b7fe638d6fca255e3244e0fba51fa698e93" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -472,11 +476,12 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cd8ca94ae7e2b32cc3895d9981f3772aab0b4756aa60e9ed0bcfee50f0e1328" +checksum = "e9e131624d08a25cfc40557041e7dc42e1182fa1153e7592d120f769a1edce56" dependencies = [ "alloy-primitives", + "alloy-rpc-types-debug", "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-serde", @@ -485,9 +490,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076b47e834b367d8618c52dd0a0d6a711ddf66154636df394805300af4923b8a" +checksum = "07429a1099cd17227abcddb91b5e38c960aaeb02a6967467f5bb561fbe716ac6" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -496,19 +501,21 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a2a86ad7b7d718c15e79d0779bd255561b6b22968dc5ed2e7c0fbc43bb55fe" +checksum = "aeff305b7d10cc1c888456d023e7bb8a5ea82e9e42b951e37619b88cc1a1486d" dependencies = [ "alloy-primitives", + "derive_more 2.0.1", "serde", + "serde_with", ] [[package]] name = "alloy-rpc-types-eth" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c2f847e635ec0be819d06e2ada4bcc4e4204026a83c4bfd78ae8d550e027ae7" +checksum = "db46b0901ee16bbb68d986003c66dcb74a12f9d9b3c44f8e85d51974f2458f0f" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -527,9 +534,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fc58180302a94c934d455eeedb3ecb99cdc93da1dbddcdbbdb79dd6fe618b2a" +checksum = "36f10620724bd45f80c79668a8cdbacb6974f860686998abce28f6196ae79444" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -541,9 +548,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae699248d02ade9db493bbdae61822277dc14ae0f82a5a4153203b60e34422a6" +checksum = "5413814be7a22fbc81e0f04a2401fcc3eb25e56fd53b04683e8acecc6e1fe01b" dependencies = [ "alloy-primitives", "serde", @@ -552,9 +559,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf7d793c813515e2b627b19a15693960b3ed06670f9f66759396d06ebe5747b" +checksum = "53410a18a61916e2c073a6519499514e027b01e77eeaf96acd1df7cf96ef6bb2" dependencies = [ "alloy-primitives", "async-trait", @@ -567,9 +574,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51a424bc5a11df0d898ce0fd15906b88ebe2a6e4f17a514b51bc93946bb756bd" +checksum = "e6006c4cbfa5d08cadec1fcabea6cb56dc585a30a9fce40bcf81e307d6a71c8e" dependencies = [ "alloy-consensus", "alloy-network", @@ -656,12 +663,13 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f317d20f047b3de4d9728c556e2e9a92c9a507702d2016424cd8be13a74ca5e" +checksum = "d94ee404368a3d9910dfe61b203e888c6b0e151a50e147f95da8baff9f9c7763" dependencies = [ "alloy-json-rpc", "alloy-primitives", + "auto_impl", "base64 0.22.1", "derive_more 2.0.1", "futures", @@ -679,9 +687,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff084ac7b1f318c87b579d221f11b748341d68b9ddaa4ffca5e62ed2b8cfefb4" +checksum = "a2f8a6338d594f6c6481292215ee8f2fd7b986c80aba23f3f44e761a8658de78" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -694,9 +702,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb099cdad8ed2e6a80811cdf9bbf715ebf4e34c981b4a6e2d1f9daacbf8b218" +checksum = "17a37a8ca18006fa0a58c7489645619ff58cfa073f2b29c4e052c9bd114b123a" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -714,9 +722,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e915e1250dc129ad48d264573ccd08e4716fdda564a772fd217875b8459aff9" +checksum = "679b0122b7bca9d4dc5eb2c0549677a3c53153f6e232f23f4b3ba5575f74ebde" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -748,12 +756,12 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.22" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1154c8187a5ff985c95a8b2daa2fedcf778b17d7668e5e50e556c4ff9c881154" +checksum = "e64c09ec565a90ed8390d82aa08cd3b22e492321b96cb4a3d4f58414683c9e2f" dependencies = [ "alloy-primitives", - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.101", @@ -2010,8 +2018,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -2028,13 +2046,39 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim", + "syn 2.0.101", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.101", ] @@ -4516,7 +4560,6 @@ name = "revive-dt-common" version = "0.1.0" dependencies = [ "alloy", - "alloy-primitives", "anyhow", "clap", "moka", @@ -4533,7 +4576,6 @@ name = "revive-dt-compiler" version = "0.1.0" dependencies = [ "alloy", - "alloy-primitives", "anyhow", "dashmap", "foundry-compilers-artifacts", @@ -4597,8 +4639,6 @@ name = "revive-dt-format" version = "0.1.0" dependencies = [ "alloy", - "alloy-primitives", - "alloy-sol-types", "anyhow", "futures", "regex", @@ -4641,6 +4681,7 @@ version = "0.1.0" dependencies = [ "alloy", "anyhow", + "futures", "revive-common", "revive-dt-format", ] @@ -4649,7 +4690,7 @@ dependencies = [ name = "revive-dt-report" version = "0.1.0" dependencies = [ - "alloy-primitives", + "alloy", "anyhow", "indexmap 2.10.0", "paste", @@ -5115,10 +5156,11 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -5132,10 +5174,19 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" 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 = [ "proc-macro2", "quote", @@ -5223,7 +5274,7 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.101", diff --git a/Cargo.toml b/Cargo.toml index a1865f1..e929337 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" } -alloy-primitives = "1.2.1" -alloy-sol-types = "1.2.1" anyhow = "1.0" bson = { version = "2.15.0" } cacache = { version = "13.1.0" } @@ -75,7 +73,7 @@ revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" } [workspace.dependencies.alloy] -version = "1.0.22" +version = "1.0.37" default-features = false features = [ "json-abi", @@ -92,6 +90,7 @@ features = [ "serde", "rpc-types-eth", "genesis", + "sol-types", ] [profile.bench] diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index bc1b8f7..68b185f 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -10,7 +10,6 @@ rust-version.workspace = true [dependencies] alloy = { workspace = true } -alloy-primitives = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } moka = { workspace = true, features = ["sync"] } diff --git a/crates/common/src/types/private_key_allocator.rs b/crates/common/src/types/private_key_allocator.rs index f48fa15..5bab1d3 100644 --- a/crates/common/src/types/private_key_allocator.rs +++ b/crates/common/src/types/private_key_allocator.rs @@ -1,6 +1,6 @@ +use alloy::primitives::U256; use alloy::signers::local::PrivateKeySigner; -use alloy_primitives::U256; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; /// 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. @@ -10,25 +10,26 @@ pub struct PrivateKeyAllocator { next_private_key: U256, /// The highest private key (exclusive) that can be returned by this allocator. - highest_private_key_exclusive: U256, + highest_private_key_inclusive: U256, } impl PrivateKeyAllocator { /// 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 { - next_private_key: U256::ZERO, - highest_private_key_exclusive, + next_private_key: U256::ONE, + highest_private_key_inclusive, } } /// Allocates a new private key and errors out if the maximum private key has been reached. pub fn allocate(&mut self) -> Result { - 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"); }; 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; Ok(private_key) } diff --git a/crates/compiler/Cargo.toml b/crates/compiler/Cargo.toml index 6797a22..ce40223 100644 --- a/crates/compiler/Cargo.toml +++ b/crates/compiler/Cargo.toml @@ -16,7 +16,6 @@ revive-dt-solc-binaries = { workspace = true } revive-common = { workspace = true } alloy = { workspace = true } -alloy-primitives = { workspace = true } anyhow = { workspace = true } dashmap = { workspace = true } foundry-compilers-artifacts = { workspace = true } diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 5656528..7780eba 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -11,7 +11,7 @@ use std::{ }; use alloy::json_abi::JsonAbi; -use alloy_primitives::Address; +use alloy::primitives::Address; use anyhow::{Context as _, Result}; use semver::Version; use serde::{Deserialize, Serialize}; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 75419e8..7814d48 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -28,7 +28,11 @@ use temp_dir::TempDir; #[command(name = "retester")] pub enum Context { /// Executes tests in the MatterLabs format differentially on multiple targets concurrently. - ExecuteTests(Box), + Test(Box), + + /// Executes differential benchmarks on various platforms. + Benchmark(Box), + /// Exports the JSON schema of the MatterLabs test format used by the tool. ExportJsonSchema, } @@ -46,7 +50,8 @@ impl Context { impl AsRef for Context { fn as_ref(&self) -> &WorkingDirectoryConfiguration { 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!(), } } @@ -55,7 +60,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &CorpusConfiguration { 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!(), } } @@ -64,7 +70,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &SolcConfiguration { 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!(), } } @@ -73,7 +80,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &ResolcConfiguration { 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!(), } } @@ -82,7 +90,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &GethConfiguration { 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!(), } } @@ -91,7 +100,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &KurtosisConfiguration { 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!(), } } @@ -100,7 +110,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &KitchensinkConfiguration { 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!(), } } @@ -109,7 +120,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &ReviveDevNodeConfiguration { 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!(), } } @@ -118,7 +130,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &EthRpcConfiguration { 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!(), } } @@ -127,7 +140,11 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &GenesisConfiguration { match self { - Self::ExecuteTests(context) => context.as_ref().as_ref(), + Self::Test(context) => context.as_ref().as_ref(), + Self::Benchmark(..) => { + static GENESIS: LazyLock = LazyLock::new(Default::default); + &GENESIS + } Self::ExportJsonSchema => unreachable!(), } } @@ -136,7 +153,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &WalletConfiguration { 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!(), } } @@ -145,7 +163,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &ConcurrencyConfiguration { 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!(), } } @@ -154,7 +173,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &CompilationConfiguration { 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!(), } } @@ -163,7 +183,8 @@ impl AsRef for Context { impl AsRef for Context { fn as_ref(&self) -> &ReportConfiguration { 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!(), } } @@ -268,6 +289,11 @@ pub struct BenchmarkingContext { )] pub platforms: Vec, + /// 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, @@ -495,7 +521,7 @@ impl AsRef for BenchmarkingContext { #[derive(Clone, Debug, Parser, Serialize)] pub struct CorpusConfiguration { /// A list of test corpus JSON files to be tested. - #[arg(long = "corpus", short)] + #[arg(short = 'c', long = "corpus")] pub paths: Vec, } @@ -627,7 +653,7 @@ pub struct EthRpcConfiguration { } /// A set of configuration parameters for the genesis. -#[derive(Clone, Debug, Parser, Serialize)] +#[derive(Clone, Debug, Default, Parser, Serialize)] pub struct GenesisConfiguration { /// Specifies the path of the genesis file to use for the nodes that are started. /// diff --git a/crates/core/src/differential_benchmarks/driver.rs b/crates/core/src/differential_benchmarks/driver.rs new file mode 100644 index 0000000..5714cf1 --- /dev/null +++ b/crates/core/src/differential_benchmarks/driver.rs @@ -0,0 +1,758 @@ +use std::{collections::HashMap, ops::ControlFlow, sync::Arc, 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, trace}; + +use crate::{ + differential_benchmarks::{ExecutionState, WatcherEvent}, + helpers::{CachedCompiler, TestDefinition, TestPlatformInformation}, +}; + +/// The differential tests driver for a single platform. +pub struct Driver<'a, I> { + /// The information of the platform that this driver is for. + platform_information: &'a TestPlatformInformation<'a>, + + /// The resolver of the platform. + resolver: Arc, + + /// 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>, + + /// 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, + + /// 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, +{ + // region:Constructors & Initialization + pub async fn new( + platform_information: &'a TestPlatformInformation<'a>, + test_definition: &'a TestDefinition<'a>, + private_key_allocator: Arc>, + cached_compiler: &CachedCompiler<'a>, + watcher_tx: UnboundedSender, + steps: I, + ) -> Result { + let mut this = Driver { + 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::>; + 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::::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 { + while let Some(result) = self.execute_next_step().await { + result? + } + Ok(self.steps_executed) + } + + pub async fn execute_next_step(&mut self) -> Option> { + 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( + 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)] + pub async fn execute_function_call( + &mut self, + _: &StepPath, + step: &FunctionCallStep, + ) -> Result { + 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> { + trace!("Handling Function Call Contract Deployment"); + + let mut instances_we_must_deploy = IndexMap::::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); + } + } + + trace!( + deployed_contracts = receipts.len(), + "Handled function call contract deployment" + ); + Ok(receipts) + } + + async fn handle_function_call_execution( + &mut self, + step: &FunctionCallStep, + mut deployment_receipts: HashMap, + ) -> Result { + trace!("Handling the function call execution"); + 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(_) => { + trace!("Creating the transaction"); + let tx = step + .as_transaction(self.resolver.as_ref(), self.default_resolution_context()) + .await?; + trace!("Created the transaction"); + trace!("Calling the execute transaction when handling the function call execution"); + self.execute_transaction(tx).await + } + } + } + + async fn handle_function_call_call_frame_tracing( + &mut self, + tx_hash: TxHash, + ) -> Result { + 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)] + pub async fn execute_balance_assertion( + &mut self, + _: &StepPath, + _: &BalanceAssertionStep, + ) -> anyhow::Result { + // Kept empty intentionally for the benchmark driver. + Ok(1) + } + + #[instrument(level = "info", skip_all, err(Debug))] + async fn execute_storage_empty_assertion_step( + &mut self, + _: &StepPath, + _: &StorageEmptyAssertionStep, + ) -> Result { + // Kept empty intentionally for the benchmark driver. + Ok(1) + } + + #[instrument(level = "info", skip_all, err(Debug))] + async fn execute_repeat_step( + &mut self, + step_path: &StepPath, + step: &RepeatStep, + ) -> Result { + let tasks = (0..step.repeat) + .map(|_| Driver { + 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: Vec<(StepPath, Step)> = 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(); + steps.into_iter() + }, + watcher_tx: self.watcher_tx.clone(), + }) + .map(|driver| driver.execute_all()) + .collect::>(); + + // 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", skip_all, err(Debug))] + pub async fn execute_account_allocation( + &mut self, + _: &StepPath, + step: &AllocateAccountStep, + ) -> Result { + 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( + 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, + ) -> Result<(Address, JsonAbi, Option)> { + 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( + 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, + ) -> 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::::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", skip_all)] + async fn step_address_auto_deployment( + &mut self, + step_address: &StepAddress, + ) -> Result
{ + 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(transaction_hash = tracing::field::Empty))] + async fn execute_transaction( + &self, + transaction: TransactionRequest, + ) -> anyhow::Result { + trace!("Submitting transaction"); + 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(10 * 60), + PollingWaitBehavior::Constant(Duration::from_secs(1)), + || { + async move { + match node.get_receipt(transaction_hash).await { + Ok(receipt) => Ok(ControlFlow::Break(receipt)), + Err(_) => Ok(ControlFlow::Continue(())), + } + } + .instrument(info_span!("Polling for receipt")) + }, + ) + .await + } + // endregion:Transaction Execution +} diff --git a/crates/core/src/differential_benchmarks/entry_point.rs b/crates/core/src/differential_benchmarks/entry_point.rs new file mode 100644 index 0000000..7702d52 --- /dev/null +++ b/crates/core/src/differential_benchmarks/entry_point.rs @@ -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::>(); + + // 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::>() + .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(()) +} diff --git a/crates/core/src/differential_benchmarks/execution_state.rs b/crates/core/src/differential_benchmarks/execution_state.rs new file mode 100644 index 0000000..501526f --- /dev/null +++ b/crates/core/src/differential_benchmarks/execution_state.rs @@ -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>, + + /// A map of all of the deployed contracts and information about them. + pub deployed_contracts: HashMap, + + /// This map stores the variables used for each one of the cases contained in the metadata file. + pub variables: HashMap, +} + +impl ExecutionState { + pub fn new( + compiled_contracts: HashMap>, + deployed_contracts: HashMap, + ) -> 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(), + } + } +} diff --git a/crates/core/src/differential_benchmarks/mod.rs b/crates/core/src/differential_benchmarks/mod.rs new file mode 100644 index 0000000..68f8a49 --- /dev/null +++ b/crates/core/src/differential_benchmarks/mod.rs @@ -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::*; diff --git a/crates/core/src/differential_benchmarks/watcher.rs b/crates/core/src/differential_benchmarks/watcher.rs new file mode 100644 index 0000000..696a7b8 --- /dev/null +++ b/crates/core/src/differential_benchmarks/watcher.rs @@ -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, + + /// 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>>, +} + +impl Watcher { + pub fn new( + platform_identifier: PlatformIdentifier, + blocks_stream: Pin>>, + ) -> (Self, UnboundedSender) { + let (tx, rx) = unbounded_channel::(); + ( + 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::::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 stdout = std::io::stdout().lock(); + writeln!( + stdout, + "Watcher information for {}", + self.platform_identifier + )?; + writeln!( + stdout, + "block_number,block_timestamp,mined_gas,block_gas_limit,tx_count" + )?; + for block in mined_blocks_information { + writeln!( + stdout, + "{},{},{},{},{}", + 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, +} diff --git a/crates/core/src/differential_tests/driver.rs b/crates/core/src/differential_tests/driver.rs index b02087a..4b3f36f 100644 --- a/crates/core/src/differential_tests/driver.rs +++ b/crates/core/src/differential_tests/driver.rs @@ -40,14 +40,14 @@ use crate::{ type StepsIterator = std::vec::IntoIter<(StepPath, Step)>; -pub struct DifferentialTestsDriver<'a, I> { +pub struct Driver<'a, I> { /// The drivers for the various platforms that we're executing the tests on. - platform_drivers: BTreeMap>, + platform_drivers: BTreeMap>, } -impl<'a, I> DifferentialTestsDriver<'a, I> where I: Iterator {} +impl<'a, I> Driver<'a, I> where I: Iterator {} -impl<'a> DifferentialTestsDriver<'a, StepsIterator> { +impl<'a> Driver<'a, StepsIterator> { // region:Constructors pub async fn new_root( test_definition: &'a TestDefinition<'a>, @@ -85,7 +85,7 @@ impl<'a> DifferentialTestsDriver<'a, StepsIterator> { test_definition: &'a TestDefinition<'a>, private_key_allocator: Arc>, cached_compiler: &CachedCompiler<'a>, - ) -> Result> { + ) -> Result> { let steps: Vec<(StepPath, Step)> = test_definition .case .steps_iterator() @@ -96,7 +96,7 @@ impl<'a> DifferentialTestsDriver<'a, StepsIterator> { .collect(); let steps_iterator: StepsIterator = steps.into_iter(); - DifferentialTestsPlatformDriver::new( + PlatformDriver::new( information, test_definition, private_key_allocator, @@ -125,7 +125,7 @@ impl<'a> DifferentialTestsDriver<'a, StepsIterator> { } /// The differential tests driver for a single platform. -pub struct DifferentialTestsPlatformDriver<'a, I> { +pub struct PlatformDriver<'a, I> { /// The information of the platform that this driver is for. platform_information: &'a TestPlatformInformation<'a>, @@ -147,7 +147,7 @@ pub struct DifferentialTestsPlatformDriver<'a, I> { steps_iterator: I, } -impl<'a, I> DifferentialTestsPlatformDriver<'a, I> +impl<'a, I> PlatformDriver<'a, I> where I: Iterator, { @@ -164,7 +164,7 @@ where Self::init_execution_state(platform_information, test_definition, cached_compiler) .await .context("Failed to initialize the execution state of the platform")?; - Ok(DifferentialTestsPlatformDriver { + Ok(PlatformDriver { platform_information, test_definition, private_key_allocator, @@ -330,7 +330,14 @@ where )] pub async fn execute_next_step(&mut self) -> Option> { let (step_path, step) = self.steps_iterator.next()?; - Some(self.execute_step(&step_path, &step).await) + + 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( @@ -455,7 +462,7 @@ where Method::Fallback | Method::FunctionName(_) => { let resolver = self.platform_information.node.resolver().await?; let tx = match step - .legacy_transaction(resolver.as_ref(), self.default_resolution_context()) + .as_transaction(resolver.as_ref(), self.default_resolution_context()) .await { Ok(tx) => tx, @@ -803,7 +810,7 @@ where step: &RepeatStep, ) -> Result { let tasks = (0..step.repeat) - .map(|_| DifferentialTestsPlatformDriver { + .map(|_| PlatformDriver { platform_information: self.platform_information, test_definition: self.test_definition, private_key_allocator: self.private_key_allocator.clone(), diff --git a/crates/core/src/differential_tests/entry_point.rs b/crates/core/src/differential_tests/entry_point.rs index 2d4a99c..0fea540 100644 --- a/crates/core/src/differential_tests/entry_point.rs +++ b/crates/core/src/differential_tests/entry_point.rs @@ -18,7 +18,7 @@ use revive_dt_config::{Context, TestExecutionContext}; use revive_dt_report::{Reporter, ReporterEvent, TestCaseStatus}; use crate::{ - differential_tests::DifferentialTestsDriver, + differential_tests::Driver, helpers::{CachedCompiler, NodePool, collect_metadata_files, create_test_definitions_stream}, }; @@ -51,7 +51,7 @@ pub async fn handle_differential_tests( for platform in platforms.iter() { let platform_identifier = platform.platform_identifier(); - let context = Context::ExecuteTests(Box::new(context.clone())); + let context = Context::Test(Box::new(context.clone())); let node_pool = NodePool::new(context, *platform) .await .inspect_err(|err| { @@ -71,7 +71,7 @@ pub async fn handle_differential_tests( info!("Spawned the platform nodes"); // Preparing test definitions. - let full_context = Context::ExecuteTests(Box::new(context.clone())); + let full_context = Context::Test(Box::new(context.clone())); let test_definitions = create_test_definitions_stream( &full_context, metadata_files.iter(), @@ -112,23 +112,20 @@ pub async fn handle_differential_tests( mode = %mode ); async move { - let driver = match DifferentialTestsDriver::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; - } - }; + 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 { @@ -149,6 +146,7 @@ pub async fn handle_differential_tests( .instrument(span) })) .inspect(|_| { + info!("Finished executing all test cases"); reporter_clone .report_completion_event() .expect("Can't fail") diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs deleted file mode 100644 index d6939c3..0000000 --- a/crates/core/src/driver/mod.rs +++ /dev/null @@ -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>, - - /// This map stores the contracts deployments for this case. - deployed_contracts: HashMap, - - /// This map stores the variables used for each one of the cases contained in the metadata - /// file. - variables: HashMap, - - /// 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>, -} - -impl CaseState { - pub fn new( - compiler_version: Version, - compiled_contracts: HashMap>, - deployed_contracts: HashMap, - execution_reporter: ExecutionSpecificReporter, - private_key_allocator: Arc>, - ) -> 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 { - 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> { - let mut instances_we_must_deploy = IndexMap::::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, - node: &dyn EthereumNode, - ) -> anyhow::Result { - 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 { - 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, - node: &dyn EthereumNode, - ) -> anyhow::Result<(Address, JsonAbi, Option)> { - 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::::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 { - 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, -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index dbf576b..b50e619 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -19,8 +19,6 @@ use revive_dt_node::{ use revive_dt_node_interaction::EthereumNode; use tracing::info; -pub mod driver; - /// A trait that describes the interface for the platforms that are supported by the tool. #[allow(clippy::type_complexity)] pub trait Platform { diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index 8f350bf..12cd8bb 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -1,3 +1,4 @@ +mod differential_benchmarks; mod differential_tests; mod helpers; @@ -11,7 +12,10 @@ use revive_dt_config::Context; use revive_dt_core::Platform; use revive_dt_format::metadata::Metadata; -use crate::differential_tests::handle_differential_tests; +use crate::{ + differential_benchmarks::handle_differential_benchmarks, + differential_tests::handle_differential_tests, +}; fn main() -> anyhow::Result<()> { let (writer, _guard) = tracing_appender::non_blocking::NonBlockingBuilder::default() @@ -37,7 +41,7 @@ fn main() -> anyhow::Result<()> { let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task(); match context { - Context::ExecuteTests(context) => tokio::runtime::Builder::new_multi_thread() + Context::Test(context) => tokio::runtime::Builder::new_multi_thread() .worker_threads(context.concurrency_configuration.number_of_threads) .enable_all() .build() @@ -49,6 +53,23 @@ fn main() -> anyhow::Result<()> { 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 => { diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index 0ae781a..d1e5050 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -14,8 +14,6 @@ revive-dt-common = { workspace = true } revive-common = { workspace = true } alloy = { workspace = true } -alloy-primitives = { workspace = true } -alloy-sol-types = { workspace = true } anyhow = { workspace = true } futures = { workspace = true } regex = { workspace = true } diff --git a/crates/format/src/steps.rs b/crates/format/src/steps.rs index 36dc7d6..19ab310 100644 --- a/crates/format/src/steps.rs +++ b/crates/format/src/steps.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, fmt::Display, str::FromStr}; +use alloy::primitives::{FixedBytes, utils::parse_units}; use alloy::{ eips::BlockNumberOrTag, json_abi::Function, @@ -7,7 +8,6 @@ use alloy::{ primitives::{Address, Bytes, U256}, rpc::types::TransactionRequest, }; -use alloy_primitives::{FixedBytes, utils::parse_units}; use anyhow::Context as _; use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream}; use schemars::JsonSchema; @@ -537,7 +537,7 @@ impl FunctionCallStep { } /// Parse this input into a legacy transaction. - pub async fn legacy_transaction( + pub async fn as_transaction( &self, resolver: &(impl ResolverApi + ?Sized), context: ResolutionContext<'_>, @@ -959,9 +959,9 @@ impl<'de> Deserialize<'de> for EtherValue { #[cfg(test)] 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_primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address}; - use alloy_sol_types::SolValue; use std::{collections::HashMap, pin::Pin}; use super::*; @@ -1115,7 +1115,7 @@ mod tests { let encoded = input.encoded_input(&resolver, context).await.unwrap(); 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(); assert_eq!( decoded.0, @@ -1162,7 +1162,7 @@ mod tests { let encoded = input.encoded_input(&resolver, context).await.unwrap(); 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(); assert_eq!( decoded.0, diff --git a/crates/format/src/traits.rs b/crates/format/src/traits.rs index ea6e888..5e1006a 100644 --- a/crates/format/src/traits.rs +++ b/crates/format/src/traits.rs @@ -3,8 +3,8 @@ use std::pin::Pin; use alloy::eips::BlockNumberOrTag; use alloy::json_abi::JsonAbi; +use alloy::primitives::TxHash; use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256}; -use alloy_primitives::TxHash; use anyhow::Result; use crate::metadata::{ContractIdent, ContractInstance}; diff --git a/crates/node-interaction/Cargo.toml b/crates/node-interaction/Cargo.toml index 361a109..f9c9261 100644 --- a/crates/node-interaction/Cargo.toml +++ b/crates/node-interaction/Cargo.toml @@ -15,6 +15,7 @@ revive-dt-format = { workspace = true } alloy = { workspace = true } anyhow = { workspace = true } +futures = { workspace = true } [lints] workspace = true diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index 86804dc..dbd2bf9 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -3,11 +3,12 @@ use std::pin::Pin; 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::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest}; use anyhow::Result; +use futures::Stream; use revive_common::EVMVersion; use revive_dt_format::traits::ResolverApi; @@ -22,6 +23,16 @@ pub trait EthereumNode { /// Returns the nodes connection string. fn connection_string(&self) -> &str; + fn submit_transaction( + &self, + transaction: TransactionRequest, + ) -> Pin> + '_>>; + + fn get_receipt( + &self, + tx_hash: TxHash, + ) -> Pin> + '_>>; + /// Execute the [TransactionRequest] and return a [TransactionReceipt]. fn execute_transaction( &self, @@ -53,4 +64,32 @@ pub trait EthereumNode { /// Returns the EVM version of the node. 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>>>> + + '_, + >, + >; +} + +#[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, } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index bf498e4..ae0d0ce 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -20,18 +20,25 @@ use alloy::{ network::{Ethereum, EthereumWallet, NetworkWallet}, primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256}, providers::{ - Provider, ProviderBuilder, + Identity, Provider, ProviderBuilder, RootProvider, ext::DebugApi, - fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, + fillers::{ + CachedNonceManager, ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, + WalletFiller, + }, }, rpc::types::{ - EIP1186AccountProofResponse, TransactionRequest, - trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, + EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest, + trace::geth::{ + DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame, + }, }, }; use anyhow::Context as _; +use futures::{Stream, StreamExt}; use revive_common::EVMVersion; -use tracing::{Instrument, instrument}; +use tokio::sync::OnceCell; +use tracing::{Instrument, error, instrument}; use revive_dt_common::{ fs::clear_directory, @@ -39,7 +46,7 @@ use revive_dt_common::{ }; use revive_dt_config::*; use revive_dt_format::traits::ResolverApi; -use revive_dt_node_interaction::EthereumNode; +use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation}; use crate::{ Node, @@ -71,6 +78,18 @@ pub struct GethNode { wallet: Arc, nonce_manager: CachedNonceManager, chain_id_filler: ChainIdFiller, + provider: OnceCell< + FillProvider< + JoinFill< + JoinFill< + JoinFill, ChainIdFiller>, + NonceFiller, + >, + WalletFiller>, + >, + RootProvider, + >, + >, } impl GethNode { @@ -121,6 +140,7 @@ impl GethNode { wallet: wallet.clone(), chain_id_filler: Default::default(), nonce_manager: Default::default(), + provider: Default::default(), } } @@ -235,7 +255,7 @@ impl GethNode { match process { Ok(process) => self.handle = Some(process), Err(err) => { - tracing::error!(?err, "Failed to start geth, shutting down gracefully"); + error!(?err, "Failed to start geth, shutting down gracefully"); self.shutdown() .context("Failed to gracefully shutdown after geth start error")?; return Err(err); @@ -247,21 +267,27 @@ impl GethNode { async fn provider( &self, - ) -> anyhow::Result, impl Provider, 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) + ) -> anyhow::Result< + FillProvider, impl Provider + Clone, Ethereum>, + > { + self.provider + .get_or_try_init(|| async move { + 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 + .map_err(Into::into) + }) .await - .map_err(Into::into) + .cloned() } } @@ -278,6 +304,50 @@ impl EthereumNode for GethNode { &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::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::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( level = "info", skip_all, @@ -287,8 +357,7 @@ impl EthereumNode for GethNode { fn execute_transaction( &self, transaction: TransactionRequest, - ) -> Pin> + '_>> - { + ) -> Pin> + '_>> { Box::pin(async move { let provider = self .provider() @@ -296,12 +365,12 @@ impl EthereumNode for GethNode { .context("Failed to create provider for transaction submission")?; let pending_transaction = provider - .send_transaction(transaction) - .await - .inspect_err( - |err| tracing::error!(%err, "Encountered an error when submitting the transaction"), - ) - .context("Failed to submit transaction to geth node")?; + .send_transaction(transaction) + .await + .inspect_err( + |err| error!(%err, "Encountered an error when submitting the transaction"), + ) + .context("Failed to submit transaction to geth node")?; let transaction_hash = *pending_transaction.tx_hash(); // The following is a fix for the "transaction indexing is in progress" error that we used @@ -321,7 +390,6 @@ impl EthereumNode for GethNode { // 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 // backoff each time we attempt to get the receipt and find that it's not available. - let provider = Arc::new(provider); poll( Self::RECEIPT_POLLING_DURATION, PollingWaitBehavior::Constant(Duration::from_millis(200)), @@ -355,14 +423,12 @@ impl EthereumNode for GethNode { &self, tx_hash: TxHash, trace_options: GethDebugTracingOptions, - ) -> Pin> + '_>> - { + ) -> Pin> + '_>> { Box::pin(async move { - let provider = Arc::new( - self.provider() - .await - .context("Failed to create provider for tracing")?, - ); + let provider = self + .provider() + .await + .context("Failed to create provider for tracing")?; poll( Self::TRACE_POLLING_DURATION, PollingWaitBehavior::Constant(Duration::from_millis(200)), @@ -460,6 +526,46 @@ impl EthereumNode for GethNode { fn evm_version(&self) -> EVMVersion { EVMVersion::Cancun } + + fn subscribe_to_full_blocks_information( + &self, + ) -> Pin< + Box< + dyn Future>>>> + + '_, + >, + > { + 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>>) + }) + } } pub struct GethNodeResolver, P: Provider> { diff --git a/crates/node/src/lighthouse_geth.rs b/crates/node/src/lighthouse_geth.rs index 5a51cb8..ebeef45 100644 --- a/crates/node/src/lighthouse_geth.rs +++ b/crates/node/src/lighthouse_geth.rs @@ -24,29 +24,36 @@ use std::{ }; use alloy::{ - eips::{BlockId, BlockNumberOrTag}, + eips::BlockNumberOrTag, genesis::{Genesis, GenesisAccount}, network::{Ethereum, EthereumWallet, NetworkWallet}, primitives::{ Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256, address, }, providers::{ - Provider, ProviderBuilder, + Identity, Provider, ProviderBuilder, RootProvider, ext::DebugApi, - fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, + fillers::{ + CachedNonceManager, ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, + WalletFiller, + }, }, rpc::{ client::{BuiltInConnectionString, ClientBuilder, RpcClient}, types::{ - EIP1186AccountProofResponse, TransactionRequest, - trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, + EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest, + trace::geth::{ + DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame, + }, }, }, }; use anyhow::Context as _; +use futures::{Stream, StreamExt}; use revive_common::EVMVersion; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::serde_as; +use tokio::sync::OnceCell; use tracing::{Instrument, info, instrument}; use revive_dt_common::{ @@ -55,7 +62,7 @@ use revive_dt_common::{ }; use revive_dt_config::*; use revive_dt_format::traits::ResolverApi; -use revive_dt_node_interaction::EthereumNode; +use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation}; use crate::{ Node, @@ -101,7 +108,31 @@ pub struct LighthouseGethNode { /* Provider Related Fields */ wallet: Arc, nonce_manager: CachedNonceManager, - chain_id_filler: ChainIdFiller, + + persistent_http_provider: OnceCell< + FillProvider< + JoinFill< + JoinFill< + JoinFill, ChainIdFiller>, + NonceFiller, + >, + WalletFiller>, + >, + RootProvider, + >, + >, + persistent_ws_provider: OnceCell< + FillProvider< + JoinFill< + JoinFill< + JoinFill, ChainIdFiller>, + NonceFiller, + >, + WalletFiller>, + >, + RootProvider, + >, + >, } impl LighthouseGethNode { @@ -170,7 +201,8 @@ impl LighthouseGethNode { /* Provider Related Fields */ wallet: wallet.clone(), nonce_manager: Default::default(), - chain_id_filler: Default::default(), + persistent_http_provider: OnceCell::const_new(), + persistent_ws_provider: OnceCell::const_new(), } } @@ -205,6 +237,8 @@ impl LighthouseGethNode { execution_layer_extra_parameters: vec![ "--nodiscover".to_string(), "--cache=4096".to_string(), + "--txlookuplimit=0".to_string(), + "--gcmode=archive".to_string(), "--txpool.globalslots=100000".to_string(), "--txpool.globalqueue=100000".to_string(), "--txpool.accountslots=128".to_string(), @@ -216,6 +250,7 @@ impl LighthouseGethNode { "--ws.port=8546".to_string(), "--ws.api=eth,net,web3,txpool,engine".to_string(), "--ws.origins=*".to_string(), + "--verbosity=4".to_string(), ], consensus_layer_extra_parameters: vec![ "--disable-deposit-contract-sync".to_string(), @@ -351,17 +386,34 @@ impl LighthouseGethNode { fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string), err(Debug), )] + #[allow(clippy::type_complexity)] async fn ws_provider( &self, - ) -> anyhow::Result, impl Provider, Ethereum>> - { - let client = ClientBuilder::default() - .connect_with(BuiltInConnectionString::Ws( - self.ws_connection_string.as_str().parse().unwrap(), - None, - )) - .await?; - Ok(self.provider(client)) + ) -> anyhow::Result< + FillProvider< + JoinFill< + JoinFill< + JoinFill, ChainIdFiller>, + NonceFiller, + >, + WalletFiller>, + >, + RootProvider, + >, + > { + self.persistent_ws_provider + .get_or_try_init(|| async move { + info!("Initializing the WS provider of the lighthouse node"); + let client = ClientBuilder::default() + .connect_with(BuiltInConnectionString::Ws( + self.ws_connection_string.as_str().parse().unwrap(), + None, + )) + .await?; + Ok(self.provider(client)) + }) + .await + .cloned() } #[instrument( @@ -370,22 +422,46 @@ impl LighthouseGethNode { 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, impl Provider, Ethereum>> - { - let client = ClientBuilder::default() - .connect_with(BuiltInConnectionString::Http( - self.http_connection_string.as_str().parse().unwrap(), - )) - .await?; - Ok(self.provider(client)) + ) -> anyhow::Result< + FillProvider< + JoinFill< + JoinFill< + JoinFill, ChainIdFiller>, + NonceFiller, + >, + WalletFiller>, + >, + RootProvider, + >, + > { + self.persistent_http_provider + .get_or_try_init(|| async move { + info!("Initializing the HTTP provider of the lighthouse node"); + let client = ClientBuilder::default() + .connect_with(BuiltInConnectionString::Http( + self.http_connection_string.as_str().parse().unwrap(), + )) + .await?; + Ok(self.provider(client)) + }) + .await + .cloned() } + #[allow(clippy::type_complexity)] fn provider( &self, rpc_client: RpcClient, - ) -> FillProvider, impl Provider, Ethereum> { + ) -> FillProvider< + JoinFill< + JoinFill, ChainIdFiller>, NonceFiller>, + WalletFiller>, + >, + RootProvider, + > { ProviderBuilder::new() .disable_recommended_fillers() .filler(FallbackGasFiller::new( @@ -393,7 +469,7 @@ impl LighthouseGethNode { 1_000_000_000, 1_000_000_000, )) - .filler(self.chain_id_filler.clone()) + .filler(ChainIdFiller::new(Some(420420420))) .filler(NonceFiller::new(self.nonce_manager.clone())) .wallet(self.wallet.clone()) .connect_client(rpc_client) @@ -407,60 +483,67 @@ impl LighthouseGethNode { err(Debug), )] async fn fund_all_accounts(&self) -> anyhow::Result<()> { - let mut providers = - futures::future::try_join_all((0..100).map(|_| self.ws_provider()).collect::>()) - .await - .context("Failed to create the providers")? - .into_iter() - .map(Arc::new) - .collect::>(); + let provider = self + .ws_provider() + .await + .context("Failed to create the WS provider")?; + let mut full_block_subscriber = provider + .subscribe_full_blocks() + .into_stream() + .await + .context("Full block subscriber")?; let mut tx_hashes = futures::future::try_join_all( NetworkWallet::::signer_addresses(self.wallet.as_ref()) .enumerate() .map(|(nonce, address)| { - let provider = providers[nonce % 100].clone(); + let provider = provider.clone(); async move { - let transaction = TransactionRequest::default() + let mut transaction = TransactionRequest::default() .from(self.prefunded_account_address) .to(address) .nonce(nonce as _) + .gas_limit(25_000_000) + .max_fee_per_gas(1_000_000_000) + .max_priority_fee_per_gas(1_000_000_000) .value(INITIAL_BALANCE.try_into().unwrap()); + transaction.chain_id = Some(420420420); provider .send_transaction(transaction) .await .map(|tx| *tx.tx_hash()) } - }) - .collect::>(), + }), ) .await .context("Failed to submit all transactions")? .into_iter() .collect::>(); - let provider = providers.pop().unwrap(); - let mut block_number = 0 as BlockNumber; - while !tx_hashes.is_empty() { - let Ok(Some(block)) = provider - .get_block(BlockId::Number(BlockNumberOrTag::Number(block_number))) - .await - else { + 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.header.timestamp, + block.timestamp = block_timestamp, + block.transaction_count = block_transaction_count, remaining_transactions = tx_hashes.len(), - "Discovered new block in funding accounts" + "Discovered new block when funding accounts" ); - block_number += 1 + if tx_hashes.is_empty() { + break; + } } Ok(()) @@ -468,11 +551,12 @@ impl LighthouseGethNode { fn internal_execute_transaction<'a>( transaction: TransactionRequest, - provider: Arc< - FillProvider + 'a, impl Provider + 'a, Ethereum>, + provider: FillProvider< + impl TxFiller + 'a, + impl Provider + Clone + 'a, + Ethereum, >, - ) -> Pin> + 'a>> - { + ) -> Pin> + 'a>> { Box::pin(async move { let pending_transaction = provider .send_transaction(transaction) @@ -552,17 +636,60 @@ impl EthereumNode for LighthouseGethNode { fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string), err, )] - fn execute_transaction( + fn submit_transaction( &self, transaction: TransactionRequest, - ) -> Pin> + '_>> - { + ) -> Pin> + '_>> { Box::pin(async move { let provider = self .ws_provider() .await - .map(Arc::new) - .context("Failed to create provider for transaction submission")?; + .context("Failed to create the provider for transaction submission")?; + tracing::trace!("Submit transaction, provider created"); + let pending_transaction = provider + .send_transaction(transaction) + .await + .context("Failed to submit the transaction through the provider")?; + tracing::trace!("Submitted transaction"); + 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::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::pin(async move { + let provider = self + .ws_provider() + .await + .context("Failed to create provider for transaction execution")?; Self::internal_execute_transaction(transaction, provider).await }) } @@ -572,8 +699,7 @@ impl EthereumNode for LighthouseGethNode { &self, tx_hash: TxHash, trace_options: GethDebugTracingOptions, - ) -> Pin> + '_>> - { + ) -> Pin> + '_>> { Box::pin(async move { let provider = Arc::new( self.http_provider() @@ -663,7 +789,7 @@ impl EthereumNode for LighthouseGethNode { }) } - // #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn resolver( &self, ) -> Pin>> + '_>> { @@ -677,6 +803,43 @@ impl EthereumNode for LighthouseGethNode { fn evm_version(&self) -> EVMVersion { EVMVersion::Cancun } + + fn subscribe_to_full_blocks_information( + &self, + ) -> Pin< + Box< + dyn Future>>>> + + '_, + >, + > { + 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>>) + }) + } } pub struct LighthouseGethNodeResolver, P: Provider> { diff --git a/crates/node/src/substrate.rs b/crates/node/src/substrate.rs index f4e61b8..4e61456 100644 --- a/crates/node/src/substrate.rs +++ b/crates/node/src/substrate.rs @@ -23,17 +23,23 @@ use alloy::{ TxHash, U256, }, providers::{ - Provider, ProviderBuilder, + Identity, Provider, ProviderBuilder, RootProvider, ext::DebugApi, - fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, + fillers::{ + CachedNonceManager, ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, + WalletFiller, + }, }, rpc::types::{ - EIP1186AccountProofResponse, TransactionReceipt, + EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest, eth::{Block, Header, Transaction}, - trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, + trace::geth::{ + DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame, + }, }, }; use anyhow::Context as _; +use futures::{Stream, StreamExt}; use revive_common::EVMVersion; use revive_dt_common::fs::clear_directory; use revive_dt_format::traits::ResolverApi; @@ -43,7 +49,8 @@ use sp_core::crypto::Ss58Codec; use sp_runtime::AccountId32; 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 crate::{ @@ -59,6 +66,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 /// to the command. #[derive(Debug)] + pub struct SubstrateNode { id: u32, node_binary: PathBuf, @@ -72,6 +80,20 @@ pub struct SubstrateNode { wallet: Arc, nonce_manager: CachedNonceManager, chain_id_filler: ChainIdFiller, + #[allow(clippy::type_complexity)] + provider: OnceCell< + FillProvider< + JoinFill< + JoinFill< + JoinFill, ChainIdFiller>, + NonceFiller, + >, + WalletFiller>, + >, + RootProvider, + ReviveNetwork, + >, + >, } impl SubstrateNode { @@ -123,6 +145,7 @@ impl SubstrateNode { wallet: wallet.clone(), chain_id_filler: Default::default(), nonce_manager: Default::default(), + provider: Default::default(), } } @@ -336,22 +359,31 @@ impl SubstrateNode { async fn provider( &self, ) -> anyhow::Result< - FillProvider, impl Provider, ReviveNetwork>, + FillProvider< + impl TxFiller, + impl Provider + Clone, + ReviveNetwork, + >, > { - ProviderBuilder::new() - .disable_recommended_fillers() - .network::() - .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.rpc_url) + self.provider + .get_or_try_init(|| async move { + ProviderBuilder::new() + .disable_recommended_fillers() + .network::() + .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.rpc_url) + .await + .map_err(Into::into) + }) .await - .map_err(Into::into) + .cloned() } } @@ -368,9 +400,41 @@ impl EthereumNode for SubstrateNode { &self.rpc_url } + fn submit_transaction( + &self, + transaction: TransactionRequest, + ) -> Pin> + '_>> { + 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::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: alloy::rpc::types::TransactionRequest, + transaction: TransactionRequest, ) -> Pin> + '_>> { Box::pin(async move { let receipt = self @@ -391,8 +455,7 @@ impl EthereumNode for SubstrateNode { &self, tx_hash: TxHash, trace_options: GethDebugTracingOptions, - ) -> Pin> + '_>> - { + ) -> Pin> + '_>> { Box::pin(async move { self.provider() .await @@ -467,6 +530,46 @@ impl EthereumNode for SubstrateNode { fn evm_version(&self) -> EVMVersion { EVMVersion::Cancun } + + fn subscribe_to_full_blocks_information( + &self, + ) -> Pin< + Box< + dyn Future>>>> + + '_, + >, + > { + 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, + transaction_hashes: block + .transactions + .into_hashes() + .as_hashes() + .expect("Must be hashes") + .to_vec(), + }) + }); + + Ok(Box::pin(mined_block_information_stream) + as Pin>>) + }) + } } pub struct SubstrateNodeResolver, P: Provider> { diff --git a/crates/report/Cargo.toml b/crates/report/Cargo.toml index eae7fa7..9ba6e41 100644 --- a/crates/report/Cargo.toml +++ b/crates/report/Cargo.toml @@ -13,7 +13,7 @@ revive-dt-config = { workspace = true } revive-dt-format = { workspace = true } revive-dt-compiler = { workspace = true } -alloy-primitives = { workspace = true } +alloy = { workspace = true } anyhow = { workspace = true } paste = { workspace = true } indexmap = { workspace = true, features = ["serde"] } diff --git a/crates/report/src/aggregator.rs b/crates/report/src/aggregator.rs index 642237d..bb235eb 100644 --- a/crates/report/src/aggregator.rs +++ b/crates/report/src/aggregator.rs @@ -8,7 +8,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use alloy_primitives::Address; +use alloy::primitives::Address; use anyhow::{Context as _, Result}; use indexmap::IndexMap; use revive_dt_common::types::PlatformIdentifier; @@ -106,7 +106,10 @@ impl ReportAggregator { RunnerEvent::ContractDeployed(event) => { self.handle_contract_deployed_event(*event); } - RunnerEvent::Completion(event) => self.handle_completion(*event), + RunnerEvent::Completion(event) => { + self.handle_completion(*event); + break; + } } } debug!("Report aggregation completed"); diff --git a/crates/report/src/runner_event.rs b/crates/report/src/runner_event.rs index 184dcdd..f6d322f 100644 --- a/crates/report/src/runner_event.rs +++ b/crates/report/src/runner_event.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; -use alloy_primitives::Address; +use alloy::primitives::Address; use anyhow::Context as _; use indexmap::IndexMap; use revive_dt_common::types::PlatformIdentifier; diff --git a/run_tests.sh b/run_tests.sh index daf7511..fe0dfe1 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -89,8 +89,7 @@ echo "This may take a while..." echo "" # Run the tool -RUST_LOG="info,alloy_pubsub::service=error" cargo run --release -- execute-tests \ - --platform geth-evm-solc \ +RUST_LOG="info,alloy_pubsub::service=error" cargo run --release -- test \ --platform revive-dev-node-polkavm-resolc \ --corpus "$CORPUS_FILE" \ --working-directory "$WORKDIR" \