diff --git a/Cargo.lock b/Cargo.lock index bae4906..aa09b1e 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", @@ -4618,6 +4658,7 @@ version = "0.1.0" dependencies = [ "alloy", "anyhow", + "futures", "revive-common", "revive-dt-common", "revive-dt-config", @@ -4631,6 +4672,7 @@ dependencies = [ "sp-runtime", "temp-dir", "tokio", + "tower", "tracing", ] @@ -4640,6 +4682,7 @@ version = "0.1.0" dependencies = [ "alloy", "anyhow", + "futures", "revive-common", "revive-dt-format", ] @@ -4648,7 +4691,7 @@ dependencies = [ name = "revive-dt-report" version = "0.1.0" dependencies = [ - "alloy-primitives", + "alloy", "anyhow", "indexmap 2.10.0", "paste", @@ -5114,10 +5157,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", ] @@ -5131,10 +5175,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", @@ -5222,7 +5275,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", @@ -6218,8 +6271,10 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 32c59f2..b18c827 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" } @@ -59,6 +57,7 @@ tokio = { version = "1.47.0", default-features = false, features = [ "process", "rt", ] } +tower = { version = "0.5.2", features = ["limit"] } uuid = { version = "1.8", features = ["v4"] } tracing = { version = "0.1.41" } tracing-appender = { version = "0.2.3" } @@ -75,13 +74,14 @@ revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" } [workspace.dependencies.alloy] -version = "1.0.22" +version = "1.0.37" default-features = false features = [ "json-abi", "providers", - "provider-ipc", "provider-ws", + "provider-ipc", + "provider-http", "provider-debug-api", "reqwest", "rpc-types", @@ -91,6 +91,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/mod.rs b/crates/common/src/types/mod.rs index 2811086..8019524 100644 --- a/crates/common/src/types/mod.rs +++ b/crates/common/src/types/mod.rs @@ -1,9 +1,11 @@ mod identifiers; mod mode; mod private_key_allocator; +mod round_robin_pool; mod version_or_requirement; pub use identifiers::*; pub use mode::*; pub use private_key_allocator::*; +pub use round_robin_pool::*; pub use version_or_requirement::*; 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/common/src/types/round_robin_pool.rs b/crates/common/src/types/round_robin_pool.rs new file mode 100644 index 0000000..81882c1 --- /dev/null +++ b/crates/common/src/types/round_robin_pool.rs @@ -0,0 +1,24 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +pub struct RoundRobinPool { + next_index: AtomicUsize, + items: Vec, +} + +impl RoundRobinPool { + pub fn new(items: Vec) -> Self { + Self { + next_index: Default::default(), + items, + } + } + + pub fn round_robin(&self) -> &T { + let current = self.next_index.fetch_add(1, Ordering::SeqCst) % self.items.len(); + self.items.get(current).unwrap() + } + + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } +} 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/compiler/src/revive_resolc.rs b/crates/compiler/src/revive_resolc.rs index 79a32fb..f0025ea 100644 --- a/crates/compiler/src/revive_resolc.rs +++ b/crates/compiler/src/revive_resolc.rs @@ -16,6 +16,7 @@ use revive_solc_json_interface::{ SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection, SolcStandardJsonOutput, }; +use tracing::{Span, field::display}; use crate::{ CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc, @@ -80,6 +81,16 @@ impl SolidityCompiler for Resolc { } #[tracing::instrument(level = "debug", ret)] + #[tracing::instrument( + level = "error", + skip_all, + fields( + resolc_version = %self.version(), + solc_version = %self.0.solc.version(), + json_in = tracing::field::Empty + ), + err(Debug) + )] fn build( &self, CompilerInput { @@ -141,6 +152,7 @@ impl SolidityCompiler for Resolc { polkavm: None, }, }; + Span::current().record("json_in", display(serde_json::to_string(&input).unwrap())); let path = &self.0.resolc_path; let mut command = AsyncCommand::new(path); @@ -148,6 +160,8 @@ impl SolidityCompiler for Resolc { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) + .arg("--solc") + .arg(self.0.solc.path()) .arg("--standard-json"); if let Some(ref base_path) = base_path { diff --git a/crates/compiler/src/solc.rs b/crates/compiler/src/solc.rs index defdb19..9a825ad 100644 --- a/crates/compiler/src/solc.rs +++ b/crates/compiler/src/solc.rs @@ -10,8 +10,9 @@ use std::{ use dashmap::DashMap; use revive_dt_common::types::VersionOrRequirement; -use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration}; +use revive_dt_config::{SolcConfiguration, WorkingDirectoryConfiguration}; use revive_dt_solc_binaries::download_solc; +use tracing::{Span, field::display, info}; use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler}; @@ -39,9 +40,7 @@ struct SolcInner { impl Solc { pub async fn new( - context: impl AsRef - + AsRef - + AsRef, + context: impl AsRef + AsRef, version: impl Into>, ) -> Result { // This is a cache for the compiler objects so that whenever the same compiler version is @@ -69,6 +68,11 @@ impl Solc { Ok(COMPILERS_CACHE .entry((path.clone(), version.clone())) .or_insert_with(|| { + info!( + solc_path = %path.display(), + solc_version = %version, + "Created a new solc compiler object" + ); Self(Arc::new(SolcInner { solc_path: path, solc_version: version, @@ -88,6 +92,12 @@ impl SolidityCompiler for Solc { } #[tracing::instrument(level = "debug", ret)] + #[tracing::instrument( + level = "error", + skip_all, + fields(json_in = tracing::field::Empty), + err(Debug) + )] fn build( &self, CompilerInput { @@ -166,12 +176,14 @@ impl SolidityCompiler for Solc { }, }; + Span::current().record("json_in", display(serde_json::to_string(&input).unwrap())); + let path = &self.0.solc_path; let mut command = AsyncCommand::new(path); command .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + .stderr(Stdio::null()) .arg("--standard-json"); if let Some(ref base_path) = base_path { @@ -205,20 +217,18 @@ impl SolidityCompiler for Solc { if !output.status.success() { let json_in = serde_json::to_string_pretty(&input) .context("Failed to pretty-print Standard JSON input for logging")?; - let message = String::from_utf8_lossy(&output.stderr); tracing::error!( status = %output.status, - message = %message, json_input = json_in, "Compilation using solc failed" ); - anyhow::bail!("Compilation failed with an error: {message}"); + anyhow::bail!("Compilation failed"); } let parsed = serde_json::from_slice::(&output.stdout) .map_err(|e| { anyhow::anyhow!( - "failed to parse resolc JSON output: {e}\nstderr: {}", + "failed to parse resolc JSON output: {e}\nstdout: {}", String::from_utf8_lossy(&output.stdout) ) }) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index b1551bc..f7a4651 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,18 @@ 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!(), + } + } +} + +impl AsRef for Context { + fn as_ref(&self) -> &CorpusConfiguration { + match self { + Self::Test(context) => context.as_ref().as_ref(), + Self::Benchmark(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } @@ -55,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!(), } } @@ -64,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!(), } } @@ -73,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!(), } } @@ -82,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!(), } } @@ -91,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!(), } } @@ -100,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!(), } } @@ -109,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!(), } } @@ -118,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!(), } } @@ -127,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!(), } } @@ -136,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!(), } } @@ -145,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!(), } } @@ -154,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!(), } } @@ -183,9 +213,9 @@ pub struct TestExecutionContext { )] pub platforms: Vec, - /// A list of test corpus JSON files to be tested. - #[arg(long = "corpus", short)] - pub corpus: Vec, + /// Configuration parameters for the corpus files to use. + #[clap(flatten, next_help_heading = "Corpus Configuration")] + pub corpus_configuration: CorpusConfiguration, /// Configuration parameters for the solc compiler. #[clap(flatten, next_help_heading = "Solc Configuration")] @@ -236,6 +266,83 @@ pub struct TestExecutionContext { pub report_configuration: ReportConfiguration, } +#[derive(Clone, Debug, Parser, Serialize)] +pub struct BenchmarkingContext { + /// The working directory that the program will use for all of the temporary artifacts needed at + /// runtime. + /// + /// If not specified, then a temporary directory will be created and used by the program for all + /// temporary artifacts. + #[clap( + short, + long, + default_value = "", + value_hint = ValueHint::DirPath, + )] + pub working_directory: WorkingDirectoryConfiguration, + + /// The set of platforms that the differential tests should run on. + #[arg( + short = 'p', + long = "platform", + default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"] + )] + pub platforms: Vec, + + /// The default repetition count for any workload specified but that doesn't contain a repeat + /// step. + #[arg(short = 'r', long = "default-repetition-count", default_value_t = 1000)] + pub default_repetition_count: usize, + + /// Configuration parameters for the corpus files to use. + #[clap(flatten, next_help_heading = "Corpus Configuration")] + pub corpus_configuration: CorpusConfiguration, + + /// Configuration parameters for the solc compiler. + #[clap(flatten, next_help_heading = "Solc Configuration")] + pub solc_configuration: SolcConfiguration, + + /// Configuration parameters for the resolc compiler. + #[clap(flatten, next_help_heading = "Resolc Configuration")] + pub resolc_configuration: ResolcConfiguration, + + /// Configuration parameters for the geth node. + #[clap(flatten, next_help_heading = "Geth Configuration")] + pub geth_configuration: GethConfiguration, + + /// Configuration parameters for the lighthouse node. + #[clap(flatten, next_help_heading = "Lighthouse Configuration")] + pub lighthouse_configuration: KurtosisConfiguration, + + /// Configuration parameters for the Kitchensink. + #[clap(flatten, next_help_heading = "Kitchensink Configuration")] + pub kitchensink_configuration: KitchensinkConfiguration, + + /// Configuration parameters for the Revive Dev Node. + #[clap(flatten, next_help_heading = "Revive Dev Node Configuration")] + pub revive_dev_node_configuration: ReviveDevNodeConfiguration, + + /// Configuration parameters for the Eth Rpc. + #[clap(flatten, next_help_heading = "Eth RPC Configuration")] + pub eth_rpc_configuration: EthRpcConfiguration, + + /// Configuration parameters for the wallet. + #[clap(flatten, next_help_heading = "Wallet Configuration")] + pub wallet_configuration: WalletConfiguration, + + /// Configuration parameters for concurrency. + #[clap(flatten, next_help_heading = "Concurrency Configuration")] + pub concurrency_configuration: ConcurrencyConfiguration, + + /// Configuration parameters for the compilers and compilation. + #[clap(flatten, next_help_heading = "Compilation Configuration")] + pub compilation_configuration: CompilationConfiguration, + + /// Configuration parameters for the report. + #[clap(flatten, next_help_heading = "Report Configuration")] + pub report_configuration: ReportConfiguration, +} + impl Default for TestExecutionContext { fn default() -> Self { Self::parse_from(["execution-context"]) @@ -248,6 +355,12 @@ impl AsRef for TestExecutionContext { } } +impl AsRef for TestExecutionContext { + fn as_ref(&self) -> &CorpusConfiguration { + &self.corpus_configuration + } +} + impl AsRef for TestExecutionContext { fn as_ref(&self) -> &SolcConfiguration { &self.solc_configuration @@ -320,6 +433,98 @@ impl AsRef for TestExecutionContext { } } +impl Default for BenchmarkingContext { + fn default() -> Self { + Self::parse_from(["execution-context"]) + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &WorkingDirectoryConfiguration { + &self.working_directory + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &CorpusConfiguration { + &self.corpus_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &SolcConfiguration { + &self.solc_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &ResolcConfiguration { + &self.resolc_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &GethConfiguration { + &self.geth_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &KurtosisConfiguration { + &self.lighthouse_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &KitchensinkConfiguration { + &self.kitchensink_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &ReviveDevNodeConfiguration { + &self.revive_dev_node_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &EthRpcConfiguration { + &self.eth_rpc_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &WalletConfiguration { + &self.wallet_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &ConcurrencyConfiguration { + &self.concurrency_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &CompilationConfiguration { + &self.compilation_configuration + } +} + +impl AsRef for BenchmarkingContext { + fn as_ref(&self) -> &ReportConfiguration { + &self.report_configuration + } +} + +/// A set of configuration parameters for the corpus files to use for the execution. +#[derive(Clone, Debug, Parser, Serialize)] +pub struct CorpusConfiguration { + /// A list of test corpus JSON files to be tested. + #[arg(short = 'c', long = "corpus")] + pub paths: Vec, +} + /// A set of configuration parameters for Solc. #[derive(Clone, Debug, Parser, Serialize)] pub struct SolcConfiguration { @@ -397,10 +602,6 @@ pub struct KitchensinkConfiguration { value_parser = parse_duration )] pub start_timeout_ms: Duration, - - /// This configures the tool to use Kitchensink instead of using the revive-dev-node. - #[clap(long = "kitchensink.dont-use-dev-node")] - pub use_kitchensink: bool, } /// A set of configuration parameters for the revive dev node. @@ -448,7 +649,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. /// @@ -499,8 +700,8 @@ pub struct WalletConfiguration { /// This argument controls which private keys the nodes should have access to and be added to /// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set /// of the node. - #[clap(long = "wallet.additional-keys", default_value_t = 200)] - additional_keys: usize, + #[clap(long = "wallet.additional-keys", default_value_t = 100_000)] + pub additional_keys: usize, /// The wallet object that will be used. #[clap(skip)] diff --git a/crates/core/src/differential_benchmarks/driver.rs b/crates/core/src/differential_benchmarks/driver.rs new file mode 100644 index 0000000..abdc6fd --- /dev/null +++ b/crates/core/src/differential_benchmarks/driver.rs @@ -0,0 +1,770 @@ +use std::{ + collections::HashMap, + ops::ControlFlow, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + time::Duration, +}; + +use alloy::{ + hex, + json_abi::JsonAbi, + network::{Ethereum, TransactionBuilder}, + primitives::{Address, TxHash, U256}, + rpc::types::{ + TransactionReceipt, TransactionRequest, + trace::geth::{ + CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType, + GethDebugTracingOptions, + }, + }, +}; +use anyhow::{Context as _, Result, bail}; +use indexmap::IndexMap; +use revive_dt_common::{ + futures::{PollingWaitBehavior, poll}, + types::PrivateKeyAllocator, +}; +use revive_dt_format::{ + metadata::{ContractInstance, ContractPathAndIdent}, + steps::{ + AllocateAccountStep, BalanceAssertionStep, Calldata, EtherValue, FunctionCallStep, Method, + RepeatStep, Step, StepAddress, StepIdx, StepPath, StorageEmptyAssertionStep, + }, + traits::{ResolutionContext, ResolverApi}, +}; +use tokio::sync::{Mutex, mpsc::UnboundedSender}; +use tracing::{Instrument, Span, debug, error, field::display, info, info_span, instrument}; + +use crate::{ + differential_benchmarks::{ExecutionState, WatcherEvent}, + helpers::{CachedCompiler, TestDefinition, TestPlatformInformation}, +}; + +static DRIVER_COUNT: AtomicUsize = AtomicUsize::new(0); + +/// The differential tests driver for a single platform. +pub struct Driver<'a, I> { + /// The id of the driver. + driver_id: usize, + + /// The information of the platform that this driver is for. + platform_information: &'a TestPlatformInformation<'a>, + + /// The resolver of the platform. + resolver: Arc, + + /// 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 { + driver_id: DRIVER_COUNT.fetch_add(1, Ordering::SeqCst), + platform_information, + resolver: platform_information + .node + .resolver() + .await + .context("Failed to create resolver")?, + test_definition, + private_key_allocator, + execution_state: ExecutionState::empty(), + steps_executed: 0, + steps_iterator: steps, + watcher_tx, + }; + this.init_execution_state(cached_compiler) + .await + .context("Failed to initialize the execution state of the platform")?; + Ok(this) + } + + async fn init_execution_state(&mut self, cached_compiler: &CachedCompiler<'a>) -> Result<()> { + let compiler_output = cached_compiler + .compile_contracts( + self.test_definition.metadata, + self.test_definition.metadata_file_path, + self.test_definition.mode.clone(), + None, + self.platform_information.compiler.as_ref(), + self.platform_information.platform, + &self.platform_information.reporter, + ) + .await + .inspect_err(|err| { + error!( + ?err, + platform_identifier = %self.platform_information.platform.platform_identifier(), + "Pre-linking compilation failed" + ) + }) + .context("Failed to produce the pre-linking compiled contracts")?; + + let mut deployed_libraries = None::>; + 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( + driver_id = self.driver_id, + platform_identifier = %self.platform_information.platform.platform_identifier(), + %step_path, + ), + err(Debug), + )] + async fn execute_step(&mut self, step_path: &StepPath, step: &Step) -> Result<()> { + let steps_executed = match step { + Step::FunctionCall(step) => self + .execute_function_call(step_path, step.as_ref()) + .await + .context("Function call step Failed"), + Step::Repeat(step) => self + .execute_repeat_step(step_path, step.as_ref()) + .await + .context("Repetition Step Failed"), + Step::AllocateAccount(step) => self + .execute_account_allocation(step_path, step.as_ref()) + .await + .context("Account Allocation Step Failed"), + // The following steps are disabled in the benchmarking driver. + Step::BalanceAssertion(..) | Step::StorageEmptyAssertion(..) => Ok(0), + }?; + self.steps_executed += steps_executed; + Ok(()) + } + + #[instrument(level = "info", skip_all, fields(driver_id = self.driver_id))] + pub async fn execute_function_call( + &mut self, + _: &StepPath, + step: &FunctionCallStep, + ) -> Result { + 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> { + 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); + } + } + + Ok(receipts) + } + + async fn handle_function_call_execution( + &mut self, + step: &FunctionCallStep, + mut deployment_receipts: HashMap, + ) -> Result { + match step.method { + // This step was already executed when `handle_step` was called. We just need to + // lookup the transaction receipt in this case and continue on. + Method::Deployer => deployment_receipts + .remove(&step.instance) + .context("Failed to find deployment receipt for constructor call"), + Method::Fallback | Method::FunctionName(_) => { + let tx = step + .as_transaction(self.resolver.as_ref(), self.default_resolution_context()) + .await?; + self.execute_transaction(tx).await + } + } + } + + async fn handle_function_call_call_frame_tracing( + &mut self, + tx_hash: TxHash, + ) -> Result { + self.platform_information + .node + .trace_transaction( + tx_hash, + GethDebugTracingOptions { + tracer: Some(GethDebugTracerType::BuiltInTracer( + GethDebugBuiltInTracerType::CallTracer, + )), + tracer_config: GethDebugTracerConfig(serde_json::json! {{ + "onlyTopCall": true, + "withLog": false, + "withStorage": false, + "withMemory": false, + "withStack": false, + "withReturnData": true + }}), + ..Default::default() + }, + ) + .await + .map(|trace| { + trace + .try_into_call_frame() + .expect("Impossible - we requested a callframe trace so we must get it back") + }) + } + + async fn handle_function_call_variable_assignment( + &mut self, + step: &FunctionCallStep, + tracing_result: &CallFrame, + ) -> Result<()> { + let Some(ref assignments) = step.variable_assignments else { + return Ok(()); + }; + + // Handling the return data variable assignments. + for (variable_name, output_word) in assignments.return_data.iter().zip( + tracing_result + .output + .as_ref() + .unwrap_or_default() + .to_vec() + .chunks(32), + ) { + let value = U256::from_be_slice(output_word); + self.execution_state + .variables + .insert(variable_name.clone(), value); + tracing::info!( + variable_name, + variable_value = hex::encode(value.to_be_bytes::<32>()), + "Assigned variable" + ); + } + + Ok(()) + } + + #[instrument(level = "info", skip_all, fields(driver_id = self.driver_id))] + pub async fn execute_balance_assertion( + &mut self, + _: &StepPath, + _: &BalanceAssertionStep, + ) -> anyhow::Result { + // Kept empty intentionally for the benchmark driver. + Ok(1) + } + + #[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))] + async fn execute_storage_empty_assertion_step( + &mut self, + _: &StepPath, + _: &StorageEmptyAssertionStep, + ) -> Result { + // Kept empty intentionally for the benchmark driver. + Ok(1) + } + + #[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))] + async fn execute_repeat_step( + &mut self, + step_path: &StepPath, + step: &RepeatStep, + ) -> Result { + let tasks = (0..step.repeat) + .map(|_| Driver { + driver_id: DRIVER_COUNT.fetch_add(1, Ordering::SeqCst), + platform_information: self.platform_information, + resolver: self.resolver.clone(), + test_definition: self.test_definition, + private_key_allocator: self.private_key_allocator.clone(), + execution_state: self.execution_state.clone(), + steps_executed: 0, + steps_iterator: { + let steps = step + .steps + .iter() + .cloned() + .enumerate() + .map(|(step_idx, step)| { + let step_idx = StepIdx::new(step_idx); + let step_path = step_path.append(step_idx); + (step_path, step) + }) + .collect::>(); + steps.into_iter() + }, + watcher_tx: self.watcher_tx.clone(), + }) + .map(|driver| driver.execute_all()); + + // TODO: Determine how we want to know the `ignore_block_before` and if it's through the + // receipt and how this would impact the architecture and the possibility of us not waiting + // for receipts in the future. + self.watcher_tx + .send(WatcherEvent::RepetitionStartEvent { + ignore_block_before: 0, + }) + .context("Failed to send message on the watcher's tx")?; + + let res = futures::future::try_join_all(tasks) + .await + .context("Repetition execution failed")?; + Ok(res.into_iter().sum()) + } + + #[instrument(level = "info", fields(driver_id = self.driver_id), skip_all, err(Debug))] + pub async fn execute_account_allocation( + &mut self, + _: &StepPath, + step: &AllocateAccountStep, + ) -> Result { + let Some(variable_name) = step.variable_name.strip_prefix("$VARIABLE:") else { + bail!("Account allocation must start with $VARIABLE:"); + }; + + let private_key = self + .private_key_allocator + .lock() + .await + .allocate() + .context("Account allocation through the private key allocator failed")?; + let account = private_key.address(); + let variable = U256::from_be_slice(account.0.as_slice()); + + self.execution_state + .variables + .insert(variable_name.to_string(), variable); + + Ok(1) + } + // endregion:Step Handling + + // region:Contract Deployment + #[instrument( + level = "info", + skip_all, + fields( + driver_id = self.driver_id, + platform_identifier = %self.platform_information.platform.platform_identifier(), + %contract_instance, + %deployer + ), + err(Debug), + )] + async fn get_or_deploy_contract_instance( + &mut self, + contract_instance: &ContractInstance, + deployer: Address, + calldata: Option<&Calldata>, + value: Option, + ) -> 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( + driver_id = self.driver_id, + platform_identifier = %self.platform_information.platform.platform_identifier(), + %contract_instance, + %deployer + ), + err(Debug), + )] + async fn deploy_contract( + &mut self, + contract_instance: &ContractInstance, + deployer: Address, + calldata: Option<&Calldata>, + value: Option, + ) -> 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", fields(driver_id = self.driver_id), 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(driver_id = self.driver_id, transaction_hash = tracing::field::Empty) + )] + async fn execute_transaction( + &self, + transaction: TransactionRequest, + ) -> anyhow::Result { + let node = self.platform_information.node; + let transaction_hash = node + .submit_transaction(transaction) + .await + .context("Failed to submit transaction")?; + Span::current().record("transaction_hash", display(transaction_hash)); + + info!("Submitted transaction"); + self.watcher_tx + .send(WatcherEvent::SubmittedTransaction { transaction_hash }) + .context("Failed to send the transaction hash to the watcher")?; + + info!("Starting to poll for transaction receipt"); + poll( + Duration::from_secs(30 * 60), + PollingWaitBehavior::Constant(Duration::from_secs(1)), + || { + async move { + match node.get_receipt(transaction_hash).await { + Ok(receipt) => { + info!("Polling succeeded, receipt found"); + Ok(ControlFlow::Break(receipt)) + } + Err(_) => Ok(ControlFlow::Continue(())), + } + } + .instrument(info_span!("Polling for receipt")) + }, + ) + .await + } + // endregion:Transaction Execution +} 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..12ea840 --- /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 stderr = std::io::stderr().lock(); + writeln!( + stderr, + "Watcher information for {}", + self.platform_identifier + )?; + writeln!( + stderr, + "block_number,block_timestamp,mined_gas,block_gas_limit,tx_count" + )?; + for block in mined_blocks_information { + writeln!( + stderr, + "{},{},{},{},{}", + block.block_number, + block.block_timestamp, + block.mined_gas, + block.block_gas_limit, + block.transaction_hashes.len() + )? + } + } + // endregion:TEMPORARY + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum WatcherEvent { + /// Informs the watcher that it should begin watching for the blocks mined by the platforms. + /// Before the watcher receives this event it will not be watching for the mined blocks. The + /// reason behind this is that we do not want the initialization transactions (e.g., contract + /// deployments) to be included in the overall TPS and GPS measurements since these blocks will + /// most likely only contain a single transaction since they're just being used for + /// initialization. + RepetitionStartEvent { + /// This is the block number of the last block seen before the repetition started. This is + /// used to instruct the watcher to ignore all block prior to this block when it starts + /// streaming the blocks. + ignore_block_before: BlockNumber, + }, + + /// Informs the watcher that a transaction was submitted and that the watcher should watch for a + /// transaction with this hash in the blocks that it watches. + SubmittedTransaction { + /// The hash of the submitted transaction. + transaction_hash: TxHash, + }, + + /// Informs the watcher that all of the transactions of this benchmark have been submitted and + /// that it can expect to receive no further transaction hashes and not even watch the channel + /// any longer. + AllTransactionsSubmitted, +} diff --git a/crates/core/src/differential_tests/driver.rs b/crates/core/src/differential_tests/driver.rs new file mode 100644 index 0000000..deb6d15 --- /dev/null +++ b/crates/core/src/differential_tests/driver.rs @@ -0,0 +1,1044 @@ +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; + +use alloy::{ + consensus::EMPTY_ROOT_HASH, + 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 futures::TryStreamExt; +use indexmap::IndexMap; +use revive_dt_common::types::{PlatformIdentifier, PrivateKeyAllocator}; +use revive_dt_format::{ + metadata::{ContractInstance, ContractPathAndIdent}, + steps::{ + AllocateAccountStep, BalanceAssertionStep, Calldata, EtherValue, Expected, ExpectedOutput, + FunctionCallStep, Method, RepeatStep, Step, StepAddress, StepIdx, StepPath, + StorageEmptyAssertionStep, + }, + traits::ResolutionContext, +}; +use tokio::sync::Mutex; +use tracing::{debug, error, info, instrument}; + +use crate::{ + differential_tests::ExecutionState, + helpers::{CachedCompiler, TestDefinition, TestPlatformInformation}, +}; + +type StepsIterator = std::vec::IntoIter<(StepPath, Step)>; + +pub struct Driver<'a, I> { + /// The drivers for the various platforms that we're executing the tests on. + platform_drivers: BTreeMap>, +} + +impl<'a, I> Driver<'a, I> where I: Iterator {} + +impl<'a> Driver<'a, StepsIterator> { + // region:Constructors + pub async fn new_root( + test_definition: &'a TestDefinition<'a>, + private_key_allocator: Arc>, + cached_compiler: &CachedCompiler<'a>, + ) -> Result { + let platform_drivers = futures::future::try_join_all(test_definition.platforms.iter().map( + |(identifier, information)| { + let identifier = *identifier; + let private_key_allocator = private_key_allocator.clone(); + async move { + Self::create_platform_driver( + identifier, + information, + test_definition, + private_key_allocator, + cached_compiler, + ) + .await + .map(|driver| (identifier, driver)) + } + }, + )) + .await + .context("Failed to create the drivers for the various platforms")? + .into_iter() + .collect::>(); + + Ok(Self { platform_drivers }) + } + + async fn create_platform_driver( + identifier: PlatformIdentifier, + information: &'a TestPlatformInformation<'a>, + test_definition: &'a TestDefinition<'a>, + private_key_allocator: Arc>, + cached_compiler: &CachedCompiler<'a>, + ) -> Result> { + let steps: Vec<(StepPath, Step)> = test_definition + .case + .steps_iterator() + .enumerate() + .map(|(step_idx, step)| -> (StepPath, Step) { + (StepPath::new(vec![StepIdx::new(step_idx)]), step) + }) + .collect(); + let steps_iterator: StepsIterator = steps.into_iter(); + + PlatformDriver::new( + information, + test_definition, + private_key_allocator, + cached_compiler, + steps_iterator, + ) + .await + .context(format!("Failed to create driver for {identifier}")) + } + // endregion:Constructors + + // region:Execution + #[instrument(level = "info", skip_all)] + pub async fn execute_all(mut self) -> Result { + let platform_drivers = std::mem::take(&mut self.platform_drivers); + let results = futures::future::try_join_all( + platform_drivers + .into_values() + .map(|driver| driver.execute_all()), + ) + .await + .context("Failed to execute all of the steps on the driver")?; + Ok(results.first().copied().unwrap_or_default()) + } + // endregion:Execution +} + +/// The differential tests driver for a single platform. +pub struct PlatformDriver<'a, I> { + /// The information of the platform that this driver is for. + platform_information: &'a TestPlatformInformation<'a>, + + /// The definition of the test that the driver is instructed to execute. + test_definition: &'a TestDefinition<'a>, + + /// The private key allocator used by this driver and other drivers when account allocations are + /// needed. + private_key_allocator: Arc>, + + /// The execution state associated with the platform. + execution_state: ExecutionState, + + /// 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> PlatformDriver<'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>, + steps: I, + ) -> Result { + let execution_state = + Self::init_execution_state(platform_information, test_definition, cached_compiler) + .await + .context("Failed to initialize the execution state of the platform")?; + Ok(PlatformDriver { + platform_information, + test_definition, + private_key_allocator, + execution_state, + steps_executed: 0, + steps_iterator: steps, + }) + } + + async fn init_execution_state( + platform_information: &'a TestPlatformInformation<'a>, + test_definition: &'a TestDefinition<'a>, + cached_compiler: &CachedCompiler<'a>, + ) -> Result { + let compiler_output = cached_compiler + .compile_contracts( + test_definition.metadata, + test_definition.metadata_file_path, + test_definition.mode.clone(), + None, + platform_information.compiler.as_ref(), + platform_information.platform, + &platform_information.reporter, + ) + .await + .inspect_err(|err| { + error!( + ?err, + platform_identifier = %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 = test_definition + .metadata + .contract_sources() + .inspect_err(|err| { + error!( + ?err, + platform_identifier = %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 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 = 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 = platform_information + .node + .execute_transaction(tx) + .await + .inspect_err(|err| { + error!( + ?err, + %library_instance, + platform_identifier = %platform_information.platform.platform_identifier(), + "Failed to deploy the library" + ) + })?; + + debug!( + ?library_instance, + platform_identifier = %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( + test_definition.metadata, + test_definition.metadata_file_path, + test_definition.mode.clone(), + deployed_libraries.as_ref(), + platform_information.compiler.as_ref(), + platform_information.platform, + &platform_information.reporter, + ) + .await + .inspect_err(|err| { + error!( + ?err, + platform_identifier = %platform_information.platform.platform_identifier(), + "Pre-linking compilation failed" + ) + }) + .context("Failed to compile the post-link contracts")?; + + Ok(ExecutionState::new( + compiler_output.contracts, + deployed_libraries.unwrap_or_default(), + )) + } + // endregion:Constructors & Initialization + + // region:Step Handling + #[instrument(level = "info", skip_all)] + pub async fn execute_all(mut self) -> Result { + while let Some(result) = self.execute_next_step().await { + result? + } + Ok(self.steps_executed) + } + + #[instrument( + level = "info", + skip_all, + fields( + platform_identifier = %self.platform_information.platform.platform_identifier(), + node_id = self.platform_information.node.id(), + ), + )] + 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::BalanceAssertion(step) => self + .execute_balance_assertion(step_path, step.as_ref()) + .await + .context("Balance Assertion Step Failed"), + Step::StorageEmptyAssertion(step) => self + .execute_storage_empty_assertion_step(step_path, step.as_ref()) + .await + .context("Storage Empty Assertion 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"), + }?; + 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")?; + self.handle_function_call_assertions(step, &execution_receipt, &tracing_result) + .await + .context("Failed to handle function call assertions")?; + Ok(1) + } + + async fn handle_function_call_contract_deployment( + &mut self, + step: &FunctionCallStep, + ) -> Result> { + 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(); + let resolver = self.platform_information.node.resolver().await?; + step.caller + .resolve_address(resolver.as_ref(), context) + .await? + }; + if let (_, _, Some(receipt)) = self + .get_or_deploy_contract_instance(&instance, caller, calldata, value) + .await + .context("Failed to get or deploy contract instance during input execution")? + { + receipts.insert(instance.clone(), receipt); + } + } + + Ok(receipts) + } + + async fn handle_function_call_execution( + &mut self, + step: &FunctionCallStep, + mut deployment_receipts: HashMap, + ) -> Result { + match step.method { + // This step was already executed when `handle_step` was called. We just need to + // lookup the transaction receipt in this case and continue on. + Method::Deployer => deployment_receipts + .remove(&step.instance) + .context("Failed to find deployment receipt for constructor call"), + Method::Fallback | Method::FunctionName(_) => { + let resolver = self.platform_information.node.resolver().await?; + let tx = match step + .as_transaction(resolver.as_ref(), self.default_resolution_context()) + .await + { + Ok(tx) => tx, + Err(err) => { + return Err(err); + } + }; + + match self.platform_information.node.execute_transaction(tx).await { + Ok(receipt) => Ok(receipt), + Err(err) => Err(err), + } + } + } + } + + 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(()) + } + + async fn handle_function_call_assertions( + &mut self, + step: &FunctionCallStep, + receipt: &TransactionReceipt, + tracing_result: &CallFrame, + ) -> Result<()> { + // Resolving the `step.expected` into a series of expectations that we can then assert on. + let mut expectations = match step { + 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 = &step.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 { + self.handle_function_call_assertion_item(receipt, tracing_result, expectation) + .await + }) + .await + } + + async fn handle_function_call_assertion_item( + &self, + receipt: &TransactionReceipt, + tracing_result: &CallFrame, + assertion: ExpectedOutput, + ) -> Result<()> { + let resolver = self + .platform_information + .node + .resolver() + .await + .context("Failed to create the resolver for the node")?; + + if let Some(ref version_requirement) = assertion.compiler_version { + if !version_requirement.matches(self.platform_information.compiler.version()) { + return Ok(()); + } + } + + let resolution_context = self + .default_resolution_context() + .with_block_number(receipt.block_number.as_ref()) + .with_transaction_hash(&receipt.transaction_hash); + + // Handling the receipt state assertion. + let expected = !assertion.exception; + let actual = receipt.status(); + if actual != expected { + tracing::error!( + expected, + actual, + ?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) = assertion.return_data { + let expected = expected_calldata; + let actual = &tracing_result.output.as_ref().unwrap_or_default(); + if !expected + .is_equivalent(actual, resolver.as_ref(), resolution_context) + .await + .context("Failed to resolve calldata equivalence for return data assertion")? + { + tracing::error!( + ?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) = assertion.events { + // Handling the events length assertion. + let expected = expected_events.len(); + let actual = 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(receipt.logs()).enumerate() + { + // Handling the emitter assertion. + if let Some(ref expected_address) = expected_event.address { + let expected = expected_address + .resolve_address(resolver.as_ref(), 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.as_ref(), resolution_context) + .await + .context("Failed to resolve event topic equivalence")? + { + tracing::error!( + event_idx, + ?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.as_ref(), resolution_context) + .await + .context("Failed to resolve event value equivalence")? + { + tracing::error!( + event_idx, + ?receipt, + ?expected, + ?actual, + "Event value assertion failed", + ); + anyhow::bail!( + "Event value assertion failed - Expected {expected:?} but got {actual:?}", + ); + } + } + } + + Ok(()) + } + + #[instrument(level = "info", skip_all)] + pub async fn execute_balance_assertion( + &mut self, + _: &StepPath, + step: &BalanceAssertionStep, + ) -> anyhow::Result { + self.step_address_auto_deployment(&step.address) + .await + .context("Failed to perform auto-deployment for the step address")?; + + let resolver = self.platform_information.node.resolver().await?; + let address = step + .address + .resolve_address(resolver.as_ref(), self.default_resolution_context()) + .await?; + + let balance = self.platform_information.node.balance_of(address).await?; + + let expected = step.expected_balance; + 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(1) + } + + #[instrument(level = "info", skip_all, err(Debug))] + async fn execute_storage_empty_assertion_step( + &mut self, + _: &StepPath, + step: &StorageEmptyAssertionStep, + ) -> Result { + self.step_address_auto_deployment(&step.address) + .await + .context("Failed to perform auto-deployment for the step address")?; + + let resolver = self.platform_information.node.resolver().await?; + let address = step + .address + .resolve_address(resolver.as_ref(), self.default_resolution_context()) + .await?; + + let storage = self + .platform_information + .node + .latest_state_proof(address, Default::default()) + .await?; + let is_empty = storage.storage_hash == EMPTY_ROOT_HASH; + + let expected = step.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(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(|_| PlatformDriver { + platform_information: self.platform_information, + 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() + }, + }) + .map(|driver| driver.execute_all()) + .collect::>(); + let res = futures::future::try_join_all(tasks) + .await + .context("Repetition execution failed")?; + Ok(res.first().copied().unwrap_or_default()) + } + + #[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()?; + 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 resolver = self.platform_information.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 self.platform_information.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.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 +} diff --git a/crates/core/src/differential_tests/entry_point.rs b/crates/core/src/differential_tests/entry_point.rs new file mode 100644 index 0000000..0fea540 --- /dev/null +++ b/crates/core/src/differential_tests/entry_point.rs @@ -0,0 +1,240 @@ +//! The main entry point into differential testing. + +use std::{ + collections::BTreeMap, + io::{BufWriter, Write, stderr}, + sync::Arc, + time::Instant, +}; + +use anyhow::Context as _; +use futures::{FutureExt, StreamExt}; +use revive_dt_common::types::PrivateKeyAllocator; +use revive_dt_core::Platform; +use tokio::sync::Mutex; +use tracing::{Instrument, error, info, info_span, instrument}; + +use revive_dt_config::{Context, TestExecutionContext}; +use revive_dt_report::{Reporter, ReporterEvent, TestCaseStatus}; + +use crate::{ + differential_tests::Driver, + helpers::{CachedCompiler, NodePool, collect_metadata_files, create_test_definitions_stream}, +}; + +/// Handles the differential testing executing it according to the information defined in the +/// context +#[instrument(level = "info", err(Debug), skip_all)] +pub async fn handle_differential_tests( + context: TestExecutionContext, + reporter: Reporter, +) -> anyhow::Result<()> { + let reporter_clone = reporter.clone(); + + // Discover all of the metadata files that are defined in the context. + let metadata_files = collect_metadata_files(&context) + .context("Failed to collect metadata files for differential testing")?; + info!(len = metadata_files.len(), "Discovered metadata files"); + + // Discover the list of platforms that the tests should run on based on the context. + let platforms = context + .platforms + .iter() + .copied() + .map(Into::<&dyn Platform>::into) + .collect::>(); + + // Starting the nodes of the various platforms specified in the context. + let platforms_and_nodes = { + let mut map = BTreeMap::new(); + + for platform in platforms.iter() { + let platform_identifier = platform.platform_identifier(); + + let context = Context::Test(Box::new(context.clone())); + let node_pool = NodePool::new(context, *platform) + .await + .inspect_err(|err| { + error!( + ?err, + %platform_identifier, + "Failed to initialize the node pool for the platform." + ) + }) + .context("Failed to initialize the node pool")?; + + map.insert(platform_identifier, (*platform, node_pool)); + } + + map + }; + info!("Spawned the platform nodes"); + + // Preparing test definitions. + let full_context = Context::Test(Box::new(context.clone())); + let test_definitions = create_test_definitions_stream( + &full_context, + metadata_files.iter(), + &platforms_and_nodes, + reporter.clone(), + ) + .await + .collect::>() + .await; + info!(len = test_definitions.len(), "Created test definitions"); + + // Creating everything else required for the driver to run. + let cached_compiler = CachedCompiler::new( + context + .working_directory + .as_path() + .join("compilation_cache"), + context + .compilation_configuration + .invalidate_compilation_cache, + ) + .await + .map(Arc::new) + .context("Failed to initialize cached compiler")?; + let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new( + context.wallet_configuration.highest_private_key_exclusive(), + ))); + + // Creating the driver and executing all of the steps. + let driver_task = futures::future::join_all(test_definitions.iter().map(|test_definition| { + let private_key_allocator = private_key_allocator.clone(); + let cached_compiler = cached_compiler.clone(); + let mode = test_definition.mode.clone(); + let span = info_span!( + "Executing Test Case", + metadata_file_path = %test_definition.metadata_file_path.display(), + case_idx = %test_definition.case_idx, + mode = %mode + ); + async move { + let driver = + match Driver::new_root(test_definition, private_key_allocator, &cached_compiler) + .await + { + Ok(driver) => driver, + Err(error) => { + test_definition + .reporter + .report_test_failed_event(format!("{error:#}")) + .expect("Can't fail"); + error!("Test Case Failed"); + return; + } + }; + info!("Created the driver for the test case"); + + match driver.execute_all().await { + Ok(steps_executed) => test_definition + .reporter + .report_test_succeeded_event(steps_executed) + .expect("Can't fail"), + Err(error) => { + test_definition + .reporter + .report_test_failed_event(format!("{error:#}")) + .expect("Can't fail"); + error!("Test Case Failed"); + } + }; + info!("Finished the execution of the test case") + } + .instrument(span) + })) + .inspect(|_| { + info!("Finished executing all test cases"); + reporter_clone + .report_completion_event() + .expect("Can't fail") + }); + let cli_reporting_task = start_cli_reporting_task(reporter); + + futures::future::join(driver_task, cli_reporting_task).await; + + Ok(()) +} + +#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)] +async fn start_cli_reporting_task(reporter: Reporter) { + let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail"); + drop(reporter); + + let start = Instant::now(); + + const GREEN: &str = "\x1B[32m"; + const RED: &str = "\x1B[31m"; + const GREY: &str = "\x1B[90m"; + const COLOR_RESET: &str = "\x1B[0m"; + const BOLD: &str = "\x1B[1m"; + const BOLD_RESET: &str = "\x1B[22m"; + + let mut number_of_successes = 0; + let mut number_of_failures = 0; + + let mut buf = BufWriter::new(stderr()); + while let Ok(event) = aggregator_events_rx.recv().await { + let ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted { + metadata_file_path, + mode, + case_status, + } = event + else { + continue; + }; + + let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display()); + for (case_idx, case_status) in case_status.into_iter() { + let _ = write!(buf, "\tCase Index {case_idx:>3}: "); + let _ = match case_status { + TestCaseStatus::Succeeded { steps_executed } => { + number_of_successes += 1; + writeln!( + buf, + "{}{}Case Succeeded{} - Steps Executed: {}{}", + GREEN, BOLD, BOLD_RESET, steps_executed, COLOR_RESET + ) + } + TestCaseStatus::Failed { reason } => { + number_of_failures += 1; + writeln!( + buf, + "{}{}Case Failed{} - Reason: {}{}", + RED, + BOLD, + BOLD_RESET, + reason.trim(), + COLOR_RESET, + ) + } + TestCaseStatus::Ignored { reason, .. } => writeln!( + buf, + "{}{}Case Ignored{} - Reason: {}{}", + GREY, + BOLD, + BOLD_RESET, + reason.trim(), + COLOR_RESET, + ), + }; + } + let _ = writeln!(buf); + } + + // Summary at the end. + let _ = writeln!( + buf, + "{} cases: {}{}{} cases succeeded, {}{}{} cases failed in {} seconds", + number_of_successes + number_of_failures, + GREEN, + number_of_successes, + COLOR_RESET, + RED, + number_of_failures, + COLOR_RESET, + start.elapsed().as_secs() + ); +} diff --git a/crates/core/src/differential_tests/execution_state.rs b/crates/core/src/differential_tests/execution_state.rs new file mode 100644 index 0000000..5cae329 --- /dev/null +++ b/crates/core/src/differential_tests/execution_state.rs @@ -0,0 +1,35 @@ +use std::{collections::HashMap, path::PathBuf}; + +use alloy::{ + json_abi::JsonAbi, + primitives::{Address, U256}, +}; + +use revive_dt_format::metadata::{ContractIdent, ContractInstance}; + +#[derive(Clone)] +/// The state associated with the test execution of one of the tests. +pub struct ExecutionState { + /// The compiled contracts, these contracts have been compiled and have had the libraries linked + /// against them and therefore they're ready to be deployed on-demand. + pub compiled_contracts: HashMap>, + + /// 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(), + } + } +} diff --git a/crates/core/src/differential_tests/mod.rs b/crates/core/src/differential_tests/mod.rs new file mode 100644 index 0000000..ee2554f --- /dev/null +++ b/crates/core/src/differential_tests/mod.rs @@ -0,0 +1,11 @@ +//! This module contains all of the code responsible for performing differential tests including the +//! driver implementation, state implementation, and the core logic that allows for tests to be +//! executed. + +mod driver; +mod entry_point; +mod execution_state; + +pub use driver::*; +pub use entry_point::*; +pub use execution_state::*; 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/cached_compiler.rs b/crates/core/src/helpers/cached_compiler.rs similarity index 91% rename from crates/core/src/cached_compiler.rs rename to crates/core/src/helpers/cached_compiler.rs index c10f7e1..93017d8 100644 --- a/crates/core/src/cached_compiler.rs +++ b/crates/core/src/helpers/cached_compiler.rs @@ -5,7 +5,7 @@ use std::{ borrow::Cow, collections::HashMap, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, LazyLock}, }; use futures::FutureExt; @@ -19,7 +19,7 @@ use anyhow::{Context as _, Error, Result}; use revive_dt_report::ExecutionSpecificReporter; use semver::Version; use serde::{Deserialize, Serialize}; -use tokio::sync::{Mutex, RwLock}; +use tokio::sync::{Mutex, RwLock, Semaphore}; use tracing::{Instrument, debug, debug_span, instrument}; pub struct CachedCompiler<'a> { @@ -165,10 +165,22 @@ impl<'a> CachedCompiler<'a> { cache_value.compiler_output } None => { - compilation_callback() + let compiler_output = compilation_callback() .await .context("Compilation callback failed (cache miss path)")? - .compiler_output + .compiler_output; + self.artifacts_cache + .insert( + &cache_key, + &CacheValue { + compiler_output: compiler_output.clone(), + }, + ) + .await + .context( + "Failed to write the cached value of the compilation artifacts", + )?; + compiler_output } } } @@ -186,6 +198,12 @@ async fn compile_contracts( compiler: &dyn SolidityCompiler, reporter: &ExecutionSpecificReporter, ) -> Result { + // Puts a limit on how many compilations we can perform at any given instance which helps us + // with some of the errors we've been seeing with high concurrency on MacOS (we have not tried + // it on Linux so we don't know if these issues also persist there or not.) + static SPAWN_GATE: LazyLock = LazyLock::new(|| Semaphore::new(100)); + let _permit = SPAWN_GATE.acquire().await?; + let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref()) .with_allowed_extension("sol") .with_use_cached_fs(true) diff --git a/crates/core/src/helpers/metadata.rs b/crates/core/src/helpers/metadata.rs new file mode 100644 index 0000000..60f351b --- /dev/null +++ b/crates/core/src/helpers/metadata.rs @@ -0,0 +1,33 @@ +use revive_dt_config::CorpusConfiguration; +use revive_dt_format::{corpus::Corpus, metadata::MetadataFile}; +use tracing::{info, info_span, instrument}; + +/// Given an object that implements [`AsRef`], this function finds all of the +/// corpus files and produces a map containing all of the [`MetadataFile`]s discovered. +#[instrument(level = "debug", name = "Collecting Corpora", skip_all)] +pub fn collect_metadata_files( + context: impl AsRef, +) -> anyhow::Result> { + let mut metadata_files = Vec::new(); + + let corpus_configuration = AsRef::::as_ref(&context); + for path in &corpus_configuration.paths { + let span = info_span!("Processing corpus file", path = %path.display()); + let _guard = span.enter(); + + let corpus = Corpus::try_from_path(path)?; + info!( + name = corpus.name(), + number_of_contained_paths = corpus.path_count(), + "Deserialized corpus file" + ); + metadata_files.extend(corpus.enumerate_tests()); + } + + // There's a possibility that there are certain paths that all lead to the same metadata files + // and therefore it's important that we sort them and then deduplicate them. + metadata_files.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path)); + metadata_files.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path); + + Ok(metadata_files) +} diff --git a/crates/core/src/helpers/mod.rs b/crates/core/src/helpers/mod.rs new file mode 100644 index 0000000..eba4f67 --- /dev/null +++ b/crates/core/src/helpers/mod.rs @@ -0,0 +1,9 @@ +mod cached_compiler; +mod metadata; +mod pool; +mod test; + +pub use cached_compiler::*; +pub use metadata::*; +pub use pool::*; +pub use test::*; diff --git a/crates/core/src/pool.rs b/crates/core/src/helpers/pool.rs similarity index 80% rename from crates/core/src/pool.rs rename to crates/core/src/helpers/pool.rs index 06fb2be..66b1736 100644 --- a/crates/core/src/pool.rs +++ b/crates/core/src/helpers/pool.rs @@ -16,7 +16,7 @@ pub struct NodePool { impl NodePool { /// Create a new Pool. This will start as many nodes as there are workers in `config`. - pub fn new(context: Context, platform: &dyn Platform) -> anyhow::Result { + pub async fn new(context: Context, platform: &dyn Platform) -> anyhow::Result { let concurrency_configuration = AsRef::::as_ref(&context); let nodes = concurrency_configuration.number_of_nodes; @@ -33,11 +33,18 @@ impl NodePool { .join() .map_err(|error| anyhow::anyhow!("failed to spawn node: {:?}", error)) .context("Failed to join node spawn thread")? - .map_err(|error| anyhow::anyhow!("node failed to spawn: {error}")) .context("Node failed to spawn")?, ); } + let pre_transactions_tasks = nodes + .iter_mut() + .map(|node| node.pre_transactions()) + .collect::>(); + futures::future::try_join_all(pre_transactions_tasks) + .await + .context("Failed to run the pre-transactions task")?; + Ok(Self { nodes, next: Default::default(), diff --git a/crates/core/src/helpers/test.rs b/crates/core/src/helpers/test.rs new file mode 100644 index 0000000..ec16b91 --- /dev/null +++ b/crates/core/src/helpers/test.rs @@ -0,0 +1,325 @@ +use std::collections::BTreeMap; +use std::sync::Arc; +use std::{borrow::Cow, path::Path}; + +use futures::{Stream, StreamExt, stream}; +use indexmap::{IndexMap, indexmap}; +use revive_dt_common::iterators::EitherIter; +use revive_dt_common::types::PlatformIdentifier; +use revive_dt_config::Context; +use revive_dt_format::mode::ParsedMode; +use serde_json::{Value, json}; + +use revive_dt_compiler::Mode; +use revive_dt_compiler::SolidityCompiler; +use revive_dt_format::{ + case::{Case, CaseIdx}, + metadata::MetadataFile, +}; +use revive_dt_node_interaction::EthereumNode; +use revive_dt_report::{ExecutionSpecificReporter, Reporter}; +use revive_dt_report::{TestSpecificReporter, TestSpecifier}; +use tracing::{debug, error, info}; + +use crate::Platform; +use crate::helpers::NodePool; + +pub async fn create_test_definitions_stream<'a>( + // This is only required for creating the compiler objects and is not used anywhere else in the + // function. + context: &Context, + metadata_files: impl IntoIterator, + platforms_and_nodes: &'a BTreeMap, + reporter: Reporter, +) -> impl Stream> { + stream::iter( + metadata_files + .into_iter() + // Flatten over the cases. + .flat_map(|metadata_file| { + metadata_file + .cases + .iter() + .enumerate() + .map(move |(case_idx, case)| (metadata_file, case_idx, case)) + }) + // Flatten over the modes, prefer the case modes over the metadata file modes. + .flat_map(move |(metadata_file, case_idx, case)| { + let reporter = reporter.clone(); + + let modes = case.modes.as_ref().or(metadata_file.modes.as_ref()); + let modes = match modes { + Some(modes) => EitherIter::A( + ParsedMode::many_to_modes(modes.iter()).map(Cow::<'static, _>::Owned), + ), + None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)), + }; + + modes.into_iter().map(move |mode| { + ( + metadata_file, + case_idx, + case, + mode.clone(), + reporter.test_specific_reporter(Arc::new(TestSpecifier { + solc_mode: mode.as_ref().clone(), + metadata_file_path: metadata_file.metadata_file_path.clone(), + case_idx: CaseIdx::new(case_idx), + })), + ) + }) + }) + // Inform the reporter of each one of the test cases that were discovered which we expect to + // run. + .inspect(|(_, _, _, _, reporter)| { + reporter + .report_test_case_discovery_event() + .expect("Can't fail"); + }), + ) + // Creating the Test Definition objects from all of the various objects we have and creating + // their required dependencies (e.g., compiler). + .filter_map( + move |(metadata_file, case_idx, case, mode, reporter)| async move { + let mut platforms = BTreeMap::new(); + for (platform, node_pool) in platforms_and_nodes.values() { + let node = node_pool.round_robbin(); + let compiler = platform + .new_compiler(context.clone(), mode.version.clone().map(Into::into)) + .await + .inspect_err(|err| { + error!( + ?err, + platform_identifier = %platform.platform_identifier(), + "Failed to instantiate the compiler" + ) + }) + .ok()?; + + reporter + .report_node_assigned_event( + node.id(), + platform.platform_identifier(), + node.connection_string(), + ) + .expect("Can't fail"); + + let reporter = + reporter.execution_specific_reporter(node.id(), platform.platform_identifier()); + + platforms.insert( + platform.platform_identifier(), + TestPlatformInformation { + platform: *platform, + node, + compiler, + reporter, + }, + ); + } + + Some(TestDefinition { + /* Metadata file information */ + metadata: metadata_file, + metadata_file_path: metadata_file.metadata_file_path.as_path(), + + /* Mode Information */ + mode: mode.clone(), + + /* Case Information */ + case_idx: CaseIdx::new(case_idx), + case, + + /* Platform and Node Assignment Information */ + platforms, + + /* Reporter */ + reporter, + }) + }, + ) + // Filter out the test cases which are incompatible or that can't run in the current setup. + .filter_map(move |test| async move { + match test.check_compatibility() { + Ok(()) => Some(test), + Err((reason, additional_information)) => { + debug!( + metadata_file_path = %test.metadata.metadata_file_path.display(), + case_idx = %test.case_idx, + mode = %test.mode, + reason, + additional_information = + serde_json::to_string(&additional_information).unwrap(), + "Ignoring Test Case" + ); + test.reporter + .report_test_ignored_event( + reason.to_string(), + additional_information + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect::>(), + ) + .expect("Can't fail"); + None + } + } + }) + .inspect(|test| { + info!( + metadata_file_path = %test.metadata_file_path.display(), + case_idx = %test.case_idx, + mode = %test.mode, + "Created a test case definition" + ); + }) +} + +/// This is a full description of a differential test to run alongside the full metadata file, the +/// specific case to be tested, the platforms that the tests should run on, the specific nodes of +/// these platforms that they should run on, the compilers to use, and everything else needed making +/// it a complete description. +pub struct TestDefinition<'a> { + /* Metadata file information */ + pub metadata: &'a MetadataFile, + pub metadata_file_path: &'a Path, + + /* Mode Information */ + pub mode: Cow<'a, Mode>, + + /* Case Information */ + pub case_idx: CaseIdx, + pub case: &'a Case, + + /* Platform and Node Assignment Information */ + pub platforms: BTreeMap>, + + /* Reporter */ + pub reporter: TestSpecificReporter, +} + +impl<'a> TestDefinition<'a> { + /// Checks if this test can be ran with the current configuration. + pub fn check_compatibility(&self) -> TestCheckFunctionResult { + self.check_metadata_file_ignored()?; + self.check_case_file_ignored()?; + self.check_target_compatibility()?; + self.check_evm_version_compatibility()?; + self.check_compiler_compatibility()?; + Ok(()) + } + + /// Checks if the metadata file is ignored or not. + fn check_metadata_file_ignored(&self) -> TestCheckFunctionResult { + if self.metadata.ignore.is_some_and(|ignore| ignore) { + Err(("Metadata file is ignored.", indexmap! {})) + } else { + Ok(()) + } + } + + /// Checks if the case file is ignored or not. + fn check_case_file_ignored(&self) -> TestCheckFunctionResult { + if self.case.ignore.is_some_and(|ignore| ignore) { + Err(("Case is ignored.", indexmap! {})) + } else { + Ok(()) + } + } + + /// Checks if the platforms all support the desired targets in the metadata file. + fn check_target_compatibility(&self) -> TestCheckFunctionResult { + let mut error_map = indexmap! { + "test_desired_targets" => json!(self.metadata.targets.as_ref()), + }; + let mut is_allowed = true; + for (_, platform_information) in self.platforms.iter() { + let is_allowed_for_platform = match self.metadata.targets.as_ref() { + None => true, + Some(required_vm_identifiers) => { + required_vm_identifiers.contains(&platform_information.platform.vm_identifier()) + } + }; + is_allowed &= is_allowed_for_platform; + error_map.insert( + platform_information.platform.platform_identifier().into(), + json!(is_allowed_for_platform), + ); + } + + if is_allowed { + Ok(()) + } else { + Err(( + "One of the platforms do do not support the targets allowed by the test.", + error_map, + )) + } + } + + // Checks for the compatibility of the EVM version with the platforms specified. + fn check_evm_version_compatibility(&self) -> TestCheckFunctionResult { + let Some(evm_version_requirement) = self.metadata.required_evm_version else { + return Ok(()); + }; + + let mut error_map = indexmap! { + "test_desired_evm_version" => json!(self.metadata.required_evm_version), + }; + let mut is_allowed = true; + for (_, platform_information) in self.platforms.iter() { + let is_allowed_for_platform = + evm_version_requirement.matches(&platform_information.node.evm_version()); + is_allowed &= is_allowed_for_platform; + error_map.insert( + platform_information.platform.platform_identifier().into(), + json!(is_allowed_for_platform), + ); + } + + if is_allowed { + Ok(()) + } else { + Err(( + "EVM version is incompatible for the platforms specified", + error_map, + )) + } + } + + /// Checks if the platforms compilers support the mode that the test is for. + fn check_compiler_compatibility(&self) -> TestCheckFunctionResult { + let mut error_map = indexmap! { + "test_desired_evm_version" => json!(self.metadata.required_evm_version), + }; + let mut is_allowed = true; + for (_, platform_information) in self.platforms.iter() { + let is_allowed_for_platform = platform_information + .compiler + .supports_mode(self.mode.optimize_setting, self.mode.pipeline); + is_allowed &= is_allowed_for_platform; + error_map.insert( + platform_information.platform.platform_identifier().into(), + json!(is_allowed_for_platform), + ); + } + + if is_allowed { + Ok(()) + } else { + Err(( + "Compilers do not support this mode either for the provided platforms.", + error_map, + )) + } + } +} + +pub struct TestPlatformInformation<'a> { + pub platform: &'a dyn Platform, + pub node: &'a dyn EthereumNode, + pub compiler: Box, + pub reporter: ExecutionSpecificReporter, +} + +type TestCheckFunctionResult = Result<(), (&'static str, IndexMap<&'static str, Value>)>; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index dbf576b..5550a80 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -14,13 +14,13 @@ use revive_dt_common::types::*; use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc}; use revive_dt_config::*; use revive_dt_node::{ - Node, geth::GethNode, lighthouse_geth::LighthouseGethNode, substrate::SubstrateNode, + Node, node_implementations::geth::GethNode, + node_implementations::lighthouse_geth::LighthouseGethNode, + node_implementations::substrate::SubstrateNode, }; use revive_dt_node_interaction::EthereumNode; use 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 3d569ff..12cd8bb 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -1,55 +1,21 @@ -mod cached_compiler; -mod pool; +mod differential_benchmarks; +mod differential_tests; +mod helpers; -use std::{ - borrow::Cow, - collections::{BTreeSet, HashMap}, - io::{BufWriter, Write, stderr}, - path::Path, - sync::Arc, - time::Instant, -}; - -use alloy::{ - network::{Ethereum, TransactionBuilder}, - rpc::types::TransactionRequest, -}; -use anyhow::Context as _; use clap::Parser; -use futures::stream; -use futures::{Stream, StreamExt}; -use indexmap::{IndexMap, indexmap}; -use revive_dt_node_interaction::EthereumNode; -use revive_dt_report::{ - ExecutionSpecificReporter, ReportAggregator, Reporter, ReporterEvent, TestCaseStatus, - TestSpecificReporter, TestSpecifier, -}; +use revive_dt_report::ReportAggregator; use schemars::schema_for; -use serde_json::{Value, json}; -use tokio::sync::Mutex; -use tracing::{debug, error, info, info_span, instrument}; +use tracing::info; use tracing_subscriber::{EnvFilter, FmtSubscriber}; -use revive_dt_common::{ - iterators::EitherIter, - types::{Mode, PrivateKeyAllocator}, -}; -use revive_dt_compiler::SolidityCompiler; -use revive_dt_config::{Context, *}; -use revive_dt_core::{ - Platform, - driver::{CaseDriver, CaseState}, -}; -use revive_dt_format::{ - case::{Case, CaseIdx}, - corpus::Corpus, - metadata::{ContractPathAndIdent, Metadata, MetadataFile}, - mode::ParsedMode, - steps::{FunctionCallStep, Step}, -}; +use revive_dt_config::Context; +use revive_dt_core::Platform; +use revive_dt_format::metadata::Metadata; -use crate::cached_compiler::CachedCompiler; -use crate::pool::NodePool; +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() @@ -75,37 +41,37 @@ fn main() -> anyhow::Result<()> { let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task(); match context { - Context::ExecuteTests(context) => { - let tests = collect_corpora(&context) - .context("Failed to collect corpus files from provided arguments")? - .into_iter() - .inspect(|(corpus, _)| { - reporter - .report_corpus_file_discovery_event(corpus.clone()) - .expect("Can't fail") - }) - .flat_map(|(_, files)| files.into_iter()) - .inspect(|metadata_file| { - reporter - .report_metadata_file_discovery_event( - metadata_file.metadata_file_path.clone(), - metadata_file.content.clone(), - ) - .expect("Can't fail") - }) - .collect::>(); + Context::Test(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_tests_handling_task = + handle_differential_tests(*context, reporter); - 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 { - execute_corpus(*context, &tests, reporter, report_aggregator_task) - .await - .context("Failed to execute corpus") - }) - } + futures::future::try_join(differential_tests_handling_task, report_aggregator_task) + .await?; + + Ok(()) + }), + Context::Benchmark(context) => tokio::runtime::Builder::new_multi_thread() + .worker_threads(context.concurrency_configuration.number_of_threads) + .enable_all() + .build() + .expect("Failed building the Runtime") + .block_on(async move { + let differential_benchmarks_handling_task = + handle_differential_benchmarks(*context, reporter); + + futures::future::try_join( + differential_benchmarks_handling_task, + report_aggregator_task, + ) + .await?; + + Ok(()) + }), Context::ExportJsonSchema => { let schema = schema_for!(Metadata); println!("{}", serde_json::to_string_pretty(&schema).unwrap()); @@ -113,671 +79,3 @@ fn main() -> anyhow::Result<()> { } } } - -#[instrument(level = "debug", name = "Collecting Corpora", skip_all)] -fn collect_corpora( - context: &TestExecutionContext, -) -> anyhow::Result>> { - let mut corpora = HashMap::new(); - - for path in &context.corpus { - let span = info_span!("Processing corpus file", path = %path.display()); - let _guard = span.enter(); - - let corpus = Corpus::try_from_path(path)?; - info!( - name = corpus.name(), - number_of_contained_paths = corpus.path_count(), - "Deserialized corpus file" - ); - let tests = corpus.enumerate_tests(); - corpora.insert(corpus, tests); - } - - Ok(corpora) -} - -async fn run_driver( - context: TestExecutionContext, - metadata_files: &[MetadataFile], - reporter: Reporter, - report_aggregator_task: impl Future>, - platforms: Vec<&dyn Platform>, -) -> anyhow::Result<()> { - let mut nodes = Vec::<(&dyn Platform, NodePool)>::new(); - for platform in platforms.into_iter() { - let pool = NodePool::new(Context::ExecuteTests(Box::new(context.clone())), platform) - .inspect_err(|err| { - error!( - ?err, - platform_identifier = %platform.platform_identifier(), - "Failed to initialize the node pool for the platform." - ) - }) - .context("Failed to initialize the node pool")?; - nodes.push((platform, pool)); - } - - let tests_stream = tests_stream( - &context, - metadata_files.iter(), - nodes.as_slice(), - reporter.clone(), - ) - .await; - let driver_task = start_driver_task(&context, tests_stream) - .await - .context("Failed to start driver task")?; - let cli_reporting_task = start_cli_reporting_task(reporter); - - let (_, _, rtn) = tokio::join!(cli_reporting_task, driver_task, report_aggregator_task); - rtn?; - - Ok(()) -} - -async fn tests_stream<'a>( - args: &TestExecutionContext, - metadata_files: impl IntoIterator + Clone, - nodes: &'a [(&dyn Platform, NodePool)], - reporter: Reporter, -) -> impl Stream> { - let tests = metadata_files - .into_iter() - .flat_map(|metadata_file| { - metadata_file - .cases - .iter() - .enumerate() - .map(move |(case_idx, case)| (metadata_file, case_idx, case)) - }) - // Flatten over the modes, prefer the case modes over the metadata file modes. - .flat_map(|(metadata_file, case_idx, case)| { - let reporter = reporter.clone(); - - let modes = case.modes.as_ref().or(metadata_file.modes.as_ref()); - let modes = match modes { - Some(modes) => EitherIter::A( - ParsedMode::many_to_modes(modes.iter()).map(Cow::<'static, _>::Owned), - ), - None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)), - }; - - modes.into_iter().map(move |mode| { - ( - metadata_file, - case_idx, - case, - mode.clone(), - reporter.test_specific_reporter(Arc::new(TestSpecifier { - solc_mode: mode.as_ref().clone(), - metadata_file_path: metadata_file.metadata_file_path.clone(), - case_idx: CaseIdx::new(case_idx), - })), - ) - }) - }) - .collect::>(); - - // Note: before we do any kind of filtering or process the iterator in any way, we need to - // inform the report aggregator of all of the cases that were found as it keeps a state of the - // test cases for its internal use. - for (_, _, _, _, reporter) in tests.iter() { - reporter - .report_test_case_discovery_event() - .expect("Can't fail") - } - - stream::iter(tests.into_iter()) - .filter_map( - move |(metadata_file, case_idx, case, mode, reporter)| async move { - let mut platforms = Vec::new(); - for (platform, node_pool) in nodes.iter() { - let node = node_pool.round_robbin(); - let compiler = platform - .new_compiler( - Context::ExecuteTests(Box::new(args.clone())), - mode.version.clone().map(Into::into), - ) - .await - .inspect_err(|err| { - error!( - ?err, - platform_identifier = %platform.platform_identifier(), - "Failed to instantiate the compiler" - ) - }) - .ok()?; - - let reporter = reporter - .execution_specific_reporter(node.id(), platform.platform_identifier()); - platforms.push((*platform, node, compiler, reporter)); - } - - Some(Test { - metadata: metadata_file, - metadata_file_path: metadata_file.metadata_file_path.as_path(), - mode: mode.clone(), - case_idx: CaseIdx::new(case_idx), - case, - platforms, - reporter, - }) - }, - ) - .filter_map(move |test| async move { - match test.check_compatibility() { - Ok(()) => Some(test), - Err((reason, additional_information)) => { - debug!( - metadata_file_path = %test.metadata.metadata_file_path.display(), - case_idx = %test.case_idx, - mode = %test.mode, - reason, - additional_information = - serde_json::to_string(&additional_information).unwrap(), - - "Ignoring Test Case" - ); - test.reporter - .report_test_ignored_event( - reason.to_string(), - additional_information - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect::>(), - ) - .expect("Can't fail"); - None - } - } - }) -} - -async fn start_driver_task<'a>( - context: &TestExecutionContext, - tests: impl Stream>, -) -> anyhow::Result> { - info!("Starting driver task"); - - let cached_compiler = Arc::new( - CachedCompiler::new( - context - .working_directory - .as_path() - .join("compilation_cache"), - context - .compilation_configuration - .invalidate_compilation_cache, - ) - .await - .context("Failed to initialize cached compiler")?, - ); - - Ok(tests.for_each_concurrent( - context.concurrency_configuration.concurrency_limit(), - move |test| { - let cached_compiler = cached_compiler.clone(); - - async move { - for (platform, node, _, _) in test.platforms.iter() { - test.reporter - .report_node_assigned_event( - node.id(), - platform.platform_identifier(), - node.connection_string(), - ) - .expect("Can't fail"); - } - - let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new( - context.wallet_configuration.highest_private_key_exclusive(), - ))); - - let reporter = test.reporter.clone(); - let result = - handle_case_driver(&test, cached_compiler, private_key_allocator).await; - - match result { - Ok(steps_executed) => reporter - .report_test_succeeded_event(steps_executed) - .expect("Can't fail"), - Err(error) => reporter - .report_test_failed_event(format!("{error:#}")) - .expect("Can't fail"), - } - } - }, - )) -} - -#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)] -async fn start_cli_reporting_task(reporter: Reporter) { - let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail"); - drop(reporter); - - let start = Instant::now(); - - const GREEN: &str = "\x1B[32m"; - const RED: &str = "\x1B[31m"; - const GREY: &str = "\x1B[90m"; - const COLOR_RESET: &str = "\x1B[0m"; - const BOLD: &str = "\x1B[1m"; - const BOLD_RESET: &str = "\x1B[22m"; - - let mut number_of_successes = 0; - let mut number_of_failures = 0; - - let mut buf = BufWriter::new(stderr()); - while let Ok(event) = aggregator_events_rx.recv().await { - let ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted { - metadata_file_path, - mode, - case_status, - } = event - else { - continue; - }; - - let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display()); - for (case_idx, case_status) in case_status.into_iter() { - let _ = write!(buf, "\tCase Index {case_idx:>3}: "); - let _ = match case_status { - TestCaseStatus::Succeeded { steps_executed } => { - number_of_successes += 1; - writeln!( - buf, - "{}{}Case Succeeded{} - Steps Executed: {}{}", - GREEN, BOLD, BOLD_RESET, steps_executed, COLOR_RESET - ) - } - TestCaseStatus::Failed { reason } => { - number_of_failures += 1; - writeln!( - buf, - "{}{}Case Failed{} - Reason: {}{}", - RED, - BOLD, - BOLD_RESET, - reason.trim(), - COLOR_RESET, - ) - } - TestCaseStatus::Ignored { reason, .. } => writeln!( - buf, - "{}{}Case Ignored{} - Reason: {}{}", - GREY, - BOLD, - BOLD_RESET, - reason.trim(), - COLOR_RESET, - ), - }; - } - let _ = writeln!(buf); - } - - // Summary at the end. - let _ = writeln!( - buf, - "{} cases: {}{}{} cases succeeded, {}{}{} cases failed in {} seconds", - number_of_successes + number_of_failures, - GREEN, - number_of_successes, - COLOR_RESET, - RED, - number_of_failures, - COLOR_RESET, - start.elapsed().as_secs() - ); -} - -#[allow(clippy::too_many_arguments)] -#[instrument( - level = "info", - name = "Handling Case" - skip_all, - fields( - metadata_file_path = %test.metadata.relative_path().display(), - mode = %test.mode, - case_idx = %test.case_idx, - case_name = test.case.name.as_deref().unwrap_or("Unnamed Case"), - ) -)] -async fn handle_case_driver<'a>( - test: &Test<'a>, - cached_compiler: Arc>, - private_key_allocator: Arc>, -) -> anyhow::Result { - let platform_state = stream::iter(test.platforms.iter()) - // Compiling the pre-link contracts. - .filter_map(|(platform, node, compiler, reporter)| { - let cached_compiler = cached_compiler.clone(); - - async move { - let compiler_output = cached_compiler - .compile_contracts( - test.metadata, - test.metadata_file_path, - test.mode.clone(), - None, - compiler.as_ref(), - *platform, - reporter, - ) - .await - .inspect_err(|err| { - error!( - ?err, - platform_identifier = %platform.platform_identifier(), - "Pre-linking compilation failed" - ) - }) - .ok()?; - Some((test, platform, node, compiler, reporter, compiler_output)) - } - }) - // Deploying the libraries for the platform. - .filter_map( - |(test, platform, node, compiler, reporter, compiler_output)| async move { - let mut deployed_libraries = None::>; - let mut contract_sources = test - .metadata - .contract_sources() - .inspect_err(|err| { - error!( - ?err, - platform_identifier = %platform.platform_identifier(), - "Failed to retrieve contract sources from metadata" - ) - }) - .ok()?; - for library_instance in test - .metadata - .libraries - .iter() - .flatten() - .flat_map(|(_, map)| map.values()) - { - debug!(%library_instance, "Deploying Library Instance"); - - let ContractPathAndIdent { - contract_source_path: library_source_path, - contract_ident: library_ident, - } = contract_sources.remove(library_instance)?; - - let (code, abi) = compiler_output - .contracts - .get(&library_source_path) - .and_then(|contracts| contracts.get(library_ident.as_str()))?; - - let code = alloy::hex::decode(code).ok()?; - - // Getting the deployer address from the cases themselves. This is to ensure - // that we're doing the deployments from different accounts and therefore we're - // not slowed down by the nonce. - let deployer_address = test - .case - .steps - .iter() - .filter_map(|step| match step { - Step::FunctionCall(input) => input.caller.as_address().copied(), - Step::BalanceAssertion(..) => None, - Step::StorageEmptyAssertion(..) => None, - Step::Repeat(..) => None, - Step::AllocateAccount(..) => None, - }) - .next() - .unwrap_or(FunctionCallStep::default_caller_address()); - let tx = TransactionBuilder::::with_deploy_code( - TransactionRequest::default().from(deployer_address), - code, - ); - let receipt = node - .execute_transaction(tx) - .await - .inspect_err(|err| { - error!( - ?err, - %library_instance, - platform_identifier = %platform.platform_identifier(), - "Failed to deploy the library" - ) - }) - .ok()?; - - debug!( - ?library_instance, - platform_identifier = %platform.platform_identifier(), - "Deployed library" - ); - - let library_address = receipt.contract_address?; - - deployed_libraries.get_or_insert_default().insert( - library_instance.clone(), - (library_ident.clone(), library_address, abi.clone()), - ); - } - - Some(( - test, - platform, - node, - compiler, - reporter, - compiler_output, - deployed_libraries, - )) - }, - ) - // Compiling the post-link contracts. - .filter_map( - |(test, platform, node, compiler, reporter, _, deployed_libraries)| { - let cached_compiler = cached_compiler.clone(); - let private_key_allocator = private_key_allocator.clone(); - - async move { - let compiler_output = cached_compiler - .compile_contracts( - test.metadata, - test.metadata_file_path, - test.mode.clone(), - deployed_libraries.as_ref(), - compiler.as_ref(), - *platform, - reporter, - ) - .await - .inspect_err(|err| { - error!( - ?err, - platform_identifier = %platform.platform_identifier(), - "Pre-linking compilation failed" - ) - }) - .ok()?; - - let case_state = CaseState::new( - compiler.version().clone(), - compiler_output.contracts, - deployed_libraries.unwrap_or_default(), - reporter.clone(), - private_key_allocator, - ); - - Some((*node, platform.platform_identifier(), case_state)) - } - }, - ) - // Collect - .collect::>() - .await; - - let mut driver = CaseDriver::new(test.metadata, test.case, platform_state); - driver - .execute() - .await - .inspect(|steps_executed| info!(steps_executed, "Case succeeded")) -} - -async fn execute_corpus( - context: TestExecutionContext, - tests: &[MetadataFile], - reporter: Reporter, - report_aggregator_task: impl Future>, -) -> anyhow::Result<()> { - let platforms = context - .platforms - .iter() - .copied() - .collect::>() - .into_iter() - .map(Into::<&dyn Platform>::into) - .collect::>(); - - run_driver(context, tests, reporter, report_aggregator_task, platforms).await?; - - Ok(()) -} - -/// this represents a single "test"; a mode, path and collection of cases. -#[allow(clippy::type_complexity)] -struct Test<'a> { - metadata: &'a MetadataFile, - metadata_file_path: &'a Path, - mode: Cow<'a, Mode>, - case_idx: CaseIdx, - case: &'a Case, - platforms: Vec<( - &'a dyn Platform, - &'a dyn EthereumNode, - Box, - ExecutionSpecificReporter, - )>, - reporter: TestSpecificReporter, -} - -impl<'a> Test<'a> { - /// Checks if this test can be ran with the current configuration. - pub fn check_compatibility(&self) -> TestCheckFunctionResult { - self.check_metadata_file_ignored()?; - self.check_case_file_ignored()?; - self.check_target_compatibility()?; - self.check_evm_version_compatibility()?; - self.check_compiler_compatibility()?; - Ok(()) - } - - /// Checks if the metadata file is ignored or not. - fn check_metadata_file_ignored(&self) -> TestCheckFunctionResult { - if self.metadata.ignore.is_some_and(|ignore| ignore) { - Err(("Metadata file is ignored.", indexmap! {})) - } else { - Ok(()) - } - } - - /// Checks if the case file is ignored or not. - fn check_case_file_ignored(&self) -> TestCheckFunctionResult { - if self.case.ignore.is_some_and(|ignore| ignore) { - Err(("Case is ignored.", indexmap! {})) - } else { - Ok(()) - } - } - - /// Checks if the platforms all support the desired targets in the metadata file. - fn check_target_compatibility(&self) -> TestCheckFunctionResult { - let mut error_map = indexmap! { - "test_desired_targets" => json!(self.metadata.targets.as_ref()), - }; - let mut is_allowed = true; - for (platform, ..) in self.platforms.iter() { - let is_allowed_for_platform = match self.metadata.targets.as_ref() { - None => true, - Some(targets) => { - let mut target_matches = false; - for target in targets.iter() { - if &platform.vm_identifier() == target { - target_matches = true; - break; - } - } - target_matches - } - }; - is_allowed &= is_allowed_for_platform; - error_map.insert( - platform.platform_identifier().into(), - json!(is_allowed_for_platform), - ); - } - - if is_allowed { - Ok(()) - } else { - Err(( - "One of the platforms do do not support the targets allowed by the test.", - error_map, - )) - } - } - - // Checks for the compatibility of the EVM version with the platforms specified. - fn check_evm_version_compatibility(&self) -> TestCheckFunctionResult { - let Some(evm_version_requirement) = self.metadata.required_evm_version else { - return Ok(()); - }; - - let mut error_map = indexmap! { - "test_desired_evm_version" => json!(self.metadata.required_evm_version), - }; - let mut is_allowed = true; - for (platform, node, ..) in self.platforms.iter() { - let is_allowed_for_platform = evm_version_requirement.matches(&node.evm_version()); - is_allowed &= is_allowed_for_platform; - error_map.insert( - platform.platform_identifier().into(), - json!(is_allowed_for_platform), - ); - } - - if is_allowed { - Ok(()) - } else { - Err(( - "EVM version is incompatible for the platforms specified", - error_map, - )) - } - } - - /// Checks if the platforms compilers support the mode that the test is for. - fn check_compiler_compatibility(&self) -> TestCheckFunctionResult { - let mut error_map = indexmap! { - "test_desired_evm_version" => json!(self.metadata.required_evm_version), - }; - let mut is_allowed = true; - for (platform, _, compiler, ..) in self.platforms.iter() { - let is_allowed_for_platform = - compiler.supports_mode(self.mode.optimize_setting, self.mode.pipeline); - is_allowed &= is_allowed_for_platform; - error_map.insert( - platform.platform_identifier().into(), - json!(is_allowed_for_platform), - ); - } - - if is_allowed { - Ok(()) - } else { - Err(( - "Compilers do not support this mode either for the provided platforms.", - error_map, - )) - } - } -} - -type TestCheckFunctionResult = Result<(), (&'static str, IndexMap<&'static str, Value>)>; 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/case.rs b/crates/format/src/case.rs index 75eca22..7c48279 100644 --- a/crates/format/src/case.rs +++ b/crates/format/src/case.rs @@ -3,10 +3,7 @@ use serde::{Deserialize, Serialize}; use revive_dt_common::{macros::define_wrapper_type, types::Mode}; -use crate::{ - mode::ParsedMode, - steps::{Expected, RepeatStep, Step}, -}; +use crate::{mode::ParsedMode, steps::*}; #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)] pub struct Case { 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 57b0b67..dbd2bf9 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -3,22 +3,36 @@ 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; /// An interface for all interactions with Ethereum compatible nodes. #[allow(clippy::type_complexity)] pub trait EthereumNode { + /// A function to run post spawning the nodes and before any transactions are run on the node. + fn pre_transactions(&mut self) -> Pin> + '_>>; + fn id(&self) -> usize; /// 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, @@ -50,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/Cargo.toml b/crates/node/Cargo.toml index d482621..8073c11 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -11,7 +11,9 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } alloy = { workspace = true } +futures = { workspace = true } tracing = { workspace = true } +tower = { workspace = true } tokio = { workspace = true } revive-common = { workspace = true } diff --git a/crates/node/src/constants.rs b/crates/node/src/constants.rs index 70cbf66..30fb40c 100644 --- a/crates/node/src/constants.rs +++ b/crates/node/src/constants.rs @@ -1,5 +1,10 @@ +use alloy::primitives::ChainId; + /// This constant defines how much Wei accounts are pre-seeded with in genesis. /// /// Note: After changing this number, check that the tests for substrate work as we encountered /// some issues with different values of the initial balance on substrate. pub const INITIAL_BALANCE: u128 = 10u128.pow(37); + +/// The chain id used for all of the chains spawned by the framework. +pub const CHAIN_ID: ChainId = 420420420; diff --git a/crates/node/src/helpers/mod.rs b/crates/node/src/helpers/mod.rs new file mode 100644 index 0000000..8f4ed89 --- /dev/null +++ b/crates/node/src/helpers/mod.rs @@ -0,0 +1,3 @@ +mod process; + +pub use process::*; diff --git a/crates/node/src/process.rs b/crates/node/src/helpers/process.rs similarity index 95% rename from crates/node/src/process.rs rename to crates/node/src/helpers/process.rs index 23057f0..5ffa5f5 100644 --- a/crates/node/src/process.rs +++ b/crates/node/src/helpers/process.rs @@ -110,8 +110,11 @@ impl Process { } let check_result = - check_function(stdout_line.as_deref(), stderr_line.as_deref()) - .context("Failed to wait for the process to be ready")?; + check_function(stdout_line.as_deref(), stderr_line.as_deref()).context( + format!( + "Failed to wait for the process to be ready - {stdout} - {stderr}" + ), + )?; if check_result { break; @@ -127,10 +130,10 @@ impl Process { ProcessReadinessWaitBehavior::WaitForCommandToExit => { if !child .wait() - .context("Failed waiting for kurtosis run process to finish")? + .context("Failed waiting for process to finish")? .success() { - anyhow::bail!("Failed to initialize kurtosis network",); + anyhow::bail!("Failed to spawn command"); } } } diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 5cbedf8..8607dcc 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -3,12 +3,10 @@ use alloy::genesis::Genesis; use revive_dt_node_interaction::EthereumNode; -pub mod common; pub mod constants; -pub mod geth; -pub mod lighthouse_geth; -pub mod process; -pub mod substrate; +pub mod helpers; +pub mod node_implementations; +pub mod provider_utils; /// An abstract interface for testing nodes. pub trait Node: EthereumNode { diff --git a/crates/node/src/geth.rs b/crates/node/src/node_implementations/geth.rs similarity index 78% rename from crates/node/src/geth.rs rename to crates/node/src/node_implementations/geth.rs index 57e299a..fc31d86 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/node_implementations/geth.rs @@ -20,18 +20,22 @@ use alloy::{ network::{Ethereum, EthereumWallet, NetworkWallet}, primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256}, providers::{ - Provider, ProviderBuilder, + Provider, ext::DebugApi, - fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, + fillers::{CachedNonceManager, ChainIdFiller, NonceFiller}, }, 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,13 +43,13 @@ 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, - common::FallbackGasFiller, - constants::INITIAL_BALANCE, - process::{Process, ProcessReadinessWaitBehavior}, + constants::{CHAIN_ID, INITIAL_BALANCE}, + helpers::{Process, ProcessReadinessWaitBehavior}, + provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider}, }; static NODE_COUNT: AtomicU32 = AtomicU32::new(0); @@ -70,7 +74,7 @@ pub struct GethNode { start_timeout: Duration, wallet: Arc, nonce_manager: CachedNonceManager, - chain_id_filler: ChainIdFiller, + provider: OnceCell>>, } impl GethNode { @@ -119,8 +123,8 @@ impl GethNode { handle: None, start_timeout: geth_configuration.start_timeout_ms, wallet: wallet.clone(), - chain_id_filler: Default::default(), nonce_manager: Default::default(), + provider: Default::default(), } } @@ -235,7 +239,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); @@ -245,27 +249,29 @@ impl GethNode { Ok(self) } - 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) + async fn provider(&self) -> anyhow::Result>> { + self.provider + .get_or_try_init(|| async move { + construct_concurrency_limited_provider::( + self.connection_string.as_str(), + FallbackGasFiller::default(), + ChainIdFiller::new(Some(CHAIN_ID)), + NonceFiller::new(self.nonce_manager.clone()), + self.wallet.clone(), + ) + .await + .context("Failed to construct the provider") + }) .await - .map_err(Into::into) + .cloned() } } impl EthereumNode for GethNode { + fn pre_transactions(&mut self) -> Pin> + '_>> { + Box::pin(async move { Ok(()) }) + } + fn id(&self) -> usize { self.id as _ } @@ -274,6 +280,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, @@ -283,8 +333,7 @@ impl EthereumNode for GethNode { fn execute_transaction( &self, transaction: TransactionRequest, - ) -> Pin> + '_>> - { + ) -> Pin> + '_>> { Box::pin(async move { let provider = self .provider() @@ -292,12 +341,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 @@ -317,7 +366,6 @@ impl EthereumNode for GethNode { // allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for // 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)), @@ -351,14 +399,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)), @@ -456,14 +502,54 @@ 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> { +pub struct GethNodeResolver { id: u32, - provider: FillProvider, + provider: ConcreteProvider>, } -impl, P: Provider> ResolverApi for GethNodeResolver { +impl ResolverApi for GethNodeResolver { #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] fn chain_id( &self, @@ -648,12 +734,38 @@ mod tests { (context, node) } + fn shared_state() -> &'static (TestExecutionContext, GethNode) { + static STATE: LazyLock<(TestExecutionContext, GethNode)> = LazyLock::new(new_node); + &STATE + } + fn shared_node() -> &'static GethNode { - static NODE: LazyLock<(TestExecutionContext, GethNode)> = LazyLock::new(new_node); - &NODE.1 + &shared_state().1 + } + + #[tokio::test] + async fn node_mines_simple_transfer_transaction_and_returns_receipt() { + // Arrange + let (context, node) = shared_state(); + + let account_address = context + .wallet_configuration + .wallet() + .default_signer() + .address(); + let transaction = TransactionRequest::default() + .to(account_address) + .value(U256::from(100_000_000_000_000u128)); + + // Act + let receipt = node.execute_transaction(transaction).await; + + // Assert + let _ = receipt.expect("Failed to get the receipt for the transfer"); } #[test] + #[ignore = "Ignored since they take a long time to run"] fn version_works() { // Arrange let node = shared_node(); @@ -670,6 +782,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_chain_id_from_node() { // Arrange let node = shared_node(); @@ -683,6 +796,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_gas_limit_from_node() { // Arrange let node = shared_node(); @@ -700,6 +814,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_coinbase_from_node() { // Arrange let node = shared_node(); @@ -717,6 +832,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_difficulty_from_node() { // Arrange let node = shared_node(); @@ -734,6 +850,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_hash_from_node() { // Arrange let node = shared_node(); @@ -751,6 +868,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_timestamp_from_node() { // Arrange let node = shared_node(); @@ -768,6 +886,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_number_from_node() { // Arrange let node = shared_node(); diff --git a/crates/node/src/lighthouse_geth.rs b/crates/node/src/node_implementations/lighthouse_geth.rs similarity index 68% rename from crates/node/src/lighthouse_geth.rs rename to crates/node/src/node_implementations/lighthouse_geth.rs index 7029c68..ab855f5 100644 --- a/crates/node/src/lighthouse_geth.rs +++ b/crates/node/src/node_implementations/lighthouse_geth.rs @@ -9,7 +9,7 @@ //! that the tool has. use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashSet}, fs::{File, create_dir_all}, io::Read, ops::ControlFlow, @@ -17,7 +17,7 @@ use std::{ pin::Pin, process::{Command, Stdio}, sync::{ - Arc, + Arc, LazyLock, atomic::{AtomicU32, Ordering}, }, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -31,20 +31,24 @@ use alloy::{ Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256, address, }, providers::{ - Provider, ProviderBuilder, + Provider, ext::DebugApi, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, }, 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 serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::serde_as; -use tracing::{Instrument, instrument}; +use tokio::sync::{OnceCell, Semaphore}; +use tracing::{Instrument, info, instrument}; use revive_dt_common::{ fs::clear_directory, @@ -52,13 +56,13 @@ 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, - common::FallbackGasFiller, - constants::INITIAL_BALANCE, - process::{Process, ProcessReadinessWaitBehavior}, + constants::{CHAIN_ID, INITIAL_BALANCE}, + helpers::{Process, ProcessReadinessWaitBehavior}, + provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider}, }; static NODE_COUNT: AtomicU32 = AtomicU32::new(0); @@ -75,7 +79,8 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0); pub struct LighthouseGethNode { /* Node Identifier */ id: u32, - connection_string: String, + ws_connection_string: String, + http_connection_string: String, enclave_name: String, /* Directory Paths */ @@ -91,17 +96,22 @@ pub struct LighthouseGethNode { /* Spawned Processes */ process: Option, + /* Prefunded Account Information */ + prefunded_account_address: Address, + /* Provider Related Fields */ wallet: Arc, nonce_manager: CachedNonceManager, - chain_id_filler: ChainIdFiller, + + persistent_http_provider: OnceCell>>, + persistent_ws_provider: OnceCell>>, + http_provider_requests_semaphore: LazyLock, } impl LighthouseGethNode { const BASE_DIRECTORY: &str = "lighthouse"; const LOGS_DIRECTORY: &str = "logs"; - const IPC_FILE_NAME: &str = "geth.ipc"; const CONFIG_FILE_NAME: &str = "config.yaml"; const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress"; @@ -134,10 +144,8 @@ impl LighthouseGethNode { Self { /* Node Identifier */ id, - connection_string: base_directory - .join(Self::IPC_FILE_NAME) - .display() - .to_string(), + ws_connection_string: String::default(), + http_connection_string: String::default(), enclave_name: format!( "enclave-{}-{}", SystemTime::now() @@ -160,15 +168,20 @@ impl LighthouseGethNode { /* Spawned Processes */ process: None, + /* Prefunded Account Information */ + prefunded_account_address: wallet.default_signer().address(), + /* 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(), + http_provider_requests_semaphore: LazyLock::new(|| Semaphore::const_new(500)), } } /// Create the node directory and call `geth init` to configure the genesis. - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn init(&mut self, _: Genesis) -> anyhow::Result<&mut Self> { self.init_directories() .context("Failed to initialize the directories of the Lighthouse Geth node.")?; @@ -198,10 +211,12 @@ impl LighthouseGethNode { execution_layer_extra_parameters: vec![ "--nodiscover".to_string(), "--cache=4096".to_string(), - "--txpool.globalslots=100000".to_string(), - "--txpool.globalqueue=100000".to_string(), - "--txpool.accountslots=128".to_string(), - "--txpool.accountqueue=1024".to_string(), + "--txlookuplimit=0".to_string(), + "--gcmode=archive".to_string(), + "--txpool.globalslots=500000".to_string(), + "--txpool.globalqueue=500000".to_string(), + "--txpool.accountslots=32768".to_string(), + "--txpool.accountqueue=32768".to_string(), "--http.api=admin,engine,net,eth,web3,debug,txpool".to_string(), "--http.addr=0.0.0.0".to_string(), "--ws".to_string(), @@ -211,13 +226,14 @@ impl LighthouseGethNode { "--ws.origins=*".to_string(), ], consensus_layer_extra_parameters: vec![ + "--disable-quic".to_string(), "--disable-deposit-contract-sync".to_string(), ], }], network_parameters: NetworkParameters { preset: NetworkPreset::Mainnet, seconds_per_slot: 12, - network_id: 420420420, + network_id: CHAIN_ID, deposit_contract_address: address!("0x00000000219ab540356cBB839Cbe05303d7705Fa"), altair_fork_epoch: 0, bellatrix_fork_epoch: 0, @@ -228,14 +244,8 @@ impl LighthouseGethNode { num_validator_keys_per_node: 64, genesis_delay: 10, prefunded_accounts: { - let map = NetworkWallet::::signer_addresses(&self.wallet) - .map(|address| { - ( - address, - GenesisAccount::default() - .with_balance(INITIAL_BALANCE.try_into().unwrap()), - ) - }) + let map = std::iter::once(self.prefunded_account_address) + .map(|address| (address, GenesisAccount::default().with_balance(U256::MAX))) .collect::>(); serde_json::to_string(&map).unwrap() }, @@ -248,7 +258,12 @@ impl LighthouseGethNode { public_port_start: Some(32000 + self.id as u16 * 1000), }, ), - consensus_layer_port_publisher_parameters: Default::default(), + consensus_layer_port_publisher_parameters: Some( + PortPublisherSingleItemParameters { + enabled: Some(true), + public_port_start: Some(59010 + self.id as u16 * 50), + }, + ), }), }; @@ -261,7 +276,7 @@ impl LighthouseGethNode { } /// Spawn the go-ethereum node child process. - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn spawn_process(&mut self) -> anyhow::Result<&mut Self> { let process = Process::new( None, @@ -292,6 +307,7 @@ impl LighthouseGethNode { }), }, ) + .context("Failed to spawn the kurtosis enclave") .inspect_err(|err| { tracing::error!(?err, "Failed to spawn Kurtosis"); self.shutdown().expect("Failed to shutdown kurtosis"); @@ -316,64 +332,157 @@ impl LighthouseGethNode { stdout }; - self.connection_string = stdout + self.http_connection_string = stdout + .split("el-1-geth-lighthouse") + .nth(1) + .and_then(|str| str.split(" rpc").nth(1)) + .and_then(|str| str.split("->").nth(1)) + .and_then(|str| str.split("\n").next()) + .and_then(|str| str.trim().split(" ").next()) + .map(|str| format!("http://{}", str.trim())) + .context("Failed to find the HTTP connection string of Kurtosis")?; + self.ws_connection_string = stdout .split("el-1-geth-lighthouse") .nth(1) .and_then(|str| str.split("ws").nth(1)) .and_then(|str| str.split("->").nth(1)) .and_then(|str| str.split("\n").next()) + .and_then(|str| str.trim().split(" ").next()) .map(|str| format!("ws://{}", str.trim())) .context("Failed to find the WS connection string of Kurtosis")?; + info!( + http_connection_string = self.http_connection_string, + ws_connection_string = self.ws_connection_string, + "Discovered the connection strings for the node" + ); + Ok(self) } - 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) - .await - .context("Failed to create the provider for Kurtosis") - } -} - -impl EthereumNode for LighthouseGethNode { - fn id(&self) -> usize { - self.id as _ - } - - fn connection_string(&self) -> &str { - &self.connection_string - } - #[instrument( level = "info", skip_all, - fields(geth_node_id = self.id, connection_string = self.connection_string), - err, + fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string), + err(Debug), )] - fn execute_transaction( - &self, - transaction: TransactionRequest, - ) -> Pin> + '_>> - { - Box::pin(async move { - let provider = self - .provider() + #[allow(clippy::type_complexity)] + async fn ws_provider(&self) -> anyhow::Result>> { + self.persistent_ws_provider + .get_or_try_init(|| async move { + construct_concurrency_limited_provider::( + self.ws_connection_string.as_str(), + FallbackGasFiller::default(), + ChainIdFiller::new(Some(CHAIN_ID)), + NonceFiller::new(self.nonce_manager.clone()), + self.wallet.clone(), + ) .await - .context("Failed to create provider for transaction submission")?; + .context("Failed to construct the provider") + }) + .await + .cloned() + } + #[instrument( + level = "info", + skip_all, + fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string), + err(Debug), + )] + #[allow(clippy::type_complexity)] + async fn http_provider( + &self, + ) -> anyhow::Result>> { + self.persistent_http_provider + .get_or_try_init(|| async move { + construct_concurrency_limited_provider::( + self.http_connection_string.as_str(), + FallbackGasFiller::default(), + ChainIdFiller::new(Some(CHAIN_ID)), + NonceFiller::new(self.nonce_manager.clone()), + self.wallet.clone(), + ) + .await + .context("Failed to construct the provider") + }) + .await + .cloned() + } + + /// Funds all of the accounts in the Ethereum wallet from the initially funded account. + #[instrument( + level = "info", + skip_all, + fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string), + err(Debug), + )] + async fn fund_all_accounts(&self) -> anyhow::Result<()> { + let mut full_block_subscriber = self + .ws_provider() + .await + .context("Failed to create the WS provider")? + .subscribe_full_blocks() + .into_stream() + .await + .context("Full block subscriber")?; + + let mut tx_hashes = futures::future::try_join_all( + NetworkWallet::::signer_addresses(self.wallet.as_ref()) + .enumerate() + .map(|(nonce, address)| async move { + let mut transaction = TransactionRequest::default() + .from(self.prefunded_account_address) + .to(address) + .nonce(nonce as _) + .value(INITIAL_BALANCE.try_into().unwrap()); + transaction.chain_id = Some(CHAIN_ID); + self.submit_transaction(transaction).await + }), + ) + .await + .context("Failed to submit all transactions")? + .into_iter() + .collect::>(); + + while let Some(block) = full_block_subscriber.next().await { + let Ok(block) = block else { + continue; + }; + + let block_number = block.number(); + let block_timestamp = block.header.timestamp; + let block_transaction_count = block.transactions.len(); + + for hash in block.transactions.into_hashes().as_hashes().unwrap() { + tx_hashes.remove(hash); + } + + info!( + block.number = block_number, + block.timestamp = block_timestamp, + block.transaction_count = block_transaction_count, + remaining_transactions = tx_hashes.len(), + "Discovered new block when funding accounts" + ); + + if tx_hashes.is_empty() { + break; + } + } + + Ok(()) + } + + fn internal_execute_transaction<'a>( + transaction: TransactionRequest, + provider: FillProvider< + impl TxFiller + 'a, + impl Provider + Clone + 'a, + Ethereum, + >, + ) -> Pin> + 'a>> { + Box::pin(async move { let pending_transaction = provider .send_transaction(transaction) .await @@ -404,10 +513,9 @@ impl EthereumNode for LighthouseGethNode { // allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting // 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)), + PollingWaitBehavior::Constant(Duration::from_millis(500)), move || { let provider = provider.clone(); async move { @@ -432,17 +540,94 @@ impl EthereumNode for LighthouseGethNode { .await }) } +} - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] +impl EthereumNode for LighthouseGethNode { + fn pre_transactions(&mut self) -> Pin> + '_>> { + Box::pin(async move { self.fund_all_accounts().await }) + } + + fn id(&self) -> usize { + self.id as _ + } + + fn connection_string(&self) -> &str { + &self.ws_connection_string + } + + #[instrument( + level = "info", + skip_all, + fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string), + err, + )] + fn submit_transaction( + &self, + transaction: TransactionRequest, + ) -> Pin> + '_>> { + Box::pin(async move { + let _permit = self.http_provider_requests_semaphore.acquire().await; + + let provider = self + .http_provider() + .await + .context("Failed to create the provider for transaction submission")?; + let pending_transaction = provider + .send_transaction(transaction) + .await + .context("Failed to submit the transaction through the provider")?; + Ok(*pending_transaction.tx_hash()) + }) + } + + #[instrument( + level = "info", + skip_all, + fields(lighthouse_node_id = self.id, connection_string = self.ws_connection_string), + )] + fn get_receipt( + &self, + tx_hash: TxHash, + ) -> Pin> + '_>> { + Box::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 + .http_provider() + .await + .context("Failed to create provider for transaction execution")?; + Self::internal_execute_transaction(transaction, provider).await + }) + } + + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn trace_transaction( &self, tx_hash: TxHash, trace_options: GethDebugTracingOptions, - ) -> Pin> + '_>> - { + ) -> Pin> + '_>> { Box::pin(async move { let provider = Arc::new( - self.provider() + self.http_provider() .await .context("Failed to create provider for tracing")?, ); @@ -473,7 +658,7 @@ impl EthereumNode for LighthouseGethNode { }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn state_diff( &self, tx_hash: TxHash, @@ -497,13 +682,13 @@ impl EthereumNode for LighthouseGethNode { }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn balance_of( &self, address: Address, ) -> Pin> + '_>> { Box::pin(async move { - self.provider() + self.ws_provider() .await .context("Failed to get the Geth provider")? .get_balance(address) @@ -512,14 +697,14 @@ impl EthereumNode for LighthouseGethNode { }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn latest_state_proof( &self, address: Address, keys: Vec, ) -> Pin> + '_>> { Box::pin(async move { - self.provider() + self.ws_provider() .await .context("Failed to get the Geth provider")? .get_proof(address, keys) @@ -529,13 +714,13 @@ impl EthereumNode for LighthouseGethNode { }) } - // #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn resolver( &self, ) -> Pin>> + '_>> { Box::pin(async move { let id = self.id; - let provider = self.provider().await?; + let provider = self.ws_provider().await?; Ok(Arc::new(LighthouseGethNodeResolver { id, provider }) as Arc) }) } @@ -543,6 +728,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> { @@ -553,14 +775,14 @@ pub struct LighthouseGethNodeResolver, P: Provider, P: Provider> ResolverApi for LighthouseGethNodeResolver { - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn chain_id( &self, ) -> Pin> + '_>> { Box::pin(async move { self.provider.get_chain_id().await.map_err(Into::into) }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn transaction_gas_price( &self, tx_hash: TxHash, @@ -574,7 +796,7 @@ impl, P: Provider> ResolverApi }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn block_gas_limit( &self, number: BlockNumberOrTag, @@ -589,7 +811,7 @@ impl, P: Provider> ResolverApi }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn block_coinbase( &self, number: BlockNumberOrTag, @@ -604,7 +826,7 @@ impl, P: Provider> ResolverApi }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn block_difficulty( &self, number: BlockNumberOrTag, @@ -619,7 +841,7 @@ impl, P: Provider> ResolverApi }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn block_base_fee( &self, number: BlockNumberOrTag, @@ -639,7 +861,7 @@ impl, P: Provider> ResolverApi }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn block_hash( &self, number: BlockNumberOrTag, @@ -654,7 +876,7 @@ impl, P: Provider> ResolverApi }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn block_timestamp( &self, number: BlockNumberOrTag, @@ -669,29 +891,55 @@ impl, P: Provider> ResolverApi }) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn last_block_number(&self) -> Pin> + '_>> { Box::pin(async move { self.provider.get_block_number().await.map_err(Into::into) }) } } impl Node for LighthouseGethNode { - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn shutdown(&mut self) -> anyhow::Result<()> { - if !Command::new(self.kurtosis_binary_path.as_path()) + let mut child = Command::new(self.kurtosis_binary_path.as_path()) .arg("enclave") .arg("rm") .arg("-f") .arg(self.enclave_name.as_str()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) .spawn() - .expect("Failed to spawn the enclave kill command") + .expect("Failed to spawn the enclave kill command"); + + if !child .wait() .expect("Failed to wait for the enclave kill command") .success() { - panic!("Failed to shut down the enclave {}", self.enclave_name) + let stdout = { + let mut stdout = String::default(); + child + .stdout + .take() + .expect("Should be piped") + .read_to_string(&mut stdout) + .context("Failed to read stdout of kurtosis inspect to string")?; + stdout + }; + let stderr = { + let mut stderr = String::default(); + child + .stderr + .take() + .expect("Should be piped") + .read_to_string(&mut stderr) + .context("Failed to read stderr of kurtosis inspect to string")?; + stderr + }; + + panic!( + "Failed to shut down the enclave {} - stdout: {stdout}, stderr: {stderr}", + self.enclave_name + ) } drop(self.process.take()); @@ -699,13 +947,13 @@ impl Node for LighthouseGethNode { Ok(()) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> { self.init(genesis)?.spawn_process()?; Ok(()) } - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn version(&self) -> anyhow::Result { let output = Command::new(&self.kurtosis_binary_path) .arg("version") @@ -722,7 +970,7 @@ impl Node for LighthouseGethNode { } impl Drop for LighthouseGethNode { - #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] + #[instrument(level = "info", skip_all, fields(lighthouse_node_id = self.id))] fn drop(&mut self) { self.shutdown().expect("Failed to shutdown") } @@ -853,7 +1101,9 @@ mod tests { use super::*; fn test_config() -> TestExecutionContext { - TestExecutionContext::default() + let mut config = TestExecutionContext::default(); + config.wallet_configuration.additional_keys = 100; + config } fn new_node() -> (TestExecutionContext, LighthouseGethNode) { @@ -888,6 +1138,7 @@ mod tests { async fn node_mines_simple_transfer_transaction_and_returns_receipt() { // Arrange let (context, node) = new_node(); + node.fund_all_accounts().await.expect("Failed"); let account_address = context .wallet_configuration @@ -906,6 +1157,7 @@ mod tests { } #[test] + #[ignore = "Ignored since they take a long time to run"] fn version_works() { // Arrange let (_context, node) = new_node(); @@ -922,6 +1174,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_chain_id_from_node() { // Arrange let (_context, node) = new_node(); @@ -935,6 +1188,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_gas_limit_from_node() { // Arrange let (_context, node) = new_node(); @@ -952,6 +1206,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_coinbase_from_node() { // Arrange let (_context, node) = new_node(); @@ -969,6 +1224,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_difficulty_from_node() { // Arrange let (_context, node) = new_node(); @@ -986,6 +1242,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_hash_from_node() { // Arrange let (_context, node) = new_node(); @@ -1003,6 +1260,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_timestamp_from_node() { // Arrange let (_context, node) = new_node(); @@ -1020,6 +1278,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_number_from_node() { // Arrange let (_context, node) = new_node(); diff --git a/crates/node/src/node_implementations/mod.rs b/crates/node/src/node_implementations/mod.rs new file mode 100644 index 0000000..83b8371 --- /dev/null +++ b/crates/node/src/node_implementations/mod.rs @@ -0,0 +1,3 @@ +pub mod geth; +pub mod lighthouse_geth; +pub mod substrate; diff --git a/crates/node/src/substrate.rs b/crates/node/src/node_implementations/substrate.rs similarity index 89% rename from crates/node/src/substrate.rs rename to crates/node/src/node_implementations/substrate.rs index b76c46c..fd746a1 100644 --- a/crates/node/src/substrate.rs +++ b/crates/node/src/node_implementations/substrate.rs @@ -23,17 +23,20 @@ use alloy::{ TxHash, U256, }, providers::{ - Provider, ProviderBuilder, + Provider, ext::DebugApi, - fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, + fillers::{CachedNonceManager, ChainIdFiller, NonceFiller}, }, 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,14 +46,15 @@ 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::{ Node, - common::FallbackGasFiller, - constants::INITIAL_BALANCE, - process::{Process, ProcessReadinessWaitBehavior}, + constants::{CHAIN_ID, INITIAL_BALANCE}, + helpers::{Process, ProcessReadinessWaitBehavior}, + provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider}, }; static NODE_COUNT: AtomicU32 = AtomicU32::new(0); @@ -59,6 +63,7 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0); /// or the revive-dev-node which is done by changing the path and some of the other arguments passed /// to the command. #[derive(Debug)] + pub struct SubstrateNode { id: u32, node_binary: PathBuf, @@ -71,7 +76,7 @@ pub struct SubstrateNode { eth_proxy_process: Option, wallet: Arc, nonce_manager: CachedNonceManager, - chain_id_filler: ChainIdFiller, + provider: OnceCell>>, } impl SubstrateNode { @@ -121,8 +126,8 @@ impl SubstrateNode { substrate_process: None, eth_proxy_process: None, wallet: wallet.clone(), - chain_id_filler: Default::default(), nonce_manager: Default::default(), + provider: Default::default(), } } @@ -144,6 +149,7 @@ impl SubstrateNode { .arg(self.export_chainspec_command.as_str()) .arg("--chain") .arg("dev") + .env_remove("RUST_LOG") .output() .context("Failed to export the chain-spec")?; @@ -335,27 +341,29 @@ impl SubstrateNode { async fn provider( &self, - ) -> anyhow::Result< - FillProvider, impl Provider, 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) + ) -> anyhow::Result>> { + self.provider + .get_or_try_init(|| async move { + construct_concurrency_limited_provider::( + self.rpc_url.as_str(), + FallbackGasFiller::new(250_000_000, 5_000_000_000, 1_000_000_000), + ChainIdFiller::new(Some(CHAIN_ID)), + NonceFiller::new(self.nonce_manager.clone()), + self.wallet.clone(), + ) + .await + .context("Failed to construct the provider") + }) .await - .map_err(Into::into) + .cloned() } } impl EthereumNode for SubstrateNode { + fn pre_transactions(&mut self) -> Pin> + '_>> { + Box::pin(async move { Ok(()) }) + } + fn id(&self) -> usize { self.id as _ } @@ -364,11 +372,48 @@ impl EthereumNode for SubstrateNode { &self.rpc_url } - fn execute_transaction( + fn submit_transaction( &self, - transaction: alloy::rpc::types::TransactionRequest, + 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: TransactionRequest, + ) -> Pin> + '_>> { + static SEMAPHORE: std::sync::LazyLock = + std::sync::LazyLock::new(|| tokio::sync::Semaphore::new(500)); + + Box::pin(async move { + let _permit = SEMAPHORE.acquire().await?; + let receipt = self .provider() .await @@ -387,8 +432,7 @@ impl EthereumNode for SubstrateNode { &self, tx_hash: TxHash, trace_options: GethDebugTracingOptions, - ) -> Pin> + '_>> - { + ) -> Pin> + '_>> { Box::pin(async move { self.provider() .await @@ -463,16 +507,56 @@ 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 mut block_subscription = provider + .watch_full_blocks() + .await + .context("Failed to create the blocks stream")?; + block_subscription.set_channel_size(0xFFFF); + block_subscription.set_poll_interval(Duration::from_secs(1)); + let block_stream = block_subscription.into_stream(); + + let mined_block_information_stream = block_stream.filter_map(|block| async { + let block = block.ok()?; + Some(MinedBlockInformation { + block_number: block.number(), + block_timestamp: block.header.timestamp, + mined_gas: block.header.gas_used as _, + block_gas_limit: block.header.gas_limit, + transaction_hashes: block + .transactions + .into_hashes() + .as_hashes() + .expect("Must be hashes") + .to_vec(), + }) + }); + + Ok(Box::pin(mined_block_information_stream) + as Pin>>) + }) + } } -pub struct SubstrateNodeResolver, P: Provider> { +pub struct SubstrateNodeResolver { id: u32, - provider: FillProvider, + provider: ConcreteProvider>, } -impl, P: Provider> ResolverApi - for SubstrateNodeResolver -{ +impl ResolverApi for SubstrateNodeResolver { #[instrument(level = "info", skip_all, fields(substrate_node_id = self.id))] fn chain_id( &self, @@ -1068,9 +1152,7 @@ mod tests { use crate::Node; fn test_config() -> TestExecutionContext { - let mut context = TestExecutionContext::default(); - context.kitchensink_configuration.use_kitchensink = true; - context + TestExecutionContext::default() } fn new_node() -> (TestExecutionContext, SubstrateNode) { @@ -1142,6 +1224,7 @@ mod tests { } #[test] + #[ignore = "Ignored since they take a long time to run"] fn test_init_generates_chainspec_with_balances() { let genesis_content = r#" { @@ -1195,6 +1278,7 @@ mod tests { } #[test] + #[ignore = "Ignored since they take a long time to run"] fn test_parse_genesis_alloc() { // Create test genesis file let genesis_json = r#" @@ -1237,6 +1321,7 @@ mod tests { } #[test] + #[ignore = "Ignored since they take a long time to run"] fn print_eth_to_substrate_mappings() { let eth_addresses = vec![ "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", @@ -1252,6 +1337,7 @@ mod tests { } #[test] + #[ignore = "Ignored since they take a long time to run"] fn test_eth_to_substrate_address() { let cases = vec![ ( @@ -1282,6 +1368,7 @@ mod tests { } #[test] + #[ignore = "Ignored since they take a long time to run"] fn version_works() { let node = shared_node(); @@ -1294,6 +1381,7 @@ mod tests { } #[test] + #[ignore = "Ignored since they take a long time to run"] fn eth_rpc_version_works() { let node = shared_node(); @@ -1306,6 +1394,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_chain_id_from_node() { // Arrange let node = shared_node(); @@ -1319,6 +1408,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_gas_limit_from_node() { // Arrange let node = shared_node(); @@ -1336,6 +1426,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_coinbase_from_node() { // Arrange let node = shared_node(); @@ -1353,6 +1444,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_difficulty_from_node() { // Arrange let node = shared_node(); @@ -1370,6 +1462,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_hash_from_node() { // Arrange let node = shared_node(); @@ -1387,6 +1480,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_timestamp_from_node() { // Arrange let node = shared_node(); @@ -1404,6 +1498,7 @@ mod tests { } #[tokio::test] + #[ignore = "Ignored since they take a long time to run"] async fn can_get_block_number_from_node() { // Arrange let node = shared_node(); diff --git a/crates/node/src/provider_utils/concurrency_limiter.rs b/crates/node/src/provider_utils/concurrency_limiter.rs new file mode 100644 index 0000000..73878b9 --- /dev/null +++ b/crates/node/src/provider_utils/concurrency_limiter.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use alloy::transports::BoxFuture; +use tokio::sync::Semaphore; +use tower::{Layer, Service}; + +#[derive(Clone, Debug)] +pub struct ConcurrencyLimiterLayer { + semaphore: Arc, +} + +impl ConcurrencyLimiterLayer { + pub fn new(permit_count: usize) -> Self { + Self { + semaphore: Arc::new(Semaphore::new(permit_count)), + } + } +} + +impl Layer for ConcurrencyLimiterLayer { + type Service = ConcurrencyLimiterService; + + fn layer(&self, inner: S) -> Self::Service { + ConcurrencyLimiterService { + service: inner, + semaphore: self.semaphore.clone(), + } + } +} + +#[derive(Clone)] +pub struct ConcurrencyLimiterService { + service: S, + semaphore: Arc, +} + +impl Service for ConcurrencyLimiterService +where + S: Service + Send, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let semaphore = self.semaphore.clone(); + let future = self.service.call(req); + + Box::pin(async move { + let _permit = semaphore + .acquire() + .await + .expect("Semaphore has been closed"); + tracing::debug!( + available_permits = semaphore.available_permits(), + "Acquired Semaphore Permit" + ); + future.await + }) + } +} diff --git a/crates/node/src/common.rs b/crates/node/src/provider_utils/fallback_gas_provider.rs similarity index 93% rename from crates/node/src/common.rs rename to crates/node/src/provider_utils/fallback_gas_provider.rs index f260062..ff74ea2 100644 --- a/crates/node/src/common.rs +++ b/crates/node/src/provider_utils/fallback_gas_provider.rs @@ -30,6 +30,12 @@ impl FallbackGasFiller { } } +impl Default for FallbackGasFiller { + fn default() -> Self { + FallbackGasFiller::new(25_000_000, 1_000_000_000, 1_000_000_000) + } +} + impl TxFiller for FallbackGasFiller where N: Network, diff --git a/crates/node/src/provider_utils/mod.rs b/crates/node/src/provider_utils/mod.rs new file mode 100644 index 0000000..b0738da --- /dev/null +++ b/crates/node/src/provider_utils/mod.rs @@ -0,0 +1,7 @@ +mod concurrency_limiter; +mod fallback_gas_provider; +mod provider; + +pub use concurrency_limiter::*; +pub use fallback_gas_provider::*; +pub use provider::*; diff --git a/crates/node/src/provider_utils/provider.rs b/crates/node/src/provider_utils/provider.rs new file mode 100644 index 0000000..e69ed20 --- /dev/null +++ b/crates/node/src/provider_utils/provider.rs @@ -0,0 +1,63 @@ +use std::sync::LazyLock; + +use alloy::{ + network::{Network, NetworkWallet, TransactionBuilder4844}, + providers::{ + Identity, ProviderBuilder, RootProvider, + fillers::{ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, WalletFiller}, + }, + rpc::client::ClientBuilder, +}; +use anyhow::{Context, Result}; + +use crate::provider_utils::{ConcurrencyLimiterLayer, FallbackGasFiller}; + +pub type ConcreteProvider = FillProvider< + JoinFill< + JoinFill, ChainIdFiller>, NonceFiller>, + WalletFiller, + >, + RootProvider, + N, +>; + +pub async fn construct_concurrency_limited_provider( + rpc_url: &str, + fallback_gas_filler: FallbackGasFiller, + chain_id_filler: ChainIdFiller, + nonce_filler: NonceFiller, + wallet: W, +) -> Result> +where + N: Network, + W: NetworkWallet, + Identity: TxFiller, + FallbackGasFiller: TxFiller, + ChainIdFiller: TxFiller, + NonceFiller: TxFiller, + WalletFiller: TxFiller, +{ + // This is a global limit on the RPC concurrency that applies to all of the providers created + // by the framework. With this limit, it means that we can have a maximum of N concurrent + // requests at any point of time and no more than that. This is done in an effort to stabilize + // the framework from some of the interment issues that we've been seeing related to RPC calls. + static GLOBAL_CONCURRENCY_LIMITER_LAYER: LazyLock = + LazyLock::new(|| ConcurrencyLimiterLayer::new(10)); + + let client = ClientBuilder::default() + .layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone()) + .connect(rpc_url) + .await + .context("Failed to construct the RPC client")?; + + let provider = ProviderBuilder::new() + .disable_recommended_fillers() + .network::() + .filler(fallback_gas_filler) + .filler(chain_id_filler) + .filler(nonce_filler) + .wallet(wallet) + .connect_client(client); + + Ok(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 b3a693d..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,6 +106,10 @@ impl ReportAggregator { RunnerEvent::ContractDeployed(event) => { self.handle_contract_deployed_event(*event); } + RunnerEvent::Completion(event) => { + self.handle_completion(*event); + break; + } } } debug!("Report aggregation completed"); @@ -382,6 +386,10 @@ impl ReportAggregator { .insert(event.contract_instance, event.address); } + fn handle_completion(&mut self, _: CompletionEvent) { + self.runner_rx.close(); + } + fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut TestCaseReport { self.report .test_case_information diff --git a/crates/report/src/runner_event.rs b/crates/report/src/runner_event.rs index 361555c..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; @@ -613,6 +613,8 @@ define_event! { /// The address of the contract. address: Address }, + /// Reports the completion of the run. + Completion {} } } diff --git a/run_tests.sh b/run_tests.sh index 9250f1a..f569156 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -75,7 +75,11 @@ ABSOLUTE_PATH=$(realpath "$TEST_REPO_DIR/fixtures/solidity/") cat > "$CORPUS_FILE" << EOF { "name": "MatterLabs Solidity Simple, Complex, and Semantic Tests", - "path": "$ABSOLUTE_PATH" + "paths": [ + "$(realpath "$TEST_REPO_DIR/fixtures/solidity/translated_semantic_tests")", + "$(realpath "$TEST_REPO_DIR/fixtures/solidity/complex")", + "$(realpath "$TEST_REPO_DIR/fixtures/solidity/simple")" + ] } EOF @@ -89,12 +93,14 @@ echo "This may take a while..." echo "" # Run the tool -RUST_LOG="info" cargo run --release -- execute-tests \ +cargo build --release; +RUST_LOG="info,alloy_pubsub::service=error" ./target/release/retester test \ --platform geth-evm-solc \ - --platform revive-dev-node-polkavm-resolc \ + --platform revive-dev-node-revm-solc \ --corpus "$CORPUS_FILE" \ --working-directory "$WORKDIR" \ --concurrency.number-of-nodes 5 \ + --wallet.additional-keys 100000 \ --kitchensink.path "$SUBSTRATE_NODE_BIN" \ --revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \ --eth-rpc.path "$ETH_RPC_BIN" \