diff --git a/.changelog/exact-fork-ancestry.md b/.changelog/exact-fork-ancestry.md new file mode 100644 index 0000000000000..903ddc1a8fbd9 --- /dev/null +++ b/.changelog/exact-fork-ancestry.md @@ -0,0 +1,7 @@ +--- +forge: patch +chisel: patch +anvil: patch +--- + +Pinned fork state reads and `BLOCKHASH` ancestry to the exact resolved block across reorgs. diff --git a/Cargo.lock b/Cargo.lock index 38856890ec1f4..6a9ff4e11f603 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1212,7 +1212,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1236,7 +1236,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3249,7 +3249,7 @@ dependencies = [ "terminfo", "thiserror 2.0.19", "which", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3408,7 +3408,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3440,9 +3440,9 @@ checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" [[package]] name = "commonware-actor" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beac6262223b45c6843ab70b245b526b28836a8257b1759a509ba5fab24fa4a6" +checksum = "c51f1d2adda7330ae87c9ac815d707a2a41bb91a11dc1a588e5a81fe2a5a2b1b" dependencies = [ "cfg-if", "commonware-macros", @@ -3454,9 +3454,9 @@ dependencies = [ [[package]] name = "commonware-broadcast" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a2d7e56dc894ad3f55dbe5726468915492ae8e4650ad2a45c1bcfd138c2037f" +checksum = "0f0e4a50ee1fa3ed2db220da0b0b85546556edc20cc5b71acc90af3d50a94821" dependencies = [ "commonware-actor", "commonware-codec", @@ -3471,9 +3471,9 @@ dependencies = [ [[package]] name = "commonware-codec" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8eb7efe071cea13e8b23a0a7f584398870c401d2b2aabfb6b9ab3634ebcba3a" +checksum = "785ff1148d798363638d83a1dfcca9db8ac6001a27292a23dc10a8595e1ee480" dependencies = [ "bytes", "cfg-if", @@ -3486,9 +3486,9 @@ dependencies = [ [[package]] name = "commonware-codec-macros" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a876bec347344381948901d0f5ec2e9443ebc8dbab99a63bc2db78c2fc2efbdd" +checksum = "3e308eca99c0be5ae7d8c19ecb5c2c85b7484b3b7e24f6e36eaed46a9a7cad0c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3498,9 +3498,9 @@ dependencies = [ [[package]] name = "commonware-coding" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409e9997c1bb3273b9570c26ddef4f252327c9bf18968d4439a83cfd1d3e5661" +checksum = "d561336cf1562aca4ac8df2c51035fdab8bd9d8a6f23764ba3c7ad9b87f574c9" dependencies = [ "bytes", "commonware-codec", @@ -3517,9 +3517,9 @@ dependencies = [ [[package]] name = "commonware-consensus" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf15e3402e26f062695d4dd5e4b5967f601dc7303d06b9fafdb20a6485a3a0a" +checksum = "b8c854f77e510cf97cffcf02723737bca035ee77b1aa9fc7d6dd88694d3c8f0d" dependencies = [ "bytes", "cfg-if", @@ -3548,9 +3548,9 @@ dependencies = [ [[package]] name = "commonware-cryptography" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b842f31e1aaa1af284a1f9dc4f1234761f72d07e70d53cf3f1521b0870ade094" +checksum = "57bb1a88685e18438cea8258ae5073c89175dcd7c1f8711f5b60ef0be17c80ff" dependencies = [ "ahash", "anyhow", @@ -3590,9 +3590,9 @@ dependencies = [ [[package]] name = "commonware-formatting" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab01b7f2798e29b0a4f7b47da5b7f3c106881578af3e69e8d2ee9d89d7c1d4fd" +checksum = "15d388a79f0fad7ad9fad6b5f4a594c1e6675935221224aaa4d7e376b362c203" dependencies = [ "commonware-macros", "const-hex", @@ -3600,9 +3600,9 @@ dependencies = [ [[package]] name = "commonware-macros" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e37a17d5c99ec6711098c6614af1023fd32926aa0414ec0f0613f6bb940278" +checksum = "b49853beaa75a447c448eaf6a48a38ba247a85792807956bc4487565a35fa35b" dependencies = [ "commonware-macros-impl", "tokio", @@ -3610,9 +3610,9 @@ dependencies = [ [[package]] name = "commonware-macros-impl" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d44bceee874226efca172f0e9ec9ec4eb486788ba6be1d98710f3f159b807af" +checksum = "b4b099d6d10014a6ab1ae0b112c8329570adacc1fdbf6b27c9aebb38195f4eee" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3623,9 +3623,9 @@ dependencies = [ [[package]] name = "commonware-math" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "593fa0c5aa8bd8d350724b9b1ad4e85072845aa85a06b39ca4598998bbec1e00" +checksum = "4fd414779c4ebd26d9806cf012ad4f490555844aedafafa64cc26428e2f854f1" dependencies = [ "bytes", "commonware-codec", @@ -3637,9 +3637,9 @@ dependencies = [ [[package]] name = "commonware-p2p" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47f974fcd27c0dad7ae73caeaf00c8330fd29c60f5ff1f679ccc1a51c8ed95bc" +checksum = "3775a5d3bb4dcb20d654ff64b5504234822995dc16cb4417ea65b03225944ed7" dependencies = [ "commonware-actor", "commonware-codec", @@ -3664,9 +3664,9 @@ dependencies = [ [[package]] name = "commonware-parallel" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7109817274c671f1fb0ba17cf8aecfbb70672d29379284feeac43fa1b1dc0201" +checksum = "b7a1b76ed5be614064839d20ec70f0177f92ba7da04dea0f5b0411c7e10a9ad6" dependencies = [ "cfg-if", "commonware-macros", @@ -3677,9 +3677,9 @@ dependencies = [ [[package]] name = "commonware-resolver" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a497cc9e9a2cd205d96474d0d40c7731f29085cd467adbe9ff0f94719659582" +checksum = "5958d4f162dc81cecf87cda2c00fdca00a750c994149f776d0b1ed39e0bb921c" dependencies = [ "bytes", "commonware-actor", @@ -3699,9 +3699,9 @@ dependencies = [ [[package]] name = "commonware-runtime" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a84bc6cefb099b1ffce3e04705a1e61e91d895b9f09b9e1be14c8ec5823c181" +checksum = "082c8a0fc3962c3aceddf9f5532322b15564369a7b9bf846220ff750051ce9cb" dependencies = [ "ahash", "axum", @@ -3739,9 +3739,9 @@ dependencies = [ [[package]] name = "commonware-runtime-macros" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f7b39ef9fd9f1df4fe4445c9a5ec5ba012d90d1602b12a8b8626a8563ab02c" +checksum = "e4982c0997b8a5d2039234a55fff383e443cc72f0c7b2a52b8ab93075c3edeae" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3751,9 +3751,9 @@ dependencies = [ [[package]] name = "commonware-storage" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac18e2b696ab7bbecf1059bc3b8d39fc0af002180906d65c7df3112fd9385de" +checksum = "627b4a7d67ab75bf4d8115fd883d74b9839b36a1405f5b23a874827a62dfceb1" dependencies = [ "ahash", "anyhow", @@ -3776,9 +3776,9 @@ dependencies = [ [[package]] name = "commonware-stream" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8890b69e3fa209bc9e87d199ff60ca5a21c88322d32914847511887e6dc5364b" +checksum = "0ce2d3acfc869e78d33577c1284d9cf2a0a67f98fcf09ab5d6e43efeed2d2bf4" dependencies = [ "chacha20poly1305", "commonware-codec", @@ -3796,9 +3796,9 @@ dependencies = [ [[package]] name = "commonware-utils" -version = "2026.7.0" +version = "2026.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a484dd46b738d9b5ba892312425e71e6afaf4320e388d74449bd9ca0ea14cd76" +checksum = "54c7b4991ff7135416985adf3bf39f16f77cd2066fbbe2e3854e99379d90c6e5" dependencies = [ "ahash", "bytes", @@ -4633,7 +4633,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4940,7 +4940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5541,7 +5541,7 @@ dependencies = [ [[package]] name = "foundry-block-explorers" version = "0.24.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=eabd2e218a7606505430d84bfeacd145e2c1eff3#eabd2e218a7606505430d84bfeacd145e2c1eff3" +source = "git+https://github.com/foundry-rs/foundry-core?rev=8d88d5b2ded889848a3f6de791ef1a8c60da918b#8d88d5b2ded889848a3f6de791ef1a8c60da918b" dependencies = [ "alloy-chains", "alloy-json-abi", @@ -5784,7 +5784,7 @@ dependencies = [ [[package]] name = "foundry-compilers" version = "0.21.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=eabd2e218a7606505430d84bfeacd145e2c1eff3#eabd2e218a7606505430d84bfeacd145e2c1eff3" +source = "git+https://github.com/foundry-rs/foundry-core?rev=8d88d5b2ded889848a3f6de791ef1a8c60da918b#8d88d5b2ded889848a3f6de791ef1a8c60da918b" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5815,7 +5815,7 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts" version = "0.21.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=eabd2e218a7606505430d84bfeacd145e2c1eff3#eabd2e218a7606505430d84bfeacd145e2c1eff3" +source = "git+https://github.com/foundry-rs/foundry-core?rev=8d88d5b2ded889848a3f6de791ef1a8c60da918b#8d88d5b2ded889848a3f6de791ef1a8c60da918b" dependencies = [ "foundry-compilers-artifacts-solc", "foundry-compilers-artifacts-vyper", @@ -5824,7 +5824,7 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-solc" version = "0.21.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=eabd2e218a7606505430d84bfeacd145e2c1eff3#eabd2e218a7606505430d84bfeacd145e2c1eff3" +source = "git+https://github.com/foundry-rs/foundry-core?rev=8d88d5b2ded889848a3f6de791ef1a8c60da918b#8d88d5b2ded889848a3f6de791ef1a8c60da918b" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5844,7 +5844,7 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-vyper" version = "0.21.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=eabd2e218a7606505430d84bfeacd145e2c1eff3#eabd2e218a7606505430d84bfeacd145e2c1eff3" +source = "git+https://github.com/foundry-rs/foundry-core?rev=8d88d5b2ded889848a3f6de791ef1a8c60da918b#8d88d5b2ded889848a3f6de791ef1a8c60da918b" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5857,7 +5857,7 @@ dependencies = [ [[package]] name = "foundry-compilers-core" version = "0.21.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=eabd2e218a7606505430d84bfeacd145e2c1eff3#eabd2e218a7606505430d84bfeacd145e2c1eff3" +source = "git+https://github.com/foundry-rs/foundry-core?rev=8d88d5b2ded889848a3f6de791ef1a8c60da918b#8d88d5b2ded889848a3f6de791ef1a8c60da918b" dependencies = [ "alloy-primitives", "dunce", @@ -6195,9 +6195,10 @@ dependencies = [ [[package]] name = "foundry-fork-db" version = "0.27.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=eabd2e218a7606505430d84bfeacd145e2c1eff3#eabd2e218a7606505430d84bfeacd145e2c1eff3" +source = "git+https://github.com/foundry-rs/foundry-core?rev=8d88d5b2ded889848a3f6de791ef1a8c60da918b#8d88d5b2ded889848a3f6de791ef1a8c60da918b" dependencies = [ "alloy-chains", + "alloy-consensus", "alloy-primitives", "alloy-provider", "alloy-rpc-types", @@ -6302,7 +6303,7 @@ dependencies = [ [[package]] name = "foundry-wallets" version = "0.1.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=eabd2e218a7606505430d84bfeacd145e2c1eff3#eabd2e218a7606505430d84bfeacd145e2c1eff3" +source = "git+https://github.com/foundry-rs/foundry-core?rev=8d88d5b2ded889848a3f6de791ef1a8c60da918b#8d88d5b2ded889848a3f6de791ef1a8c60da918b" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -7363,7 +7364,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8188,7 +8189,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8427,7 +8428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b685c8311c9171d1bd2895222965d25616b2de2cb5819dd3504ed9250df9fecd" dependencies = [ "ahash", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "parking_lot", "stable_deref_trait", ] @@ -9615,7 +9616,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -11000,7 +11001,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -11059,7 +11060,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -11827,7 +11828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -12467,7 +12468,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -12691,7 +12692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -13860,7 +13861,7 @@ dependencies = [ "watchexec-events", "watchexec-signals", "watchexec-supervisor", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -14008,7 +14009,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 84188557039c8..a8896b7170960 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -567,10 +567,10 @@ solar-cli = { git = "https://github.com/paradigmxyz/solar", rev = "a38f69e4f2e22 solar-lint = { git = "https://github.com/paradigmxyz/solar", rev = "a38f69e4f2e2267c549555a980aef6b0b1f249eb" } ## foundry-core -foundry-wallets = { git = "https://github.com/foundry-rs/foundry-core", rev = "eabd2e218a7606505430d84bfeacd145e2c1eff3" } -foundry-compilers = { git = "https://github.com/foundry-rs/foundry-core", rev = "eabd2e218a7606505430d84bfeacd145e2c1eff3" } -foundry-block-explorers = { git = "https://github.com/foundry-rs/foundry-core", rev = "eabd2e218a7606505430d84bfeacd145e2c1eff3" } -foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-core", rev = "eabd2e218a7606505430d84bfeacd145e2c1eff3" } +foundry-wallets = { git = "https://github.com/foundry-rs/foundry-core", rev = "8d88d5b2ded889848a3f6de791ef1a8c60da918b" } +foundry-compilers = { git = "https://github.com/foundry-rs/foundry-core", rev = "8d88d5b2ded889848a3f6de791ef1a8c60da918b" } +foundry-block-explorers = { git = "https://github.com/foundry-rs/foundry-core", rev = "8d88d5b2ded889848a3f6de791ef1a8c60da918b" } +foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-core", rev = "8d88d5b2ded889848a3f6de791ef1a8c60da918b" } ## alloy-core # alloy-dyn-abi = { path = "../../alloy-rs/core/crates/dyn-abi" } diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 9ea150a45fbfb..5858b15807463 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -45,7 +45,7 @@ use foundry_config::Config; #[cfg(feature = "monad")] use foundry_evm::hardfork::MonadHardfork; use foundry_evm::{ - backend::{BlockchainDb, BlockchainDbMeta, SharedBackend}, + backend::{BlockchainDb, BlockchainDbMeta, ForkBlock, SharedBackend}, constants::DEFAULT_CREATE2_DEPLOYER, hardfork::FoundryHardfork, utils::{apply_chain_and_block_specific_env_changes_for_chain, block_env_from_header}, @@ -151,6 +151,18 @@ const BANNER: &str = r" \__,_| |_| |_| \_/ |_| |_| "; +fn fork_source_id(urls: &[String], headers: &[String]) -> B256 { + let mut encoded = Vec::new(); + for parts in [urls, headers] { + encoded.extend_from_slice(&(parts.len() as u64).to_be_bytes()); + for part in parts { + encoded.extend_from_slice(&(part.len() as u64).to_be_bytes()); + encoded.extend_from_slice(part.as_bytes()); + } + } + keccak256(encoded) +} + /// Configurations of the EVM node #[derive(Clone, Debug)] pub struct NodeConfig { @@ -2024,14 +2036,12 @@ latest block number: {latest_block}" eyre::bail!("primary fork endpoint changed while its context was being validated"); } - let meta = BlockchainDbMeta::new(cache_block_env, eth_rpc_url.clone()); + let source_id = fork_source_id(&self.fork_urls, &self.fork_headers); + let meta = BlockchainDbMeta::new(cache_block_env, eth_rpc_url.clone()) + .with_fork_identity(block_hash, source_id); let cache_path = self.block_cache_path_for_rpc(source_chain_id, fork_block_number, ð_rpc_url); - let block_chain_db = if self.fork_chain_id.is_some() { - BlockchainDb::new_skip_check(meta, cache_path) - } else { - BlockchainDb::new(meta, cache_path) - }; + let block_chain_db = BlockchainDb::new(meta, cache_path); // After bootstrap, rebuild the provider with round-robin if multiple URLs are // configured. This ensures bootstrap used only the primary endpoint for consistency, @@ -2054,12 +2064,14 @@ latest block number: {latest_block}" // This will spawn the background thread that will use the provider to fetch // blockchain data from the other client - let backend = SharedBackend::spawn_backend( - Arc::clone(&provider), - block_chain_db.clone(), - Some(fork_block_number.into()), - ) - .await; + let anchor = ForkBlock::with_rpc_number( + evm_env.block_env.number.saturating_to(), + fork_block_number, + block_hash, + ); + let (backend, handler) = + SharedBackend::new_with_anchor(Arc::clone(&provider), block_chain_db.clone(), anchor)?; + tokio::spawn(handler); let config = ClientForkConfig { fork_urls: self.fork_urls.clone(), @@ -2436,6 +2448,17 @@ mod tests { use super::*; + #[test] + fn fork_source_identity_includes_all_urls_and_headers() { + let urls = ["http://primary".to_string(), "http://fallback".to_string()]; + let headers = ["Authorization: secret".to_string()]; + let identity = fork_source_id(&urls, &headers); + + assert_ne!(identity, fork_source_id(&urls[..1], &headers)); + assert_ne!(identity, fork_source_id(&urls, &[])); + assert_ne!(identity, fork_source_id(&[urls[1].clone(), urls[0].clone()], &headers)); + } + #[test] fn test_prune_history() { let config = PruneStateHistoryConfig::default(); diff --git a/crates/anvil/src/eth/backend/mem/fork_db.rs b/crates/anvil/src/eth/backend/mem/fork_db.rs index 47a6924669a16..6083ea1553621 100644 --- a/crates/anvil/src/eth/backend/mem/fork_db.rs +++ b/crates/anvil/src/eth/backend/mem/fork_db.rs @@ -46,11 +46,11 @@ impl Db for ForkedDatabase { } fn insert_block_hash(&mut self, number: U256, hash: B256) { - cache_block_hash(&mut self.inner().block_hashes().write(), number, hash); + cache_block_hash(&mut self.database_mut().cache.block_hashes, number, hash); } fn set_block_hashes(&mut self, block_hashes: Vec<(U256, B256)>) { - *self.inner().block_hashes().write() = block_hashes.into_iter().collect(); + self.database_mut().cache.block_hashes = block_hashes.into_iter().collect(); } fn dump_state( diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 6ccd7a63fa8ec..a844351c12b65 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -77,8 +77,9 @@ use alloy_evm::{ #[cfg(feature = "monad")] use alloy_monad_evm::{MonadContext, MonadEvmFactory}; use alloy_network::{ - AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, Network, - NetworkTransactionBuilder, ReceiptResponse, UnknownTxEnvelope, UnknownTypedTransaction, + AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, + BlockResponse, Network, NetworkTransactionBuilder, ReceiptResponse, UnknownTxEnvelope, + UnknownTypedTransaction, }; #[cfg(feature = "optimism")] use alloy_op_evm::{OpEvmContext, OpEvmFactory, OpTx}; @@ -5026,6 +5027,9 @@ where &self, replay: ForkTransactionReplay, ) -> Result<()> { + let source_chain_id = self.protocol_chain_id(); + let arbitrum_block_number = is_arbitrum(source_chain_id) + .then(|| arbitrum_replay_block_number(&replay.source_block)); let prepared = prepare_fork_transaction_replay(replay, self.is_monad())?; let fallback_execution_chain_id = self .get_fork() @@ -5043,15 +5047,14 @@ where let current_base_fee = self.base_fee(); let current_excess_blob_gas_and_price = self.excess_blob_gas_and_price(); let mut evm_env = self.evm_env.read().clone(); - let source_chain_id = self.protocol_chain_id(); if evm_env.block_env.basefee == 0 { evm_env.cfg_env.disable_base_fee = true; } let best_number = self.blockchain.storage.read().best_number; let block_number = best_number.saturating_add(1); - if is_arbitrum(source_chain_id) { - evm_env.block_env.number = U256::from(block_number); + if let Some(block_number) = arbitrum_block_number { + evm_env.block_env.number = block_number; } else { evm_env.block_env.number = evm_env.block_env.number.saturating_add(U256::from(1)); } @@ -9424,6 +9427,15 @@ fn unpack_execution_result( } } +fn arbitrum_replay_block_number(block: &AnyRpcBlock) -> U256 { + block + .other + .get("l1BlockNumber") + .cloned() + .and_then(|number| serde_json::from_value(number).ok()) + .unwrap_or_else(|| U256::from(block.header().number())) +} + /// Converts a halt reason into an [`InstructionResult`]. /// /// Abstracts over network-specific halt reason types (`HaltReason`, `OpHaltReason`) @@ -9432,12 +9444,16 @@ pub use foundry_evm::core::evm::IntoInstructionResult; #[cfg(test)] mod tests { - use super::{ForkCacheNamespace, ForkCacheSource, StagedForkCacheLease, StagedForkDbUser}; + use super::{ + ForkCacheNamespace, ForkCacheSource, StagedForkCacheLease, StagedForkDbUser, + arbitrum_replay_block_number, + }; use crate::{NodeConfig, spawn}; #[cfg(feature = "monad")] use alloy_consensus::{BlockHeader, constants::EMPTY_ROOT_HASH}; #[cfg(feature = "monad")] use alloy_network::TransactionBuilder; + use alloy_network::{AnyHeader, AnyRpcBlock, AnyRpcHeader}; #[cfg(feature = "monad")] use alloy_primitives::Address; use alloy_primitives::{B256, U256}; @@ -9445,6 +9461,7 @@ mod tests { use alloy_provider::Provider; #[cfg(feature = "monad")] use alloy_rpc_types::TransactionRequest; + use alloy_rpc_types::{Block, BlockTransactions}; use foundry_evm::{ backend::{BlockchainDb, BlockchainDbMeta}, hardfork::{EthereumHardfork, FoundryHardfork}, @@ -9463,6 +9480,21 @@ mod tests { db } + #[test] + fn arbitrum_transaction_replay_uses_l1_block_number() { + let header = AnyHeader { number: 75_219_831, ..Default::default() }; + let mut block = AnyRpcBlock::new( + Block::new( + AnyRpcHeader::from_sealed(header.seal(B256::ZERO)), + BlockTransactions::Full(Vec::new()), + ) + .into(), + ); + block.other.insert("l1BlockNumber".to_string(), serde_json::json!("0x10276d3")); + + assert_eq!(arbitrum_replay_block_number(&block), U256::from(16_938_707)); + } + fn test_endpoint_identity( hardfork: Option, instance_id: Option, diff --git a/crates/anvil/src/eth/backend/mem/monad.rs b/crates/anvil/src/eth/backend/mem/monad.rs index 4ce6affc36133..6be02831936e6 100644 --- a/crates/anvil/src/eth/backend/mem/monad.rs +++ b/crates/anvil/src/eth/backend/mem/monad.rs @@ -48,9 +48,8 @@ pub(super) struct PreparedExecution { /// Caches the fork blocks needed to construct the next Monad block's ancestor context. pub(super) async fn cache_fork_context(fork: &ClientFork) -> Result<(), BlockchainError> { - let block_number = fork.block_number(); let block = - fork.block_by_number_full(block_number).await?.ok_or(BlockchainError::BlockNotFound)?; + fork.block_by_hash_full(fork.block_hash()).await?.ok_or(BlockchainError::BlockNotFound)?; let parent_hash = block.header().parent_hash(); if !parent_hash.is_zero() { fork.block_by_hash_full(parent_hash).await?.ok_or(BlockchainError::BlockNotFound)?; diff --git a/crates/cheatcodes/src/evm/fork.rs b/crates/cheatcodes/src/evm/fork.rs index 0043169e13183..bf2b8e372a0d9 100644 --- a/crates/cheatcodes/src/evm/fork.rs +++ b/crates/cheatcodes/src/evm/fork.rs @@ -418,7 +418,7 @@ fn create_fork_request( && ccx.state.config.rpc_storage_caching.enable_for_endpoint(&url), url, evm_opts, - expected_context: None, + resolved: None, }; Ok(fork) } diff --git a/crates/chisel/src/args.rs b/crates/chisel/src/args.rs index b71041f22061b..1a3d8cd1900e6 100644 --- a/crates/chisel/src/args.rs +++ b/crates/chisel/src/args.rs @@ -139,7 +139,7 @@ async fn run_command_with_network( fork_chain_id_is_inferred, resolved_hardfork: None, source_chain_id: None, - backend: None, + cached_backend: None, calldata: None, ir_minimum: args.ir_minimum, })?; diff --git a/crates/chisel/src/dispatcher.rs b/crates/chisel/src/dispatcher.rs index e3e653a3d945c..6920d16192926 100644 --- a/crates/chisel/src/dispatcher.rs +++ b/crates/chisel/src/dispatcher.rs @@ -440,7 +440,7 @@ impl ChiselDispatcher { source.config.source_chain_id = None; // Clear the backend so that it is re-instantiated with the new fork // upon the next execution of the session source. - source.config.backend = None; + source.config.cached_backend = None; sh_println!("Set fork URL to {}", fork_url.yellow())?; @@ -468,7 +468,7 @@ impl ChiselDispatcher { source.config.foundry_config.chain = source.config.local_chain_id.map(Chain::from); source.config.resolved_hardfork = None; source.config.source_chain_id = None; - source.config.backend = None; + source.config.cached_backend = None; sh_println!("Now using local environment.") } diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index 201a736d4d06a..ef4c28a35a9d2 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -2,7 +2,10 @@ //! //! This module contains the execution logic for the [SessionSource]. -use crate::prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper}; +use crate::{ + prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper}, + source::CachedBackend, +}; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_json_abi::EventParam; use alloy_primitives::{Address, B256, U256, hex}; @@ -218,11 +221,38 @@ impl SessionSource { } async fn build_runner(&mut self, final_pc: usize) -> Result> { - let (mut evm_env, tx_env, fork_context) = self - .config - .evm_opts - .env_with_fork_context::, BlockEnvFor, TxEnvFor>() - .await?; + let (mut evm_env, tx_env, backend, resolved_fork) = match self.config.cached_backend.clone() + { + Some(CachedBackend { backend, resolved_fork }) => { + let (evm_env, tx_env) = self + .config + .evm_opts + .env_with_resolved_fork::, BlockEnvFor, TxEnvFor>( + resolved_fork.as_ref(), + ) + .await?; + (evm_env, tx_env, backend, resolved_fork) + } + None => { + let (evm_env, tx_env, resolved_fork) = self + .config + .evm_opts + .env_resolved::, BlockEnvFor, TxEnvFor>() + .await?; + let fork = self.config.evm_opts.get_fork_resolved( + &self.config.foundry_config, + evm_env.cfg_env.chain_id, + resolved_fork.as_ref(), + ); + let backend = Backend::spawn(fork)?; + self.config.cached_backend = Some(CachedBackend { + backend: backend.clone(), + resolved_fork: resolved_fork.clone(), + }); + (evm_env, tx_env, backend, resolved_fork) + } + }; + let fork_context = resolved_fork.as_ref().map(|fork| fork.context()); let fork_chain_id = fork_context.map(|context| context.source_chain_id); let fork_hardfork = fork_context.and_then(|context| context.hardfork); self.config.source_chain_id = fork_chain_id; @@ -235,18 +265,6 @@ impl SessionSource { None, ); - let backend = match self.config.backend.clone() { - Some(backend) => backend, - None => { - let fork = fork_context.and_then(|context| { - self.config.evm_opts.get_fork_with_context(&self.config.foundry_config, context) - }); - let backend = Backend::spawn(fork)?; - self.config.backend = Some(backend.clone()); - backend - } - }; - let executor = ExecutorBuilder::default() .inspectors(|stack| { stack diff --git a/crates/chisel/src/source.rs b/crates/chisel/src/source.rs index 82f07150a2fd1..aefbeee76727c 100644 --- a/crates/chisel/src/source.rs +++ b/crates/chisel/src/source.rs @@ -15,6 +15,7 @@ use foundry_config::{Config, FoundryHardfork, SolcReq}; use foundry_evm::{ backend::Backend, core::{bytecode::InstIter, evm::FoundryEvmNetwork}, + fork::ResolvedFork, opts::EvmOpts, }; use foundry_evm_networks::NetworkConfigs; @@ -38,6 +39,13 @@ pub const MIN_VM_VERSION: Version = Version::new(0, 6, 2); /// Solidity source for the `Vm` interface in [forge-std](https://github.com/foundry-rs/forge-std) static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol"); +/// In-memory backend and the exact fork identity from which it was constructed. +#[derive(Clone, Debug)] +pub(crate) struct CachedBackend { + pub(crate) backend: Backend, + pub(crate) resolved_fork: Option, +} + /// [`SessionSource`] build output. pub struct GeneratedOutput { output: ProjectCompileOutput, @@ -300,9 +308,9 @@ pub struct SessionSourceConfig { pub source_chain_id: Option, /// Disable the default `Vm` import. pub no_vm: bool, - /// In-memory REVM db for the session's runner. + /// Cached execution backend and its fork identity. #[serde(skip)] - pub backend: Option>, + pub(crate) cached_backend: Option>, /// Optionally enable traces for the REPL contract execution pub traces: bool, /// Optionally set calldata for the REPL contract execution diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index 020c7e1398b52..ffd6b3df7ee0b 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -7,13 +7,15 @@ use crate::{ BlockContext, BlockEnvFor, ChainFor, EthEvmNetwork, EvmEnvFor, FoundryContextFor, FoundryEvmFactory, FoundryEvmNetwork, HaltReasonFor, SpecFor, TxEnvFor, }, - fork::{CreateFork, ForkId, MultiFork}, + fork::{CreateFork, ForkId, ForkResult, MultiFork}, state_snapshot::StateSnapshots, utils::{ + apply_chain_and_block_specific_env_changes_for_chain, apply_chain_specific_tx_replay_env_changes_for_chain, get_blob_base_fee_update_fraction, }, }; use alloy_consensus::{BlockHeader, Typed2718}; +use alloy_eips::BlockNumHash; use alloy_evm::{Evm, EvmEnv, EvmFactory}; use alloy_genesis::GenesisAccount; use alloy_network::{ @@ -24,14 +26,16 @@ use alloy_rpc_types::{BlockNumberOrTag, BlockTransactions}; use eyre::Context; use foundry_common::{SYSTEM_TRANSACTION_TYPE, is_known_system_sender}; use foundry_evm_networks::NetworkConfigs; -pub use foundry_fork_db::{BlockchainDb, ForkBlockEnv, SharedBackend, cache::BlockchainDbMeta}; +pub use foundry_fork_db::{ + BlockchainDb, ForkBlock, ForkBlockEnv, SharedBackend, cache::BlockchainDbMeta, +}; use revm::{ Database, DatabaseCommit, JournalEntry, bytecode::Bytecode, context::{Block, BlockEnv, CfgEnv, ContextTr, JournalInner, Transaction}, context_interface::{journaled_state::account::JournaledAccountTr, result::ResultAndState}, database::{AccountState, CacheDB, DatabaseRef, EmptyDB}, - primitives::{AddressMap, HashMap as Map, KECCAK_EMPTY, Log}, + primitives::{AddressMap, HashMap as Map, KECCAK_EMPTY, Log, hardfork::SpecId}, state::{Account, AccountInfo, EvmState, EvmStorageSlot, TransactionId}, }; use std::{ @@ -93,6 +97,7 @@ struct TransactionInputs { evm_env: EvmEnvFor, tx_env: TxEnvFor, chain_context: ChainFor, + rpc_block_number: u64, } /// Environment and network configuration used while replaying transactions. @@ -103,9 +108,10 @@ struct ReplayInputs { /// Block data required to execute or position a fork at a transaction. struct TransactionForkTarget { - fork_block_number: u64, + fork_block: BlockNumHash, transaction: AnyRpcTransaction, block: AnyRpcBlock, + mined: bool, } /// A fork roll prepared for atomic publication. @@ -121,9 +127,9 @@ struct StagedForkRoll { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ForkPosition { /// The database contains all transactions through the fork's current block. - AfterBlock { block_number: u64 }, + AfterBlock { block: BlockNumHash }, /// The database contains the transactions before `transaction_index` in `block_number`. - BeforeTransaction { block_number: u64, transaction_index: usize }, + BeforeTransaction { block: BlockNumHash, transaction_index: usize }, } impl ForkPosition { @@ -131,18 +137,20 @@ impl ForkPosition { /// follows this position. fn after_transaction( self, - block_number: u64, + block: BlockNumHash, + parent_hash: B256, transaction_index: usize, transaction_count: usize, ) -> Option { let is_next = match self { - Self::AfterBlock { block_number: previous_block } => { - transaction_index == 0 && previous_block.checked_add(1) == Some(block_number) + Self::AfterBlock { block: previous } => { + transaction_index == 0 + && previous.number.checked_add(1) == Some(block.number) + && previous.hash == parent_hash + } + Self::BeforeTransaction { block: current, transaction_index: current_index } => { + current == block && current_index == transaction_index } - Self::BeforeTransaction { - block_number: current_block, - transaction_index: current_index, - } => current_block == block_number && current_index == transaction_index, }; if !is_next { return None; @@ -153,9 +161,9 @@ impl ForkPosition { } Some(if next_index == transaction_count { - Self::AfterBlock { block_number } + Self::AfterBlock { block } } else { - Self::BeforeTransaction { block_number, transaction_index: next_index } + Self::BeforeTransaction { block, transaction_index: next_index } }) } } @@ -583,6 +591,8 @@ pub struct Backend { /// /// If this is set, then the Backend is currently in forking mode active_fork_ids: Option<(LocalForkId, ForkLookupIndex)>, + /// RPC block number exposed while executing a historical transaction in a temporary backend. + fork_block_number_override: Option, /// holds additional Backend data inner: BackendInner, } @@ -595,6 +605,7 @@ impl Clone for Backend { mem_db: self.mem_db.clone(), fork_init_journaled_state: self.fork_init_journaled_state.clone(), active_fork_ids: self.active_fork_ids, + fork_block_number_override: self.fork_block_number_override, inner: self.inner.clone(), } } @@ -643,15 +654,19 @@ impl Backend { mem_db: CacheDB::new(Default::default()), fork_init_journaled_state: inner.new_journaled_state(), active_fork_ids: None, + fork_block_number_override: None, inner, }; if let Some(fork) = fork { - let (fork_id, fork, _, context) = backend.forks.create_fork(fork)?; + let ForkResult { id: fork_id, backend: fork, resolved, .. } = + backend.forks.create_fork(fork)?; + let context = resolved.context(); + let block = resolved.block(); let fork_db = ForkDB::new(fork); let fork_ids = backend.inner.insert_new_fork( fork_id.clone(), - context.block_number, + block, context.source_chain_id, fork_db, backend.inner.new_journaled_state(), @@ -690,6 +705,7 @@ impl Backend { mem_db: CacheDB::new(Default::default()), fork_init_journaled_state: self.inner.new_journaled_state(), active_fork_ids: None, + fork_block_number_override: None, inner: Default::default(), } } @@ -1050,12 +1066,28 @@ impl Backend { // get the block number we need to fork if let Some(tx_block) = tx.block_number() { - let block = fork.backend().get_full_block(tx_block)?; + let tx_block_hash = tx + .block_hash() + .ok_or_else(|| eyre::eyre!("mined transaction is missing its block hash"))?; + let block = fork.backend().get_full_block(tx_block_hash)?; + eyre::ensure!( + block.header().number() == tx_block && block.header().hash == tx_block_hash, + "transaction block changed: expected {} ({}), got {} ({})", + tx_block, + tx_block_hash, + block.header().number(), + block.header().hash + ); // we need to subtract 1 here because we want the state before the transaction // was mined - let fork_block = tx_block - 1; - Ok(TransactionForkTarget { fork_block_number: fork_block, transaction: tx, block }) + let fork_block = BlockNumHash::new( + tx_block.checked_sub(1).ok_or_else(|| { + eyre::eyre!("cannot replay a transaction in the genesis block") + })?, + block.header().parent_hash(), + ); + Ok(TransactionForkTarget { fork_block, transaction: tx, block, mined: true }) } else { if self.networks.is_monad() { eyre::bail!( @@ -1064,9 +1096,9 @@ impl Backend { } let block = fork.backend().get_full_block(BlockNumberOrTag::Latest)?; - let number = block.header().number(); + let fork_block = BlockNumHash::new(block.header().number(), block.header().hash); - Ok(TransactionForkTarget { fork_block_number: number, transaction: tx, block }) + Ok(TransactionForkTarget { fork_block, transaction: tx, block, mined: false }) } } @@ -1103,11 +1135,18 @@ impl Backend { let parent_block = if parent_hash.is_zero() { None } else { - Some( - backend - .get_full_block(parent_hash) - .wrap_err_with(|| format!("failed to fetch parent block {parent_hash}"))?, - ) + let parent_number = block.header().number().checked_sub(1).ok_or_else(|| { + eyre::eyre!("genesis block has non-zero parent hash {parent_hash}") + })?; + let parent = backend + .get_full_block(parent_hash) + .wrap_err_with(|| format!("failed to fetch parent block {parent_hash}"))?; + ensure_block_identity( + &parent, + BlockNumHash::new(parent_number, parent_hash), + "parent", + )?; + Some(parent) }; let parent = parent_block.as_ref().map(Self::full_block_tx_envs).transpose()?.unwrap_or_default(); @@ -1120,6 +1159,15 @@ impl Backend { let block = backend.get_full_block(grandparent_hash).wrap_err_with(|| { format!("failed to fetch grandparent block {grandparent_hash}") })?; + let grandparent_number = + parent_block.header().number().checked_sub(1).ok_or_else(|| { + eyre::eyre!("genesis block has non-zero parent hash {grandparent_hash}") + })?; + ensure_block_identity( + &block, + BlockNumHash::new(grandparent_number, grandparent_hash), + "grandparent", + )?; Self::full_block_tx_envs(&block)? } } else { @@ -1167,16 +1215,17 @@ impl Backend { } let fork = self.inner.get_fork_by_id(id)?; - let (block_number, position) = match fork.position { - position @ ForkPosition::AfterBlock { block_number } => (block_number, position), - position @ ForkPosition::BeforeTransaction { block_number, .. } => { - (block_number, position) - } + let (position_block, position) = match fork.position { + position @ (ForkPosition::AfterBlock { block } + | ForkPosition::BeforeTransaction { block, .. }) => (block, position), }; - let block = fork - .backend() - .get_full_block(block_number) - .wrap_err_with(|| format!("failed to fetch fork block {block_number}"))?; + let block = fork.backend().get_full_block(position_block.hash).wrap_err_with(|| { + format!( + "failed to fetch fork block {} ({})", + position_block.number, position_block.hash + ) + })?; + ensure_block_identity(&block, position_block, "fork")?; let context = Self::block_context_inputs_from_backend(fork.backend(), &block)?; Self::context_for_block_position(context, position, tx) } @@ -1193,16 +1242,19 @@ impl Backend { }; let fork = self.inner.get_fork_by_id(id)?; - let (block_number, transaction_index) = match fork.position { - ForkPosition::AfterBlock { block_number } => (block_number, None), - ForkPosition::BeforeTransaction { block_number, transaction_index } => { - (block_number, Some(transaction_index)) + let (position_block, transaction_index) = match fork.position { + ForkPosition::AfterBlock { block } => (block, None), + ForkPosition::BeforeTransaction { block, transaction_index } => { + (block, Some(transaction_index)) } }; - let block = fork - .backend() - .get_full_block(block_number) - .wrap_err_with(|| format!("failed to fetch active fork block {block_number}"))?; + let block = fork.backend().get_full_block(position_block.hash).wrap_err_with(|| { + format!( + "failed to fetch active fork block {} ({})", + position_block.number, position_block.hash + ) + })?; + ensure_block_identity(&block, position_block, "active fork")?; let context = Self::block_context_inputs_from_backend(fork.backend(), &block)?; match transaction_index { @@ -1283,22 +1335,49 @@ impl Backend { id: Option, block_number: u64, evm_env: &mut EvmEnvFor, - _tx_env: Option<&TxEnvFor>, + tx_env: Option<&TxEnvFor>, journaled_state: &mut JournaledState, ) -> eyre::Result> { trace!(?id, ?block_number, "roll fork"); let id = self.ensure_fork(id)?; + let rolled = self.forks.roll_fork(self.inner.ensure_fork_id(id).cloned()?, block_number)?; + self.apply_rolled_fork_with_context(id, rolled, evm_env, tx_env, journaled_state) + } + + fn roll_fork_exact_with_context( + &mut self, + id: LocalForkId, + block: BlockNumHash, + evm_env: &mut EvmEnvFor, + tx_env: Option<&TxEnvFor>, + journaled_state: &mut JournaledState, + ) -> eyre::Result> { + trace!(?id, ?block, "roll fork to exact block"); + let rolled = self.forks.roll_fork_exact(self.inner.ensure_fork_id(id).cloned()?, block)?; + self.apply_rolled_fork_with_context(id, rolled, evm_env, tx_env, journaled_state) + } + + fn apply_rolled_fork_with_context( + &mut self, + id: LocalForkId, + rolled: ForkResult, BlockEnvFor>, + evm_env: &mut EvmEnvFor, + _tx_env: Option<&TxEnvFor>, + journaled_state: &mut JournaledState, + ) -> eyre::Result> { + let ForkResult { id: fork_id, backend, env: fork_env, resolved } = rolled; + let context = resolved.context(); + let block = resolved.block(); let _affects_active = self.is_active_fork(id); - let (fork_id, backend, fork_env, context) = - self.forks.roll_fork(self.inner.ensure_fork_id(id).cloned()?, block_number)?; #[cfg(feature = "monad")] let context_update = if _affects_active && let Some(tx) = _tx_env { let chain_context = if self.networks.is_monad() { - let block = backend.get_full_block(context.block_number).wrap_err_with(|| { - format!("failed to fetch rolled fork block {}", context.block_number) + let block_data = backend.get_full_block(block.hash).wrap_err_with(|| { + format!("failed to fetch rolled fork block {} ({})", block.number, block.hash) })?; - let block_context = Self::block_context_inputs_from_backend(&backend, &block)?; + ensure_block_identity(&block_data, block, "rolled fork")?; + let block_context = Self::block_context_inputs_from_backend(&backend, &block_data)?; block_context.into_child().next_transaction(tx) } else { FEN::EvmFactory::default().chain_context_for_transaction(tx) @@ -1311,13 +1390,7 @@ impl Backend { let context_update = std::marker::PhantomData; // Update the local mapping only after all context fetches and decoding have succeeded. - self.inner.roll_fork( - id, - fork_id, - context.block_number, - context.source_chain_id, - backend, - )?; + self.inner.roll_fork(id, fork_id, block, context.source_chain_id, backend)?; if let Some((active_id, active_idx)) = self.active_fork_ids && active_id == id @@ -1369,7 +1442,7 @@ impl Backend { trace!(?id, ?transaction, "roll fork to transaction"); let id = self.ensure_fork(id)?; let affects_active = self.is_active_fork(id); - let TransactionForkTarget { fork_block_number, block, .. } = + let TransactionForkTarget { fork_block, block, .. } = self.get_block_number_and_block_for_transaction(id, transaction)?; let BlockTransactions::Full(transactions) = block.transactions() else { eyre::bail!("block {} does not contain full transactions", block.header().number()); @@ -1387,7 +1460,7 @@ impl Backend { let block_context = self.block_context_inputs(id, &block)?; let context_update = if affects_active && let Some(tx) = tx_env { let position = ForkPosition::BeforeTransaction { - block_number: block.header().number(), + block: BlockNumHash::new(block.header().number(), block.header().hash), transaction_index, }; ContextUpdate::Replace(Self::context_for_block_position( @@ -1402,8 +1475,8 @@ impl Backend { }; let current_fork_id = self.inner.ensure_fork_id(id).cloned()?; - let (fork_id, backend, fork_env, context) = - self.forks.roll_fork(current_fork_id, fork_block_number)?; + let ForkResult { id: fork_id, backend, env: fork_env, resolved } = + self.forks.roll_fork_exact(current_fork_id, fork_block)?; let staged_fork_journaled_state = if affects_active { self.fork_init_journaled_state.clone() } else { @@ -1412,8 +1485,8 @@ impl Backend { let mut staged_fork = self.inner.stage_fork_roll( id, fork_id, - context.block_number, - context.source_chain_id, + fork_block, + resolved.context().source_chain_id, backend, staged_fork_journaled_state, )?; @@ -1432,7 +1505,12 @@ impl Backend { ); } - update_env_block(&mut staged_evm_env, block.header()); + update_env_block::( + &mut staged_evm_env, + &block, + staged_fork.fork.source_chain_id, + self.networks, + ); let mut replay_env = staged_evm_env.clone(); self.apply_fork_tx_replay_env_changes_for( &staged_fork.fork_id, @@ -1454,7 +1532,7 @@ impl Backend { block.header().number() ); staged_fork.fork.position = ForkPosition::BeforeTransaction { - block_number: block.header().number(), + block: BlockNumHash::new(block.header().number(), block.header().hash), transaction_index, }; @@ -1481,7 +1559,7 @@ impl Backend { let id = self.ensure_fork(id)?; let _affects_active = self.is_active_fork(id); - let TransactionForkTarget { fork_block_number, block, .. } = + let TransactionForkTarget { fork_block, block, mined, .. } = self.get_block_number_and_block_for_transaction(id, transaction)?; let transaction_index = match block.transactions() { BlockTransactions::Full(transactions) => { @@ -1505,7 +1583,7 @@ impl Backend { let context_update = if _affects_active && let Some(tx) = _tx_env { let chain_context = if let Some(context) = &block_context { let position = ForkPosition::BeforeTransaction { - block_number: block.header().number(), + block: BlockNumHash::new(block.header().number(), block.header().hash), transaction_index: transaction_index.expect("checked above"), }; Self::context_for_block_position(context.clone(), position, tx)? @@ -1522,14 +1600,15 @@ impl Backend { let context_update = std::marker::PhantomData; // The parent roll must not prepare an intermediate synthetic context. - self.roll_fork_with_context(Some(id), fork_block_number, evm_env, None, journaled_state)?; + self.roll_fork_exact_with_context(id, fork_block, evm_env, None, journaled_state)?; - update_env_block(evm_env, block.header()); + let source_chain_id = self.inner.get_fork_by_id(id)?.source_chain_id; + update_env_block::(evm_env, &block, source_chain_id, self.networks); let mut replay_env = evm_env.clone(); self.apply_fork_tx_replay_env_changes(id, &mut replay_env)?; let persistent_accounts = self.inner.persistent_accounts.clone(); - let target = { + let target = if mined { let fork = self.inner.get_fork_by_id_mut(id)?; Self::replay_until( fork, @@ -1540,12 +1619,14 @@ impl Backend { journaled_state, &persistent_accounts, )? + } else { + None }; if target.is_some() && let Some(transaction_index) = transaction_index { self.inner.get_fork_by_id_mut(id)?.position = ForkPosition::BeforeTransaction { - block_number: block.header().number(), + block: BlockNumHash::new(block.header().number(), block.header().hash), transaction_index, }; } @@ -1589,7 +1670,12 @@ impl Backend { if networks.is_monad() { eyre::ensure!( fork.position - .after_transaction(full_block.header().number(), 0, transactions.len(),) + .after_transaction( + BlockNumHash::new(full_block.header().number(), full_block.header().hash), + full_block.header().parent_hash(), + 0, + transactions.len(), + ) .is_some(), "block {} does not immediately follow the active fork position", full_block.header().number() @@ -1681,6 +1767,22 @@ impl Backend { } } +fn ensure_block_identity( + block: &AnyRpcBlock, + expected: BlockNumHash, + relation: &str, +) -> eyre::Result<()> { + eyre::ensure!( + block.header().number() == expected.number && block.header().hash == expected.hash, + "{relation} block changed: expected {} ({}), got {} ({})", + expected.number, + expected.hash, + block.header().number(), + block.header().hash + ); + Ok(()) +} + impl DatabaseExt for Backend { fn chain_context_for_synthetic_transaction( &self, @@ -1782,11 +1884,14 @@ impl DatabaseExt for Backend { fn create_fork(&mut self, create_fork: CreateFork) -> eyre::Result { trace!("create fork"); - let (fork_id, fork, _, context) = self.forks.create_fork(create_fork)?; + let ForkResult { id: fork_id, backend: fork, resolved, .. } = + self.forks.create_fork(create_fork)?; + let context = resolved.context(); + let block = resolved.block(); let fork_db = ForkDB::new(fork); let (id, _) = self.inner.insert_new_fork( fork_id, - context.block_number, + block, context.source_chain_id, fork_db, self.fork_init_journaled_state.clone(), @@ -2021,7 +2126,8 @@ impl DatabaseExt for Backend { let TransactionForkTarget { transaction: tx, block, .. } = self.get_block_number_and_block_for_transaction(id, transaction)?; let tx_env = TxEnvFor::::from_any_rpc_transaction(&tx)?; - update_env_block(&mut evm_env, block.header()); + let source_chain_id = self.inner.get_fork_by_id(id)?.source_chain_id; + update_env_block::(&mut evm_env, &block, source_chain_id, self.networks); self.apply_fork_tx_replay_env_changes(id, &mut evm_env)?; let current_tx_position = if self.networks.is_monad() { @@ -2057,7 +2163,12 @@ impl DatabaseExt for Backend { self.inner .get_fork_by_id(id)? .position - .after_transaction(block.header().number(), current_tx_index, transaction_count) + .after_transaction( + BlockNumHash::new(block.header().number(), block.header().hash), + block.header().parent_hash(), + current_tx_index, + transaction_count, + ) .ok_or_else(|| { eyre::eyre!( "transaction {transaction} does not immediately follow the active \ @@ -2087,7 +2198,12 @@ impl DatabaseExt for Backend { let fork = self.inner.get_fork_by_id_mut(id)?; commit_transaction::( - TransactionInputs { evm_env, tx_env, chain_context }, + TransactionInputs { + evm_env, + tx_env, + chain_context, + rpc_block_number: block.header().number(), + }, journaled_state, fork, &fork_id, @@ -2141,24 +2257,15 @@ impl DatabaseExt for Backend { } fn active_fork_block_number(&self) -> Option { - let fork = self.inner.issued_local_fork_ids.get(&self.active_fork_id()?)?; - let fork_block = fork_block_number(fork); - let env_block = self - .forks - .get_evm_env(fork.clone()) - .ok() - .flatten() - .map(|env| env.block_env.number().saturating_to::()); - - // On Arbitrum, `fork_block` is the L2 fork pin while `env_block` can be remapped to the - // lower L1 block number. For tx-level forks, the fork pin is the parent state block while - // the env is updated to the transaction's block. The larger value is the current L2 block. - match (fork_block, env_block) { - (Some(fork_block), Some(env_block)) => Some(fork_block.max(env_block)), - (Some(fork_block), None) => Some(fork_block), - (None, Some(env_block)) => Some(env_block), - (None, None) => None, + if let Some(block_number) = self.fork_block_number_override { + return Some(block_number); } + let fork = self.inner.get_fork_by_id(self.active_fork_id()?).ok()?; + Some(match fork.position { + ForkPosition::AfterBlock { block } | ForkPosition::BeforeTransaction { block, .. } => { + block.number + } + }) } fn ensure_fork(&self, id: Option) -> eyre::Result { @@ -2665,7 +2772,7 @@ impl BackendInner { &mut self, id: LocalForkId, fork_id: ForkId, - block_number: u64, + block: BlockNumHash, source_chain_id: ChainId, db: ForkDB>, journaled_state: JournaledState, @@ -2678,7 +2785,7 @@ impl BackendInner { db, journaled_state, source_chain_id, - position: ForkPosition::AfterBlock { block_number }, + position: ForkPosition::AfterBlock { block }, }; self.forks.push(Some(fork)); idx @@ -2688,7 +2795,7 @@ impl BackendInner { &mut self, id: LocalForkId, new_fork_id: ForkId, - block_number: u64, + block: BlockNumHash, source_chain_id: ChainId, backend: SharedBackend>, ) -> eyre::Result { @@ -2703,7 +2810,7 @@ impl BackendInner { } active.db = new_db; active.source_chain_id = source_chain_id; - active.position = ForkPosition::AfterBlock { block_number }; + active.position = ForkPosition::AfterBlock { block }; } self.issued_local_fork_ids.insert(id, new_fork_id.clone()); self.created_forks.insert(new_fork_id, idx); @@ -2716,7 +2823,7 @@ impl BackendInner { &self, id: LocalForkId, new_fork_id: ForkId, - block_number: u64, + block: BlockNumHash, source_chain_id: ChainId, backend: SharedBackend>, journaled_state: JournaledState, @@ -2740,7 +2847,7 @@ impl BackendInner { db: new_db, journaled_state, source_chain_id, - position: ForkPosition::AfterBlock { block_number }, + position: ForkPosition::AfterBlock { block }, }, }) } @@ -2761,7 +2868,7 @@ impl BackendInner { pub fn insert_new_fork( &mut self, fork_id: ForkId, - block_number: u64, + block: BlockNumHash, source_chain_id: ChainId, db: ForkDB>, journaled_state: JournaledState, @@ -2772,7 +2879,7 @@ impl BackendInner { db, journaled_state, source_chain_id, - position: ForkPosition::AfterBlock { block_number }, + position: ForkPosition::AfterBlock { block }, }, ) } @@ -2927,10 +3034,13 @@ fn is_contract_in_state(evm_state: &EvmState, acc: Address) -> bool { } /// Updates the evm env's block with the block's data -fn update_env_block( +fn update_env_block + Copy, BLOCK: FoundryBlock>( evm_env: &mut EvmEnv, - header: &impl BlockHeader, + block: &N::BlockResponse, + source_chain_id: ChainId, + networks: NetworkConfigs, ) { + let header = block.header(); let block_env = &mut evm_env.block_env; block_env.set_timestamp(U256::from(header.timestamp())); block_env.set_beneficiary(header.beneficiary()); @@ -2946,6 +3056,13 @@ fn update_env_block( get_blob_base_fee_update_fraction(evm_env.cfg_env.chain_id, header.timestamp()), ); } + + apply_chain_and_block_specific_env_changes_for_chain::( + evm_env, + block, + source_chain_id, + networks, + ); } /// Executes the given transaction and commits state changes to the database _and_ the journaled @@ -2961,7 +3078,7 @@ fn commit_transaction( ::FoundryContext<'db>, >, ) -> eyre::Result<()> { - let TransactionInputs { evm_env, tx_env, chain_context } = transaction; + let TransactionInputs { evm_env, tx_env, chain_context, rpc_block_number } = transaction; let now = Instant::now(); let res = { let fork = fork.clone(); @@ -2969,6 +3086,7 @@ fn commit_transaction( let depth = journaled_state.depth; let mut db: Backend = Backend::new_with_fork(fork_id, fork, journaled_state, networks)?; + db.fork_block_number_override = Some(rpc_block_number); let mut evm = FEN::EvmFactory::default().create_foundry_nested_evm( &mut db, @@ -3036,34 +3154,28 @@ fn apply_state_changeset( Ok(()) } -fn fork_block_number(fork: &ForkId) -> Option { - let (_, block) = fork.as_str().rsplit_once('@')?; - let block = block.split_once('-').map_or(block, |(block, _)| block); - let block = block.strip_prefix("0x")?; - u64::from_str_radix(block, 16).ok() -} - #[cfg(test)] mod tests { - use super::{Fork, apply_state_changeset}; + use super::{Fork, apply_state_changeset, ensure_block_identity, update_env_block}; #[cfg(feature = "monad")] use crate::evm::MonadEvmNetwork; #[cfg(feature = "monad")] use crate::fork::CreateFork; use crate::{ - backend::{Backend, ForkPosition}, + backend::{Backend, DatabaseExt, ForkPosition}, evm::EthEvmNetwork, - fork::ForkId, opts::EvmOpts, }; use alloy_consensus::transaction::Recovered; + use alloy_eips::BlockNumHash; + use alloy_evm::EvmEnv; use alloy_network::{ - AnyNetwork, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, UnknownTxEnvelope, - UnknownTypedTransaction, + AnyHeader, AnyNetwork, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, + AnyTxType, UnknownTxEnvelope, UnknownTypedTransaction, }; use alloy_primitives::{Address, B256, U256, address}; use alloy_provider::{Provider, ProviderBuilder, mock::Asserter}; - use alloy_rpc_types::Transaction as RpcTransaction; + use alloy_rpc_types::{Block, BlockTransactions, Transaction as RpcTransaction}; use alloy_serde::WithOtherFields; use anvil::{NodeConfig, spawn}; use foundry_common::{SYSTEM_TRANSACTION_TYPE, provider::get_http_provider}; @@ -3094,10 +3206,37 @@ mod tests { db: CacheDB::new(backend), journaled_state: JournalInner::new(), source_chain_id: 1, - position: ForkPosition::AfterBlock { block_number: 0 }, + position: ForkPosition::AfterBlock { block: BlockNumHash::default() }, } } + fn rpc_block(number: u64, hash: B256, parent_hash: B256) -> AnyRpcBlock { + let header = AnyHeader { number, parent_hash, ..Default::default() }; + AnyRpcBlock::new( + Block::new( + AnyRpcHeader::from_sealed(header.seal(hash)), + BlockTransactions::Full(Vec::new()), + ) + .into(), + ) + } + + #[test] + fn validates_block_identity() { + let hash = B256::with_last_byte(2); + let block = rpc_block(2, hash, B256::with_last_byte(1)); + assert!(ensure_block_identity(&block, BlockNumHash::new(2, hash), "parent").is_ok()); + + let err = + ensure_block_identity(&block, BlockNumHash::new(2, B256::with_last_byte(3)), "parent") + .unwrap_err(); + assert!(err.to_string().contains("parent block changed")); + + let err = + ensure_block_identity(&block, BlockNumHash::new(1, hash), "grandparent").unwrap_err(); + assert!(err.to_string().contains("grandparent block changed")); + } + #[test] fn failed_fork_state_refresh_does_not_publish_transaction_changes() { let mut fork = fork_with_closed_backend(); @@ -3198,41 +3337,64 @@ mod tests { #[test] fn fork_position_advances_from_exact_transaction_predecessor() { - let parent = ForkPosition::AfterBlock { block_number: 10 }; + let parent_block = BlockNumHash::new(10, B256::with_last_byte(10)); + let block = BlockNumHash::new(11, B256::with_last_byte(11)); + let parent = ForkPosition::AfterBlock { block: parent_block }; assert_eq!( - parent.after_transaction(11, 0, 2), - Some(ForkPosition::BeforeTransaction { block_number: 11, transaction_index: 1 }) + parent.after_transaction(block, parent_block.hash, 0, 2), + Some(ForkPosition::BeforeTransaction { block, transaction_index: 1 }) ); assert_eq!( - parent.after_transaction(11, 0, 1), - Some(ForkPosition::AfterBlock { block_number: 11 }) + parent.after_transaction(block, parent_block.hash, 0, 1), + Some(ForkPosition::AfterBlock { block }) ); - let before_first = - ForkPosition::BeforeTransaction { block_number: 11, transaction_index: 0 }; + let before_first = ForkPosition::BeforeTransaction { block, transaction_index: 0 }; assert_eq!( - before_first.after_transaction(11, 0, 2), - Some(ForkPosition::BeforeTransaction { block_number: 11, transaction_index: 1 }) + before_first.after_transaction(block, parent_block.hash, 0, 2), + Some(ForkPosition::BeforeTransaction { block, transaction_index: 1 }) ); - let before_second = - ForkPosition::BeforeTransaction { block_number: 11, transaction_index: 1 }; + let before_second = ForkPosition::BeforeTransaction { block, transaction_index: 1 }; assert_eq!( - before_second.after_transaction(11, 1, 3), - Some(ForkPosition::BeforeTransaction { block_number: 11, transaction_index: 2 }) + before_second.after_transaction(block, parent_block.hash, 1, 3), + Some(ForkPosition::BeforeTransaction { block, transaction_index: 2 }) ); assert_eq!( - before_second.after_transaction(11, 1, 2), - Some(ForkPosition::AfterBlock { block_number: 11 }) + before_second.after_transaction(block, parent_block.hash, 1, 2), + Some(ForkPosition::AfterBlock { block }) ); - assert_eq!(parent.after_transaction(11, 1, 2), None); - assert_eq!(parent.after_transaction(12, 0, 1), None); - assert_eq!(before_second.after_transaction(11, 0, 3), None); - assert_eq!(before_second.after_transaction(11, 2, 3), None); - assert_eq!(before_second.after_transaction(12, 1, 3), None); - assert_eq!(before_second.after_transaction(11, 1, 1), None); - assert_eq!(parent.after_transaction(11, 0, 0), None); + assert_eq!(parent.after_transaction(block, B256::ZERO, 0, 1), None); + assert_eq!(parent.after_transaction(block, parent_block.hash, 1, 2), None); + assert_eq!(before_second.after_transaction(block, parent_block.hash, 0, 3), None); + assert_eq!(before_second.after_transaction(block, parent_block.hash, 2, 3), None); + assert_eq!(before_second.after_transaction(block, parent_block.hash, 1, 1), None); + assert_eq!(parent.after_transaction(block, parent_block.hash, 0, 0), None); + } + + #[test] + fn fork_replay_block_env_preserves_arbitrum_l1_number() { + let header = AnyHeader { number: 75_219_831, ..Default::default() }; + let mut block = AnyRpcBlock::new( + Block::new( + AnyRpcHeader::from_sealed(header.seal(B256::ZERO)), + BlockTransactions::Full(Vec::new()), + ) + .into(), + ); + block.other.insert("l1BlockNumber".to_string(), serde_json::json!("0x10276d3")); + let mut evm_env = + EvmEnv::new(revm::context::CfgEnv::::default(), BlockEnv::default()); + + update_env_block::( + &mut evm_env, + &block, + NamedChain::Arbitrum as u64, + NetworkConfigs::default(), + ); + + assert_eq!(evm_env.block_env.number, U256::from(16_938_707)); } #[tokio::test(flavor = "multi_thread")] @@ -3251,15 +3413,17 @@ mod tests { for position in [ ForkPosition::BeforeTransaction { - block_number: block_number + 1, + block: BlockNumHash::new(block_number + 1, B256::with_last_byte(1)), transaction_index: 2, }, - ForkPosition::AfterBlock { block_number: block_number + 2 }, + ForkPosition::AfterBlock { + block: BlockNumHash::new(block_number + 2, B256::with_last_byte(2)), + }, ] { backend.inner.get_fork_by_id_mut(id).unwrap().position = position; let fork = backend.active_fork().unwrap().clone(); let journaled_state = fork.journaled_state.clone(); - let temporary = Backend::::new_with_fork( + let mut temporary = Backend::::new_with_fork( &fork_id, fork, journaled_state, @@ -3268,6 +3432,13 @@ mod tests { .unwrap(); assert_eq!(temporary.active_fork().unwrap().position, position); + let expected = match position { + ForkPosition::AfterBlock { block } + | ForkPosition::BeforeTransaction { block, .. } => block.number, + }; + assert_eq!(temporary.active_fork_block_number(), Some(expected)); + temporary.fork_block_number_override = Some(expected + 1); + assert_eq!(temporary.active_fork_block_number(), Some(expected + 1)); } } @@ -3288,7 +3459,7 @@ mod tests { } fn target_fork(opts: EvmOpts, url: String) -> CreateFork { - CreateFork { url, enable_caching: false, evm_opts: opts, expected_context: None } + CreateFork { url, enable_caching: false, evm_opts: opts, resolved: None } } let (ethereum_base_api, ethereum_base) = spawn(NodeConfig::test()).await; @@ -3356,11 +3527,16 @@ mod tests { evm_opts.fork_url = Some(endpoint.to_string()); evm_opts.fork_block_number = Some(block_num); - let (evm_env, _, fork_block) = evm_opts.env::().await.unwrap(); + let (evm_env, _, resolved) = + evm_opts.env_resolved::().await.unwrap(); - let fork = - evm_opts.get_fork(&Config::default(), evm_env.cfg_env.chain_id, fork_block).unwrap(); + let fork = evm_opts + .get_fork_resolved(&Config::default(), evm_env.cfg_env.chain_id, resolved.as_ref()) + .unwrap(); + let resolved = resolved.unwrap(); + let fork_hash = resolved.hash(); + let source_id = resolved.source_id(); let backend = Backend::::spawn(Some(fork)).unwrap(); // some rng contract from etherscan @@ -3373,11 +3549,8 @@ mod tests { } drop(backend); - let meta = BlockchainDbMeta { - chain: None, - block_env: evm_env.block_env, - hosts: Default::default(), - }; + let meta = BlockchainDbMeta::new(evm_env.block_env, endpoint.to_string()) + .with_fork_identity(fork_hash, source_id); let db = BlockchainDb::new( meta, @@ -3387,15 +3560,4 @@ mod tests { assert!(db.storage().read().contains_key(&address)); assert_eq!(db.storage().read().get(&address).unwrap().len(), num_slots as usize); } - - #[test] - fn parses_fork_block_number_from_fork_id() { - let fork = ForkId::new("https://example.com/@rpc", Some(75_219_831)); - assert_eq!(super::fork_block_number(&fork), Some(75_219_831)); - assert_eq!( - super::fork_block_number(&format!("{}-1", fork.as_str()).into()), - Some(75_219_831) - ); - assert_eq!(super::fork_block_number(&ForkId::new("https://example.com", None)), None); - } } diff --git a/crates/evm/core/src/fork/mod.rs b/crates/evm/core/src/fork/mod.rs index f6f4b07e6979d..3d5f71bf6fe8f 100644 --- a/crates/evm/core/src/fork/mod.rs +++ b/crates/evm/core/src/fork/mod.rs @@ -1,9 +1,9 @@ -use super::opts::{EvmOpts, ForkContext}; +use super::opts::EvmOpts; pub mod database; mod multi; -pub use multi::{ForkId, MultiFork, MultiForkHandler}; +pub use multi::{ForkId, ForkResult, MultiFork, MultiForkHandler}; mod resolved; pub use resolved::ResolvedFork; @@ -17,6 +17,6 @@ pub struct CreateFork { pub url: String, /// All env settings as configured by the user pub evm_opts: EvmOpts, - /// Context already resolved by the runner that requested this backend. - pub expected_context: Option, + /// Exact source and block identity resolved before fork construction. + pub resolved: Option, } diff --git a/crates/evm/core/src/fork/multi.rs b/crates/evm/core/src/fork/multi.rs index c461e5e321010..582a8f415f36c 100644 --- a/crates/evm/core/src/fork/multi.rs +++ b/crates/evm/core/src/fork/multi.rs @@ -3,14 +3,15 @@ //! The design is similar to the single `SharedBackend`, `BackendHandler` but supports multiple //! concurrently active pairs at once. -use super::CreateFork; +use super::{CreateFork, ResolvedFork}; use crate::{FoundryBlock, opts::ForkContext}; +use alloy_eips::BlockNumHash; use alloy_evm::EvmEnv; use alloy_network::{AnyNetwork, Network}; use alloy_primitives::{U256, map::HashMap}; use foundry_config::Config; use foundry_fork_db::{ - BackendHandler, BlockchainDb, ForkBlockEnv, SharedBackend, cache::BlockchainDbMeta, + BackendHandler, BlockchainDb, ForkBlock, ForkBlockEnv, SharedBackend, cache::BlockchainDbMeta, }; use futures::{ FutureExt, StreamExt, @@ -70,6 +71,13 @@ impl ForkId { Self(id) } + /// Returns the identifier for an exactly resolved fork. + fn resolved(url: &str, fork: &ResolvedFork) -> Self { + let mut id = Self::new_with_context(url, Some(fork.number()), Some(&fork.context())).0; + write!(id, "#{}:{}", fork.hash(), fork.source_id()).unwrap(); + Self(id) + } + /// Returns the identifier of the fork. pub fn as_str(&self) -> &str { &self.0 @@ -88,6 +96,18 @@ impl> From for ForkId { } } +/// Backend, environment, and identity returned after creating or rolling a fork. +pub struct ForkResult { + /// Identifier assigned to the fork. + pub id: ForkId, + /// Backend pinned to the resolved fork block. + pub backend: SharedBackend, + /// EVM environment reconstructed from the resolved fork block. + pub env: EvmEnv, + /// Exact source and block identity used to construct the backend. + pub resolved: ResolvedFork, +} + /// The Sender half of multi fork pair. /// Can send requests to the `MultiForkHandler` to create forks. #[derive(Clone, Debug)] @@ -155,11 +175,7 @@ impl< /// Returns a fork backend. /// /// If no matching fork backend exists it will be created. - #[allow(clippy::type_complexity)] - pub fn create_fork( - &self, - fork: CreateFork, - ) -> eyre::Result<(ForkId, SharedBackend, EvmEnv, ForkContext)> { + pub fn create_fork(&self, fork: CreateFork) -> eyre::Result> { trace!("Creating new fork, url={}, block={:?}", fork.url, fork.evm_opts.fork_block_number); let (sender, rx) = oneshot_channel(); let req = Request::CreateFork(Box::new(fork), sender); @@ -170,12 +186,7 @@ impl< /// Rolls the block of the fork. /// /// If no matching fork backend exists it will be created. - #[allow(clippy::type_complexity)] - pub fn roll_fork( - &self, - fork: ForkId, - block: u64, - ) -> eyre::Result<(ForkId, SharedBackend, EvmEnv, ForkContext)> { + pub fn roll_fork(&self, fork: ForkId, block: u64) -> eyre::Result> { trace!(?fork, ?block, "rolling fork"); let (sender, rx) = oneshot_channel(); let req = Request::RollFork(fork, block, sender); @@ -183,6 +194,19 @@ impl< rx.recv()? } + /// Rolls a fork to an already resolved exact block. + pub fn roll_fork_exact( + &self, + fork: ForkId, + block: BlockNumHash, + ) -> eyre::Result> { + trace!(?fork, ?block, "rolling fork to exact block"); + let (sender, rx) = oneshot_channel(); + let req = Request::RollForkExact(fork, block, sender); + self.handler.clone().try_send(req).map_err(|e| eyre::eyre!("{:?}", e))?; + rx.recv()? + } + /// Returns the `EvmEnv` of the given fork, if any. pub fn get_evm_env(&self, fork: ForkId) -> eyre::Result>> { trace!(?fork, "getting env config"); @@ -250,9 +274,7 @@ type CreateFuture = Pin< > + Send, >, >; -type CreateSender = OneshotSender< - eyre::Result<(ForkId, SharedBackend, EvmEnv, ForkContext)>, ->; +type CreateSender = OneshotSender>>; type GetEvmEnvSender = OneshotSender>>; /// Request that's send to the handler. @@ -264,6 +286,8 @@ enum Request { GetFork(ForkId, OneshotSender>>), /// Adjusts the block that's being forked, by creating a new fork at the new block. RollFork(ForkId, u64, CreateSender), + /// Adjusts the fork to an already resolved exact block. + RollForkExact(ForkId, BlockNumHash, CreateSender), /// Returns the environment of the fork. GetEvmEnv(ForkId, GetEvmEnvSender), /// Updates the block number and timestamp of the fork. @@ -356,20 +380,24 @@ impl< expected_identity: Option, sender: CreateSender, ) { - let expected_context = expected_identity.as_ref().or(fork.expected_context.as_ref()); - let requested_fork_id = - ForkId::new_with_context(&fork.url, fork.evm_opts.fork_block_number, expected_context); - trace!(?requested_fork_id, "creating new fork"); - - // There could already be a task for the requested fork in progress. - if let Some(in_progress) = self.find_in_progress_task(&requested_fork_id) { + let resolved_id = + fork.resolved.as_ref().map(|resolved| ForkId::resolved(&fork.url, resolved)); + trace!(?resolved_id, "creating fork"); + + // Only deduplicate requests that already carry an exact identity. Unresolved requests at + // the same URL and height can resolve to different blocks across a reorganization. + if let Some(fork_id) = &resolved_id + && let Some(in_progress) = self.find_in_progress_task(fork_id) + { in_progress.push(sender); return; } // Need to create a new fork. + let task_id = + resolved_id.unwrap_or_else(|| ForkId::new(&fork.url, fork.evm_opts.fork_block_number)); let task = Box::pin(create_fork(fork, expected_identity)); - self.pending_tasks.push(ForkTask::Create(task, requested_fork_id, sender, Vec::new())); + self.pending_tasks.push(ForkTask::Create(task, task_id, sender, Vec::new())); } fn insert_new_fork( @@ -380,21 +408,29 @@ impl< additional_senders: Vec>, ) { self.forks.insert(fork_id.clone(), fork.clone()); - let context = - fork.opts.expected_context.expect("created forks always retain their resolved context"); - let _ = - sender.send(Ok((fork_id.clone(), fork.backend.clone(), fork.evm_env.clone(), context))); + let resolved = fork + .opts + .resolved + .as_ref() + .expect("created forks always retain their resolved identity") + .clone(); + let _ = sender.send(Ok(ForkResult { + id: fork_id.clone(), + backend: fork.backend.clone(), + env: fork.evm_env.clone(), + resolved: resolved.clone(), + })); // Notify all additional senders and track unique forkIds. for sender in additional_senders { let next_fork_id = fork.inc_senders(fork_id.clone()); self.forks.insert(next_fork_id.clone(), fork.clone()); - let _ = sender.send(Ok(( - next_fork_id, - fork.backend.clone(), - fork.evm_env.clone(), - context, - ))); + let _ = sender.send(Ok(ForkResult { + id: next_fork_id, + backend: fork.backend.clone(), + env: fork.evm_env.clone(), + resolved: resolved.clone(), + })); } } @@ -423,12 +459,30 @@ impl< Request::RollFork(fork_id, block, sender) => { if let Some(fork) = self.forks.get(&fork_id) { trace!(target: "fork::multi", "rolling {} to {}", fork_id, block); + let expected_identity = fork.opts.resolved.as_ref().map(ResolvedFork::context); let mut opts = fork.opts.clone(); opts.evm_opts.fork_block_number = Some(block); opts.evm_opts.fork_block_number_is_inferred = false; - opts.expected_context = None; - let expected_identity = fork.opts.expected_context; - self.create_fork_with_identity(opts, expected_identity, sender); + opts.resolved = None; + self.create_fork_with_identity(opts, expected_identity, sender) + } else { + let _ = + sender.send(Err(eyre::eyre!("No matching fork exists for {}", fork_id))); + } + } + Request::RollForkExact(fork_id, block, sender) => { + if let Some(fork) = self.forks.get(&fork_id) { + trace!(target: "fork::multi", "rolling {} to exact block {:?}", fork_id, block); + let mut opts = fork.opts.clone(); + opts.evm_opts.fork_block_number = Some(block.number); + opts.evm_opts.fork_block_number_is_inferred = false; + opts.resolved = Some( + opts.resolved + .as_ref() + .expect("an exact roll requires an existing resolved fork") + .at_block(block), + ); + self.create_fork(opts, sender) } else { let _ = sender.send(Err(eyre::eyre!("No matching fork exists for {}", fork_id))); @@ -670,8 +724,18 @@ async fn create_fork< // Here we use [`AnyNetwork`] to maximize compatibility with custom chains, aligned with // `EvmOpts::env` impl. let any_provider = fork.evm_opts.fork_provider_with_url::(&fork.url)?; - let (evm_env, fork_context) = - fork.evm_opts.fork_evm_env_with_context::(&any_provider).await?; + let (evm_env, resolved) = if let Some(resolved) = fork.resolved.clone() { + let evm_env = fork + .evm_opts + .fork_evm_env_at_resolved::<_, BLOCK, _, _>(&any_provider, &resolved) + .await?; + (evm_env, resolved) + } else { + let (evm_env, resolved) = + fork.evm_opts.fork_evm_env_resolved::<_, BLOCK, _, _>(&any_provider).await?; + (evm_env, resolved) + }; + let fork_context = resolved.context(); if require_endpoint_family_match && !execution_networks.supports_fork_source(&fork_context.network_profile) { @@ -681,21 +745,15 @@ async fn create_fork< execution_networks.execution_network() ); } - if let Some(expected) = fork.expected_context - && !expected.has_same_backend_target(fork_context) - { - eyre::bail!( - "fork endpoint changed while the execution environment and backend were being built" - ); - } if let Some(expected) = expected_identity { eyre::ensure!( fork_context.has_same_endpoint_identity(expected), "fork endpoint identity changed while the fork was being rolled" ); } - let number = fork_context.block_number; - let meta = BlockchainDbMeta::new(evm_env.block_env.clone(), fork.url.clone()); + let number = resolved.number(); + let meta = BlockchainDbMeta::new(evm_env.block_env.clone(), fork.url.clone()) + .with_fork_identity(resolved.hash(), resolved.source_id()); // Determine the cache path if caching is enabled. let cache_path = if fork.enable_caching { @@ -706,10 +764,68 @@ async fn create_fork< let provider = fork.evm_opts.fork_provider_with_url::(&fork.url)?; let db = BlockchainDb::new(meta, cache_path); - let (backend, handler) = SharedBackend::new(provider, db, Some(number.into())); - fork.expected_context = Some(fork_context); - let fork_id = ForkId::new_with_context(&fork.url, Some(number), Some(&fork_context)); + let anchor = ForkBlock::with_rpc_number( + evm_env.block_env.number().saturating_to(), + resolved.number(), + resolved.hash(), + ); + let (backend, handler) = SharedBackend::new_with_anchor(provider, db, anchor)?; + let fork_id = ForkId::resolved(&fork.url, &resolved); + fork.resolved = Some(resolved); let fork = CreatedFork::new(fork, evm_env, backend); Ok((fork_id, fork, handler)) } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + use foundry_evm_networks::{NetworkConfigs, NetworkVariant}; + + fn context(block_number: u64) -> ForkContext { + ForkContext { + execution_chain_id: 1, + source_chain_id: 1, + network: NetworkVariant::Ethereum, + network_profile: NetworkConfigs::default(), + block_number, + hardfork: None, + instance_id: None, + source_fork_block_number: None, + source_fork_block_hash: None, + } + } + + #[test] + fn resolved_fork_ids_include_hash_and_source_identity() { + let url = "http://localhost:8545"; + let first = ResolvedFork::new( + url, + None, + None, + Some(1), + BlockNumHash::new(1, B256::with_last_byte(1)), + context(1), + ); + let replacement = ResolvedFork::new( + url, + None, + None, + Some(1), + BlockNumHash::new(1, B256::with_last_byte(2)), + context(1), + ); + let authenticated = ResolvedFork::new( + url, + Some(&["Authorization: secret".to_string()]), + None, + Some(1), + BlockNumHash::new(1, B256::with_last_byte(1)), + context(1), + ); + + assert_ne!(ForkId::resolved(url, &first), ForkId::resolved(url, &replacement)); + assert_ne!(ForkId::resolved(url, &first), ForkId::resolved(url, &authenticated)); + } +} diff --git a/crates/evm/core/src/fork/resolved.rs b/crates/evm/core/src/fork/resolved.rs index 6a315763c0db2..82df5d2d754c1 100644 --- a/crates/evm/core/src/fork/resolved.rs +++ b/crates/evm/core/src/fork/resolved.rs @@ -1,13 +1,13 @@ use crate::opts::ForkContext; use alloy_eips::{BlockId, BlockNumHash}; -use alloy_primitives::{B256, BlockNumber}; +use alloy_primitives::{B256, BlockNumber, keccak256}; use std::fmt; /// A fork selector and block identity resolved from a configured RPC source. /// /// This context binds exact preflight reads and EVM environment reconstruction to the source, -/// selector, and block that were resolved together. The fork database itself remains -/// number-pinned. +/// selector, and block that were resolved together. The fork database uses the resolved hash for +/// state reads and block ancestry. #[derive(Clone, PartialEq, Eq, Hash)] pub struct ResolvedFork { source: ForkSource, @@ -94,6 +94,55 @@ impl ResolvedFork { pub(crate) const fn block(&self) -> BlockNumHash { self.block } + + /// Returns this resolution advanced to another exact block on the same RPC source. + pub(crate) fn at_block(&self, block: BlockNumHash) -> Self { + let mut resolved = self.clone(); + resolved.selector = Some(block.number); + resolved.block = block; + resolved.context.block_number = block.number; + resolved + } + + /// Returns an opaque identity for the complete authenticated RPC source. + pub(crate) fn source_id(&self) -> B256 { + let mut encoded = Vec::new(); + encoded.extend_from_slice(b"foundry-resolved-fork-source-v1"); + encode_source_part(&mut encoded, self.source.url.as_bytes()); + encoded.extend_from_slice( + &u64::try_from(self.source.headers.len()) + .expect("fork header count exceeds u64") + .to_be_bytes(), + ); + for header in &self.source.headers { + encode_source_part(&mut encoded, header.as_bytes()); + } + if let Some(jwt) = &self.source.jwt { + encoded.push(1); + encode_source_part(&mut encoded, jwt.as_bytes()); + } else { + encoded.push(0); + } + keccak256(encoded) + } + + /// Returns a redacted, opaque fingerprint of the complete resolved fork identity. + pub fn fingerprint(&self) -> B256 { + let encoded = serde_json::to_vec(&( + "foundry-resolved-fork-v1", + self.source_id(), + self.block, + self.context, + )) + .expect("resolved fork identity is serializable"); + keccak256(encoded) + } +} + +fn encode_source_part(encoded: &mut Vec, part: &[u8]) { + let len = u64::try_from(part.len()).expect("source identity part length exceeds u64"); + encoded.extend_from_slice(&len.to_be_bytes()); + encoded.extend_from_slice(part); } impl fmt::Debug for ResolvedFork { @@ -163,4 +212,27 @@ mod tests { assert_ne!(first, second); assert_eq!(HashSet::from([first, second]).len(), 2); } + + #[test] + fn authenticated_source_identity_is_unambiguous() { + let block = BlockNumHash::new(1, B256::with_last_byte(1)); + let context = context(1); + let plain = ResolvedFork::new("http://localhost:8545", None, None, None, block, context); + let header = ResolvedFork::new( + "http://localhost:8545", + Some(&["secret".to_string()]), + None, + None, + block, + context, + ); + let jwt = + ResolvedFork::new("http://localhost:8545", None, Some("secret"), None, block, context); + + assert_ne!(plain.source_id(), header.source_id()); + assert_ne!(plain.source_id(), jwt.source_id()); + assert_ne!(header.source_id(), jwt.source_id()); + assert_ne!(plain.fingerprint(), header.fingerprint()); + assert_ne!(plain.fingerprint(), jwt.fingerprint()); + } } diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index 8a695ec17c203..7a14b055fff09 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -226,7 +226,7 @@ fn endpoint_hardfork( /// Identity and block context of the remote chain backing a fork. /// /// The source chain ID remains distinct from the configured `CHAINID` opcode override. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize)] pub struct ForkContext { /// Chain ID exposed through `eth_chainId`. pub execution_chain_id: ChainId, @@ -903,22 +903,9 @@ impl EvmOpts { &self, fork: Option<&ResolvedFork>, ) -> eyre::Result<(EvmEnv, TX)> { - let (evm_env, tx, _) = self.env_with_resolved_fork_context(fork).await?; - Ok((evm_env, tx)) - } - - /// Returns environments at an exact fork block together with its current endpoint identity. - pub async fn env_with_resolved_fork_context< - SPEC: Into + Default + Copy, - BLOCK: FoundryBlock + Default, - TX: FoundryTransaction + Default, - >( - &self, - fork: Option<&ResolvedFork>, - ) -> eyre::Result<(EvmEnv, TX, Option)> { let Some(_) = &self.fork_url else { eyre::ensure!(fork.is_none(), "resolved fork provided without a configured fork"); - return Ok((self.local_evm_env(), self.local_tx_env(), None)); + return Ok((self.local_evm_env(), self.local_tx_env())); }; let fork = fork.ok_or_else(|| eyre::eyre!("fork must be resolved"))?; let provider = self.provider_for_resolved_fork::(fork)?; @@ -943,7 +930,7 @@ impl EvmOpts { self.ensure_expected_fork_endpoint(&identity)?; if endpoint == fork.context() && endpoint.matches_identity(&identity) { let chain_id = self.chain_id_override().unwrap_or(endpoint.execution_chain_id); - return Ok((evm_env, self.fork_tx_env(gas_price, chain_id), Some(endpoint))); + return Ok((evm_env, self.fork_tx_env(gas_price, chain_id))); } } eyre::bail!( @@ -969,8 +956,8 @@ impl EvmOpts { Ok((evm_env, block.number)) } - /// Returns the fork environment together with the remote chain identity used to build it. - pub(crate) async fn fork_evm_env_with_context< + /// Returns the EVM environment and block identity fetched from the fork endpoint. + pub(crate) async fn fork_evm_env_resolved< SPEC: Into + Default + Copy, BLOCK: FoundryBlock + Default, N: Network, @@ -978,11 +965,12 @@ impl EvmOpts { >( &self, provider: &P, - ) -> eyre::Result<(EvmEnv, ForkContext)> { + ) -> eyre::Result<(EvmEnv, ResolvedFork)> { let mut node_info_probe = self.anvil_node_info_probe(); - let (evm_env, _, context) = + let (evm_env, block, context) = self.fork_evm_env_resolved_with_context(provider, &mut node_info_probe).await?; - Ok((evm_env, context)) + let fork_url = self.fork_url.as_deref().unwrap_or_default(); + Ok((evm_env, self.resolved_fork(fork_url, block, context))) } /// Returns the fork environment, exact block, and endpoint identity resolved together. @@ -1169,6 +1157,25 @@ impl EvmOpts { Ok((evm_env, context)) } + /// Reconstructs the fork environment at an already resolved exact block. + pub(crate) async fn fork_evm_env_at_resolved< + SPEC: Into + Default + Copy, + BLOCK: FoundryBlock + Default, + N: Network, + P: Provider, + >( + &self, + provider: &P, + expected: &ResolvedFork, + ) -> eyre::Result> { + let mut node_info_probe = self.anvil_node_info_probe(); + node_info_probe.identified |= expected.context().hardfork.is_some(); + let (evm_env, _) = self + .fork_evm_env_at_resolved_with_context(provider, expected, &mut node_info_probe) + .await?; + Ok(evm_env) + } + fn fork_env_from_block< SPEC: Into + Default + Copy, BLOCK: FoundryBlock + Default, @@ -1273,11 +1280,33 @@ impl EvmOpts { pub fn get_fork( &self, config: &Config, - source_chain_id: u64, + chain_id: u64, + fork_block_number: Option, + ) -> Option { + self.get_fork_with_identity(config, chain_id, fork_block_number, None) + } + + /// Returns a fork configuration pinned to an already resolved block identity. + pub fn get_fork_resolved( + &self, + config: &Config, + chain_id: u64, + fork: Option<&ResolvedFork>, + ) -> Option { + let fork_block_number = fork.map(ResolvedFork::number); + let source_chain_id = fork.map(|fork| fork.context().source_chain_id).unwrap_or(chain_id); + self.get_fork_with_identity(config, source_chain_id, fork_block_number, fork.cloned()) + } + + fn get_fork_with_identity( + &self, + config: &Config, + chain_id: u64, fork_block_number: Option, + resolved: Option, ) -> Option { let url = self.fork_url.clone()?; - let enable_caching = config.enable_caching(&url, source_chain_id); + let enable_caching = config.enable_caching(&url, chain_id); // Pin fork_block_number to the block that was already fetched in env, so subsequent // fork operations use the same block. This prevents inconsistencies when forking at @@ -1288,19 +1317,7 @@ impl EvmOpts { evm_opts.fork_block_number_is_inferred = fork_block_number.is_some(); } - Some(CreateFork { url, enable_caching, evm_opts, expected_context: None }) - } - - /// Builds a fork request constrained to a previously resolved endpoint context. - pub fn get_fork_with_context( - &self, - config: &Config, - context: ForkContext, - ) -> Option { - let mut fork = - self.get_fork(config, context.source_chain_id, Some(context.block_number))?; - fork.expected_context = Some(context); - Some(fork) + Some(CreateFork { url, enable_caching, evm_opts, resolved }) } /// Returns the gas limit to use @@ -2716,24 +2733,34 @@ mod tests { } #[tokio::test(flavor = "multi_thread")] - async fn fork_backend_rejects_changed_expected_context() { + async fn fork_backend_rejects_changed_resolved_context() { let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await; let mut evm_opts = EvmOpts { fork_url: Some(handle.http_endpoint()), ..Default::default() }; evm_opts.infer_network_from_fork().await.unwrap(); - let (_, _, context) = - evm_opts.env_with_fork_context::().await.unwrap(); - let mut context = context.unwrap(); + let (_, _, resolved) = evm_opts.env_resolved::().await.unwrap(); + let resolved = resolved.unwrap(); + let mut context = resolved.context(); let mut invalid_instance = context.instance_id.unwrap_or_default(); invalid_instance[31] ^= 1; context.instance_id = Some(invalid_instance); - let fork = evm_opts.get_fork_with_context(&Config::default(), context).unwrap(); + let invalid = ResolvedFork::new( + evm_opts.fork_url.as_deref().unwrap(), + evm_opts.fork_source_headers(), + evm_opts.rpc_jwt.as_deref(), + evm_opts.fork_block_number, + resolved.block(), + context, + ); + let fork = evm_opts + .get_fork_resolved(&Config::default(), context.execution_chain_id, Some(&invalid)) + .unwrap(); let error = crate::backend::Backend::::spawn(Some(fork)).unwrap_err(); assert!( - error.to_string().contains( - "fork endpoint changed while the execution environment and backend were being built" - ), + error + .to_string() + .contains("changed after its block and execution context were resolved"), "{error}" ); } diff --git a/crates/evm/evm/src/executors/trace.rs b/crates/evm/evm/src/executors/trace.rs index c9e7348720d1d..2bca6965fcd01 100644 --- a/crates/evm/evm/src/executors/trace.rs +++ b/crates/evm/evm/src/executors/trace.rs @@ -1,7 +1,7 @@ use crate::executors::{Executor, ExecutorBuilder}; use alloy_primitives::{Address, ChainId, U256, map::HashMap}; use alloy_rpc_types::state::StateOverride; -use eyre::Context; +use eyre::{Context, ContextCompat}; use foundry_compilers::artifacts::EvmVersion; use foundry_config::{Chain, Config, evm_spec_id}; use foundry_evm_core::{ @@ -152,16 +152,13 @@ impl TracingExecutor { evm_opts.fork_block_number = config.fork_block_number; evm_opts.infer_network_from_fork().await?; let networks = evm_opts.networks; - let (evm_env, tx_env, fork_context) = evm_opts - .env_with_fork_context::, BlockEnvFor, TxEnvFor>() - .await?; - let Some(fork_context) = fork_context else { - eyre::bail!("fork context is missing for tracing executor"); - }; - + let (evm_env, tx_env, resolved) = + evm_opts.env_resolved::, BlockEnvFor, TxEnvFor>().await?; + let resolved = resolved.context("fork context is missing for tracing executor")?; let fork = evm_opts - .get_fork_with_context(config, fork_context) - .ok_or_else(|| eyre::eyre!("fork URL is missing for tracing executor"))?; + .get_fork_resolved(config, evm_env.cfg_env.chain_id, Some(&resolved)) + .context("fork URL is missing for tracing executor")?; + let fork_context = resolved.context(); let chain = fork_context.source_chain_id.into(); Ok((evm_env, tx_env, fork, chain, networks, fork_context.hardfork)) diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index a471598c2a076..fa956d19fbd51 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -70,6 +70,7 @@ use foundry_evm::{ BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor, }, executors::ShowmapDomain, + fork::ResolvedFork, fuzz::{BaseCounterExample, BasicTxDetails, CounterExample}, hardforks::{ExecutionSpec, TempoHardfork}, opts::EvmOpts, @@ -2060,6 +2061,8 @@ impl TestArgs { // Clone config and evm_opts before dispatch (needed for mutation testing). let config_for_mutation = config.clone(); let evm_opts_for_mutation = evm_opts.clone(); + let mutation_fork = + if self.mutate.is_some() { evm_opts.resolve_fork().await? } else { None }; // Detect per-test network annotations. let override_networks = @@ -2076,6 +2079,7 @@ impl TestArgs { output, &mut filter, execution.clone(), + mutation_fork.as_ref(), ) .await? } else { @@ -2099,6 +2103,7 @@ impl TestArgs { }, ..execution.clone() }, + None, ) .await?; @@ -2123,6 +2128,7 @@ impl TestArgs { }, ..execution.clone() }, + None, ) .await?; merge_outcomes(&mut outcome, pass_outcome); @@ -2563,6 +2569,7 @@ impl TestArgs { Arc::new(config_for_mutation.clone()), output, evm_opts_for_mutation.clone(), + mutation_fork, mutation_config, ) .await?; @@ -2591,10 +2598,17 @@ impl TestArgs { output: &ProjectCompileOutput, filter: &mut ProjectPathsAwareFilter, execution: TestExecutionOptions, + resolved_fork: Option<&ResolvedFork>, ) -> eyre::Result<(Libraries, TestOutcome)> { let verbosity = evm_opts.verbosity; - let (evm_env, tx_env, fork) = - evm_opts.env_resolved::, BlockEnvFor, TxEnvFor>().await?; + let (evm_env, tx_env, fork) = if let Some(fork) = resolved_fork { + let (evm_env, tx_env) = evm_opts + .env_with_resolved_fork::, BlockEnvFor, TxEnvFor>(Some(fork)) + .await?; + (evm_env, tx_env, Some(fork.clone())) + } else { + evm_opts.env_resolved::, BlockEnvFor, TxEnvFor>().await? + }; let fork_context = fork.as_ref().map(|fork| fork.context()); let fork_chain_id = fork_context.map(|context| context.source_chain_id); let fork_hardfork = fork_context.and_then(|context| context.hardfork); @@ -2609,9 +2623,7 @@ impl TestArgs { .set_record_all_steps(self.evm_profile.is_some()) .initial_balance(evm_opts.initial_balance) .sender(evm_opts.sender) - .with_fork( - fork_context.and_then(|context| evm_opts.get_fork_with_context(&config, context)), - ) + .with_fork(evm_opts.get_fork_resolved(&config, evm_env.cfg_env.chain_id, fork.as_ref())) .with_fork_chain_id(fork_chain_id) .with_fork_hardfork(fork_hardfork) .enable_isolation(evm_opts.isolate) @@ -2650,9 +2662,7 @@ impl TestArgs { MultiContractRunnerBuilder::new(config.clone(), options.inline_config) .initial_balance(evm_opts.initial_balance) .sender(evm_opts.sender) - .with_fork( - fork_context.and_then(|context| evm_opts.get_fork_with_context(&config, context)), - ) + .with_fork(evm_opts.get_fork_resolved(&config, evm_env.cfg_env.chain_id, fork.as_ref())) .with_fork_chain_id(fork_chain_id) .with_fork_hardfork(fork_hardfork) .enable_isolation(evm_opts.isolate) @@ -2665,6 +2675,7 @@ impl TestArgs { } /// Dispatches `build_and_run_tests` to the correct network type based on `evm_opts.networks`. + #[allow(clippy::too_many_arguments)] async fn dispatch_network( &self, dispatch_opts: &EvmOpts, @@ -2673,31 +2684,52 @@ impl TestArgs { output: &ProjectCompileOutput, filter: &mut ProjectPathsAwareFilter, execution: TestExecutionOptions, + resolved_fork: Option<&ResolvedFork>, ) -> eyre::Result<(Libraries, TestOutcome)> { match network_dispatch_kind(dispatch_opts) { NetworkDispatchKind::Tempo => { self.build_and_run_tests::( - config, evm_opts, output, filter, execution, + config, + evm_opts, + output, + filter, + execution, + resolved_fork, ) .await } #[cfg(feature = "monad")] NetworkDispatchKind::Monad => { self.build_and_run_tests::( - config, evm_opts, output, filter, execution, + config, + evm_opts, + output, + filter, + execution, + resolved_fork, ) .await } #[cfg(feature = "optimism")] NetworkDispatchKind::Optimism => { self.build_and_run_tests::( - config, evm_opts, output, filter, execution, + config, + evm_opts, + output, + filter, + execution, + resolved_fork, ) .await } NetworkDispatchKind::Eth => { self.build_and_run_tests::( - config, evm_opts, output, filter, execution, + config, + evm_opts, + output, + filter, + execution, + resolved_fork, ) .await } diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 4d4827f8fc339..54b78c3fccce7 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -1184,7 +1184,7 @@ mod tests { enable_caching: false, url: "http://localhost:8545".into(), evm_opts: evm_opts.clone(), - expected_context: None, + resolved: None, }); assert!(!builder.create2_deployer_available(&evm_opts)); builder.fork = None; diff --git a/crates/forge/src/mutation/orchestrator.rs b/crates/forge/src/mutation/orchestrator.rs index d7d21734bb072..af59b5dd00752 100644 --- a/crates/forge/src/mutation/orchestrator.rs +++ b/crates/forge/src/mutation/orchestrator.rs @@ -26,14 +26,14 @@ use foundry_compilers::{ utils::source_files_iter, }; use foundry_config::{Config, filter::GlobMatcher}; -use foundry_evm::opts::EvmOpts; +use foundry_evm::{fork::ResolvedFork, opts::EvmOpts}; use crate::{ cmd::test::{FilterArgs, RerunFailure}, mutation::{ MutationHandler, MutationProgress, MutationReporter, MutationsSummary, mutant::{Mutant, MutationResult}, - runner::run_mutations_parallel_with_progress, + runner::{MutationEvmConfig, run_mutations_parallel_with_progress}, type_analysis::{collect_mutation_exclusions, normalize_path}, }, }; @@ -52,6 +52,7 @@ struct ExecutionCacheFingerprint<'a> { schema: &'static str, config: &'a Config, evm_opts: &'a EvmOpts, + resolved_fork: Option, filter_args: FilterArgsFingerprint<'a>, rerun_failures: Option<&'a [RerunFailure]>, num_workers: usize, @@ -131,11 +132,17 @@ pub struct MutationRunResult { pub async fn run_mutation_testing( config: Arc, output: &ProjectCompileOutput, - mut evm_opts: EvmOpts, + evm_opts: EvmOpts, + resolved_fork: Option, mutation_config: MutationRunConfig, ) -> Result { - let fork_block = evm_opts.pin_fork_block().await?; - let create2_deployer_available = evm_opts.can_use_create2_deployer(fork_block).await?; + let create2_deployer_available = + evm_opts.can_use_create2_deployer_resolved(resolved_fork.as_ref()).await?; + let mutation_evm = MutationEvmConfig { + opts: evm_opts.clone(), + resolved_fork: resolved_fork.clone(), + create2_deployer_available, + }; let num_workers = mutation_config.effective_workers(); let json_output = mutation_config.json_output; let artifact_link_references = output.artifact_ids().filter_map(|(id, artifact)| { @@ -169,6 +176,7 @@ pub async fn run_mutation_testing( &config, &execution_cache_output, &evm_opts, + resolved_fork.as_ref(), &mutation_config.filter_args, mutation_config.rerun_failures.as_deref(), num_workers, @@ -301,8 +309,7 @@ pub async fn run_mutation_testing( path.clone(), handler.src.clone(), config.clone(), - evm_opts.clone(), - create2_deployer_available, + mutation_evm.clone(), num_workers, progress.clone(), json_output, @@ -402,6 +409,7 @@ fn mutation_execution_cache_key( config: &Config, output: &ProjectCompileOutput, evm_opts: &EvmOpts, + resolved_fork: Option<&ResolvedFork>, filter_args: &FilterArgs, rerun_failures: Option<&[RerunFailure]>, num_workers: usize, @@ -419,6 +427,7 @@ fn mutation_execution_cache_key( mutation_execution_cache_key_from_parts_with_rerun_failures( config, evm_opts, + resolved_fork.map(ResolvedFork::fingerprint), filter_args, rerun_failures, num_workers, @@ -437,6 +446,7 @@ fn mutation_execution_cache_key_from_parts( mutation_execution_cache_key_from_parts_with_rerun_failures( config, evm_opts, + None, filter_args, None, num_workers, @@ -447,6 +457,7 @@ fn mutation_execution_cache_key_from_parts( fn mutation_execution_cache_key_from_parts_with_rerun_failures( config: &Config, evm_opts: &EvmOpts, + resolved_fork: Option, filter_args: &FilterArgs, rerun_failures: Option<&[RerunFailure]>, num_workers: usize, @@ -454,9 +465,10 @@ fn mutation_execution_cache_key_from_parts_with_rerun_failures( ) -> Result { artifacts.sort(); let fingerprint = ExecutionCacheFingerprint { - schema: "mutation-results-v1", + schema: "mutation-results-v2", config, evm_opts, + resolved_fork, filter_args: filter_args_fingerprint(filter_args), rerun_failures, num_workers, @@ -714,6 +726,37 @@ mod tests { assert_ne!(first_key, second_key); } + #[test] + fn execution_cache_key_changes_when_resolved_fork_changes() { + let config = Config::default(); + let evm_opts = EvmOpts::default(); + let filter_args = filter_args(); + let artifacts = vec![artifact("build-a")]; + + let first_key = mutation_execution_cache_key_from_parts_with_rerun_failures( + &config, + &evm_opts, + Some(alloy_primitives::B256::with_last_byte(1)), + &filter_args, + None, + 1, + artifacts.clone(), + ) + .unwrap(); + let second_key = mutation_execution_cache_key_from_parts_with_rerun_failures( + &config, + &evm_opts, + Some(alloy_primitives::B256::with_last_byte(2)), + &filter_args, + None, + 1, + artifacts, + ) + .unwrap(); + + assert_ne!(first_key, second_key); + } + #[test] fn execution_cache_key_changes_when_compiled_artifacts_change() { let config = Config::default(); @@ -859,6 +902,7 @@ mod tests { let first_key = mutation_execution_cache_key_from_parts_with_rerun_failures( &config, &evm_opts, + None, &filter_args, Some(&first_failures), 1, @@ -868,6 +912,7 @@ mod tests { let second_key = mutation_execution_cache_key_from_parts_with_rerun_failures( &config, &evm_opts, + None, &filter_args, Some(&second_failures), 1, diff --git a/crates/forge/src/mutation/runner.rs b/crates/forge/src/mutation/runner.rs index 5d8544d4a7cbd..963977e358401 100644 --- a/crates/forge/src/mutation/runner.rs +++ b/crates/forge/src/mutation/runner.rs @@ -29,6 +29,7 @@ use foundry_evm::{ core::evm::{ BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor, }, + fork::ResolvedFork, opts::EvmOpts, }; use rayon::prelude::*; @@ -60,6 +61,14 @@ pub struct MutationBatchResult { pub cancelled: bool, } +/// Immutable EVM inputs shared by the baseline and every mutation worker. +#[derive(Clone)] +pub struct MutationEvmConfig { + pub opts: EvmOpts, + pub resolved_fork: Option, + pub create2_deployer_available: bool, +} + /// Tracks progress and adaptive span skipping across parallel workers. pub struct SharedMutationState { /// Spans where mutations have survived - shared across workers for adaptive skipping. @@ -172,8 +181,7 @@ pub fn run_mutations_parallel_with_progress( source_path: PathBuf, original_source: Arc, config: Arc, - evm_opts: EvmOpts, - create2_deployer_available: bool, + evm: MutationEvmConfig, num_workers: usize, progress: Option, silent: bool, @@ -262,8 +270,7 @@ pub fn run_mutations_parallel_with_progress( &source_relative, &original_source, &config, - &evm_opts, - create2_deployer_available, + &evm, &shared_state, &temp_root, &filter_args, @@ -342,8 +349,7 @@ fn test_single_mutant_isolated( source_relative: &PathBuf, original_source: &Arc, config: &Arc, - evm_opts: &EvmOpts, - create2_deployer_available: bool, + evm: &MutationEvmConfig, shared_state: &Arc, temp_root: &Path, filter_args: &Arc, @@ -421,8 +427,7 @@ fn test_single_mutant_isolated( let result = match timeout { Some(budget) => run_compile_and_test_with_timeout( temp_config, - evm_opts, - create2_deployer_available, + evm, budget, temp_dir, shared_state, @@ -434,8 +439,7 @@ fn test_single_mutant_isolated( None => { let res = match compile_and_test( &temp_config, - evm_opts, - create2_deployer_available, + evm, filter_args, rerun_failures.as_ref().as_deref(), selected_sources_relative, @@ -475,8 +479,7 @@ fn test_single_mutant_isolated( #[allow(clippy::too_many_arguments)] fn run_compile_and_test_with_timeout( config: Arc, - evm_opts: &EvmOpts, - create2_deployer_available: bool, + evm: &MutationEvmConfig, budget: Duration, temp_dir: TempDir, shared_state: &Arc, @@ -486,7 +489,7 @@ fn run_compile_and_test_with_timeout( isolate: bool, ) -> MutationResult { let (tx, rx) = mpsc::channel::>(); - let opts = evm_opts.clone(); + let evm = evm.clone(); // Move `temp_dir` into the worker so its `Drop` only runs after the worker // thread exits. Do NOT capture by reference — the worker may outlive this // function on timeout. @@ -502,8 +505,7 @@ fn run_compile_and_test_with_timeout( let res = panic::catch_unwind(AssertUnwindSafe(|| { compile_and_test( &cfg, - &opts, - create2_deployer_available, + &evm, &filter_for_worker, rerun_for_worker.as_ref().as_deref(), &selected_sources_for_worker, @@ -618,18 +620,16 @@ fn temp_config_for_mutation(config: &Config, temp_path: &Path) -> Config { /// Dispatches to the correct network type based on `evm_opts.networks`. fn compile_and_test( config: &Arc, - evm_opts: &EvmOpts, - create2_deployer_available: bool, + evm: &MutationEvmConfig, filter_args: &FilterArgs, rerun_failures: Option<&[RerunFailure]>, selected_sources_relative: &[PathBuf], isolate: bool, ) -> Result { - if evm_opts.networks.is_tempo() { + if evm.opts.networks.is_tempo() { compile_and_test_inner::( config, - evm_opts, - create2_deployer_available, + evm, filter_args, rerun_failures, selected_sources_relative, @@ -637,11 +637,10 @@ fn compile_and_test( ) } else { #[cfg(feature = "monad")] - if evm_opts.networks.is_monad() { + if evm.opts.networks.is_monad() { return compile_and_test_inner::( config, - evm_opts, - create2_deployer_available, + evm, filter_args, rerun_failures, selected_sources_relative, @@ -649,11 +648,10 @@ fn compile_and_test( ); } #[cfg(feature = "optimism")] - if evm_opts.networks.is_optimism() { + if evm.opts.networks.is_optimism() { return compile_and_test_inner::( config, - evm_opts, - create2_deployer_available, + evm, filter_args, rerun_failures, selected_sources_relative, @@ -662,8 +660,7 @@ fn compile_and_test( } compile_and_test_inner::( config, - evm_opts, - create2_deployer_available, + evm, filter_args, rerun_failures, selected_sources_relative, @@ -674,13 +671,14 @@ fn compile_and_test( fn compile_and_test_inner( config: &Arc, - evm_opts: &EvmOpts, - create2_deployer_available: bool, + evm: &MutationEvmConfig, filter_args: &FilterArgs, rerun_failures: Option<&[RerunFailure]>, selected_sources_relative: &[PathBuf], isolate: bool, ) -> Result { + let evm_opts = &evm.opts; + let resolved_fork = evm.resolved_fork.as_ref(); // Compile let files = selected_sources_relative .iter() @@ -714,9 +712,10 @@ fn compile_and_test_inner( // Use block_on to run within the runtime context let results: BTreeMap = rt.block_on(async { - let (evm_env, tx_env, fork_context) = evm_opts - .env_with_fork_context::, BlockEnvFor, TxEnvFor>() + let (evm_env, tx_env) = evm_opts + .env_with_resolved_fork::, BlockEnvFor, TxEnvFor>(resolved_fork) .await?; + let fork_context = resolved_fork.map(ResolvedFork::context); let fork_chain_id = fork_context.map(|context| context.source_chain_id); let fork_hardfork = fork_context.and_then(|context| context.hardfork); @@ -728,14 +727,12 @@ fn compile_and_test_inner( .set_debug(false) .initial_balance(evm_opts.initial_balance) .sender(evm_opts.sender) - .with_fork( - fork_context.and_then(|context| evm_opts.get_fork_with_context(config, context)), - ) + .with_fork(evm_opts.get_fork_resolved(config, evm_env.cfg_env.chain_id, resolved_fork)) .with_fork_chain_id(fork_chain_id) .with_fork_hardfork(fork_hardfork) .enable_isolation(isolate) .fail_fast(true) - .with_create2_deployer_available(create2_deployer_available) + .with_create2_deployer_available(evm.create2_deployer_available) .build::(&compile_output, evm_env, tx_env, evm_opts.clone())?; runner.test_collect(&filter) diff --git a/crates/forge/tests/cli/test_cmd/exact_fork.rs b/crates/forge/tests/cli/test_cmd/exact_fork.rs new file mode 100644 index 0000000000000..d7f8b4773d012 --- /dev/null +++ b/crates/forge/tests/cli/test_cmd/exact_fork.rs @@ -0,0 +1,189 @@ +use super::*; +use axum::{Json, Router, body::Bytes}; +use serde_json::{Value, json}; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; + +async fn rpc(endpoint: &str, method: &str, params: Value) -> Value { + reqwest::Client::new() + .post(endpoint) + .json(&json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params})) + .send() + .await + .unwrap() + .json::() + .await + .unwrap()["result"] + .clone() +} + +forgetest_async!(fork_execution_uses_exact_ancestry_after_reorg, |prj, cmd| { + let (_api, anvil) = spawn(NodeConfig::test()).await; + let upstream = anvil.http_endpoint(); + let initial = rpc(&upstream, "eth_getBlockByNumber", json!(["latest", false])).await; + let initial_timestamp = + u64::from_str_radix(initial["timestamp"].as_str().unwrap().trim_start_matches("0x"), 16) + .unwrap(); + let snapshot = rpc(&upstream, "evm_snapshot", json!([])).await; + + rpc(&upstream, "evm_setNextBlockTimestamp", json!([initial_timestamp + 10])).await; + rpc(&upstream, "evm_mine", json!([])).await; + rpc(&upstream, "evm_setNextBlockTimestamp", json!([initial_timestamp + 20])).await; + rpc(&upstream, "evm_mine", json!([])).await; + let anchor = rpc(&upstream, "eth_getBlockByNumber", json!(["latest", false])).await; + let anchor_hash = anchor["hash"].as_str().unwrap().to_string(); + let anchor_number = anchor["number"].as_str().unwrap().to_string(); + let anchor_parent = anchor["parentHash"].as_str().unwrap().to_string(); + let parent = rpc(&upstream, "eth_getBlockByHash", json!([anchor_parent, false])).await; + + std::assert_eq!(rpc(&upstream, "evm_revert", json!([snapshot])).await, true); + rpc(&upstream, "evm_setNextBlockTimestamp", json!([initial_timestamp + 30])).await; + rpc(&upstream, "evm_mine", json!([])).await; + rpc(&upstream, "evm_setNextBlockTimestamp", json!([initial_timestamp + 40])).await; + rpc(&upstream, "evm_mine", json!([])).await; + let replacement = rpc(&upstream, "eth_getBlockByNumber", json!(["latest", false])).await; + std::assert_ne!(replacement["hash"], anchor["hash"]); + std::assert_ne!(replacement["parentHash"], anchor["parentHash"]); + + let exact_state_read = Arc::new(AtomicBool::new(false)); + let serve_orphan_by_number = Arc::new(AtomicBool::new(true)); + let app = Router::new().fallback({ + let upstream = upstream.clone(); + let anchor = anchor.clone(); + let anchor_hash = anchor_hash.clone(); + let anchor_number = anchor_number.clone(); + let anchor_parent = anchor_parent.clone(); + let parent = parent.clone(); + let exact_state_read = Arc::clone(&exact_state_read); + let serve_orphan_by_number = Arc::clone(&serve_orphan_by_number); + move |body: Bytes| { + let upstream = upstream.clone(); + let anchor = anchor.clone(); + let anchor_hash = anchor_hash.clone(); + let anchor_number = anchor_number.clone(); + let anchor_parent = anchor_parent.clone(); + let parent = parent.clone(); + let exact_state_read = Arc::clone(&exact_state_read); + let serve_orphan_by_number = Arc::clone(&serve_orphan_by_number); + async move { + let mut request: Value = serde_json::from_slice(&body).unwrap(); + let id = request["id"].clone(); + let method = request["method"].as_str().unwrap(); + if method == "eth_getBlockByNumber" && request["params"][0] == "latest" { + return Json(json!({"jsonrpc": "2.0", "id": id, "result": anchor})); + } + if method == "eth_getBlockByNumber" + && request["params"][0] == anchor_number + && serve_orphan_by_number.load(Ordering::Relaxed) + { + return Json(json!({"jsonrpc": "2.0", "id": id, "result": anchor})); + } + if method == "eth_getBlockByHash" && request["params"][0] == anchor_hash { + return Json(json!({"jsonrpc": "2.0", "id": id, "result": anchor})); + } + if method == "eth_getBlockByHash" && request["params"][0] == anchor_parent { + return Json(json!({"jsonrpc": "2.0", "id": id, "result": parent})); + } + let state_block_hash = if matches!( + method, + "eth_getBalance" + | "eth_getTransactionCount" + | "eth_getCode" + | "eth_getStorageAt" + ) { + request["params"] + .as_array() + .and_then(|params| params.last()) + .and_then(|block| block.get("blockHash")) + .and_then(Value::as_str) + } else { + None + }; + let exact_state = state_block_hash == Some(&anchor_hash); + if exact_state + && method == "eth_getBalance" + && request["params"][0] == "0x0000000000000000000000000000000000000100" + { + exact_state_read.store(true, Ordering::Relaxed); + return Json(json!({"jsonrpc": "2.0", "id": id, "result": "0x2a"})); + } + if exact_state { + let params = request["params"].as_array_mut().unwrap(); + *params.last_mut().unwrap() = Value::String(anchor_number); + } + let response = reqwest::Client::new() + .post(upstream) + .json(&request) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + Json(response) + } + } + }); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = format!("http://{}", listener.local_addr().unwrap()); + let _server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() }); + + let anchor_number_value = + u64::from_str_radix(anchor_number.trim_start_matches("0x"), 16).unwrap(); + let (fork_api, fork_handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(endpoint.clone())) + .with_fork_block_number(Some(anchor_number_value)) + .with_fork_chain_id(Some(U256::from(31337))), + ) + .await; + serve_orphan_by_number.store(false, Ordering::Relaxed); + let orphan_hash = fork_api.backend.get_fork().unwrap().block_hash(); + let reset_number = fork_api.backend.get_fork().unwrap().block_number(); + let canonical = + rpc(&upstream, "eth_getBlockByNumber", json!([format!("0x{reset_number:x}"), false])).await; + std::assert_ne!(canonical["hash"], orphan_hash.to_string()); + fork_api.anvil_reset(Some(Default::default())).await.unwrap(); + std::assert_eq!( + fork_api.backend.get_fork().unwrap().block_hash().to_string(), + canonical["hash"] + ); + drop(fork_handle); + + prj.add_test( + "ExactFork.t.sol", + &format!( + r#" +contract ExactForkTest {{ + function testExactForkAncestry() public {{ + require(address(0x100).balance == 42, "wrong state"); + require(blockhash(block.number - 1) == bytes32({anchor_parent}), "wrong ancestry"); + }} +}} +"# + ), + ); + + cmd.args(["test", "--fork-url", &endpoint, "--match-test", "testExactForkAncestry"]) + .assert_success(); + #[cfg(feature = "monad")] + { + let mut monad_cmd = prj.forge_command(); + monad_cmd + .args([ + "test", + "--network", + "monad", + "--fork-url", + &endpoint, + "--match-test", + "testExactForkAncestry", + "--threads", + "1", + ]) + .assert_success(); + } + assert!(exact_state_read.load(Ordering::Relaxed)); +}); diff --git a/crates/forge/tests/cli/test_cmd/mod.rs b/crates/forge/tests/cli/test_cmd/mod.rs index a8bbf70551ff0..b9e702a9743a1 100644 --- a/crates/forge/tests/cli/test_cmd/mod.rs +++ b/crates/forge/tests/cli/test_cmd/mod.rs @@ -17,6 +17,7 @@ use std::{io::Write, path::PathBuf, str::FromStr}; mod brutalize; mod core; +mod exact_fork; mod fuzz; mod invariant; mod logs; diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 46ce3f62c6d23..fb7b87548f686 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -1040,7 +1040,11 @@ impl ScriptConfig { if let Some(backend) = self.backends.get(&resolved) { backend.clone() } else { - let fork = self.evm_opts.get_fork_with_context(&self.config, resolved.context()); + let fork = self.evm_opts.get_fork_resolved( + &self.config, + evm_env.cfg_env.chain_id, + Some(&resolved), + ); let backend = Backend::spawn(fork)?; self.backends.insert(resolved, backend.clone()); backend