Skip to main content

foundry_evm_core/fork/
mod.rs

1use super::opts::{EvmOpts, ForkContext};
2use alloy_consensus::BlockHeader;
3use alloy_eips::{BlockId, BlockNumHash};
4use alloy_network::{AnyNetwork, AnyRpcBlock, Network};
5use alloy_primitives::{B256, BlockNumber, keccak256};
6use alloy_provider::{Provider, RootProvider};
7use alloy_rpc_client::RpcClient;
8use eyre::OptionExt;
9use std::{
10    fmt,
11    hash::{Hash, Hasher},
12    sync::Arc,
13};
14
15pub mod database;
16
17mod multi;
18pub use multi::{ForkId, ForkResult, MultiFork, MultiForkHandler};
19
20mod bal;
21pub use bal::{cache_bal, validate_bal};
22
23/// Represents a _fork_ of a remote chain whose data is available only via the `url` endpoint.
24#[derive(Clone, Debug)]
25pub struct CreateFork {
26    /// Whether to enable rpc storage caching for this fork
27    pub enable_caching: bool,
28    /// The URL to a node for fetching remote state
29    pub url: String,
30    /// All env settings as configured by the user
31    pub evm_opts: EvmOpts,
32}
33
34/// A prepared remote fork. The RPC client and block are shared by preflight and execution.
35///
36/// The configured selector remains separate from the observed block. Clones share the provider
37/// and header; changing the requested source or rolling a fork prepares a new snapshot. The
38/// optional number-based state mode retains this anchor, but state reads may follow a replacement
39/// block after a reorganization.
40#[derive(Clone)]
41pub struct Fork {
42    source_id: B256,
43    client: RpcClient,
44    selector: Option<BlockNumber>,
45    pub(crate) block: Arc<AnyRpcBlock>,
46    context: ForkContext,
47    /// Whether state reads use the RPC block number instead of the exact hash.
48    pub(crate) state_by_number: bool,
49}
50
51impl Fork {
52    pub(crate) fn new(
53        opts: &EvmOpts,
54        client: RpcClient,
55        block: AnyRpcBlock,
56        mut context: ForkContext,
57    ) -> Self {
58        debug_assert_eq!(block.header.number(), context.block_number);
59        context.network_profile = context.network_profile.canonical_execution_profile();
60        Self {
61            source_id: source_id(
62                opts.fork_url.as_deref().unwrap_or_default(),
63                opts.fork_source_headers(),
64                opts.rpc_jwt.as_deref(),
65            ),
66            client,
67            selector: opts.fork_block_number,
68            block: Arc::new(block),
69            context,
70            state_by_number: opts.fork_state_by_number,
71        }
72    }
73
74    /// Whether this backend's fork was created from the requested source, selector, and state mode.
75    pub fn matches_request(&self, opts: &EvmOpts) -> bool {
76        opts.fork_url.as_deref().is_some_and(|url| {
77            self.selector == opts.fork_block_number
78                && self.state_by_number == opts.fork_state_by_number
79                && self.matches_source(url, opts.fork_source_headers(), opts.rpc_jwt.as_deref())
80        })
81    }
82
83    pub(crate) fn matches_source(
84        &self,
85        url: &str,
86        headers: Option<&[String]>,
87        jwt: Option<&str>,
88    ) -> bool {
89        self.source_id == source_id(url, headers, jwt)
90    }
91
92    /// Returns the RPC block number, which may differ from the EVM-visible number on L2s.
93    pub fn number(&self) -> BlockNumber {
94        self.block.header.number()
95    }
96
97    /// Returns the fork block hash.
98    pub fn hash(&self) -> B256 {
99        self.block.header.hash
100    }
101
102    /// Returns the endpoint and execution context observed when the fork was prepared.
103    pub const fn context(&self) -> ForkContext {
104        self.context
105    }
106
107    /// Returns an exact state selector that remains valid for a retained noncanonical block.
108    pub fn exact_block_id(&self) -> BlockId {
109        BlockId::from((self.hash(), Some(false)))
110    }
111
112    /// Returns the state selector, honoring the RPC compatibility opt-in.
113    pub(crate) fn state_block_id(&self) -> BlockId {
114        if self.state_by_number { BlockId::number(self.number()) } else { self.exact_block_id() }
115    }
116
117    pub(crate) fn block(&self) -> BlockNumHash {
118        BlockNumHash::new(self.number(), self.hash())
119    }
120
121    /// Reuses the configured RPC client with the response types required by the caller.
122    pub(crate) fn provider<N: Network>(&self) -> RootProvider<N> {
123        RootProvider::new(self.client.clone())
124    }
125
126    pub(crate) async fn at_block(&self, block: BlockNumHash) -> eyre::Result<Self> {
127        let provider = self.provider::<AnyNetwork>();
128        let response = provider
129            .get_block_by_hash(block.hash)
130            .await?
131            .ok_or_eyre("exact fork block is unavailable")?;
132        eyre::ensure!(
133            response.header.number() == block.number && response.header.hash == block.hash,
134            "exact fork block does not match the requested number and hash"
135        );
136        let mut fork = self.clone();
137        fork.selector = Some(block.number);
138        fork.block = Arc::new(response);
139        fork.state_by_number = false;
140        fork.context.block_number = block.number;
141        Ok(fork)
142    }
143
144    pub(crate) const fn source_id(&self) -> B256 {
145        self.source_id
146    }
147
148    /// Returns the stable, redacted identity used by persisted execution caches.
149    pub fn fingerprint(&self) -> B256 {
150        let mut encoded = serde_json::to_vec(&(
151            "foundry-resolved-fork-v1",
152            self.source_id,
153            self.block(),
154            self.context,
155        ))
156        .expect("fork identity is serializable");
157        // Distinguish execution policies without invalidating existing hash-based disk caches.
158        if self.state_by_number {
159            encoded.extend_from_slice(b"state-by-number");
160        }
161        keccak256(encoded)
162    }
163}
164
165impl PartialEq for Fork {
166    fn eq(&self, other: &Self) -> bool {
167        (self.source_id, self.selector, self.block(), self.context, self.state_by_number)
168            == (
169                other.source_id,
170                other.selector,
171                other.block(),
172                other.context,
173                other.state_by_number,
174            )
175    }
176}
177
178impl Eq for Fork {}
179
180impl Hash for Fork {
181    fn hash<H: Hasher>(&self, state: &mut H) {
182        (self.source_id, self.selector, self.block(), self.context, self.state_by_number)
183            .hash(state);
184    }
185}
186
187impl fmt::Debug for Fork {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        let mut debug = f.debug_struct("Fork");
190        debug.field("source", &"<redacted>");
191        if let Some(number) = self.selector {
192            debug.field("selector", &number);
193        } else {
194            debug.field("selector", &"latest");
195        }
196        debug.field("block", &self.block()).finish()
197    }
198}
199
200fn source_id(url: &str, headers: Option<&[String]>, jwt: Option<&str>) -> B256 {
201    let mut encoded = Vec::from(b"foundry-resolved-fork-source-v1".as_slice());
202    encode_source_part(&mut encoded, url.as_bytes());
203    let headers = headers.unwrap_or_default();
204    encoded.extend_from_slice(
205        &u64::try_from(headers.len()).expect("fork header count exceeds u64").to_be_bytes(),
206    );
207    for header in headers {
208        encode_source_part(&mut encoded, header.as_bytes());
209    }
210    if let Some(jwt) = jwt {
211        encoded.push(1);
212        encode_source_part(&mut encoded, jwt.as_bytes());
213    } else {
214        encoded.push(0);
215    }
216    keccak256(encoded)
217}
218
219fn encode_source_part(encoded: &mut Vec<u8>, part: &[u8]) {
220    encoded.extend_from_slice(
221        &u64::try_from(part.len()).expect("source identity part length exceeds u64").to_be_bytes(),
222    );
223    encoded.extend_from_slice(part);
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use alloy_network::{AnyHeader, AnyRpcHeader};
230    use alloy_rpc_types::Block;
231    use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
232    use serde_json::json;
233    use std::collections::HashSet;
234
235    impl Fork {
236        pub(crate) fn test(
237            url: &str,
238            headers: Option<&[String]>,
239            jwt: Option<&str>,
240            selector: Option<BlockNumber>,
241            block: BlockNumHash,
242            context: ForkContext,
243        ) -> Self {
244            let opts = EvmOpts {
245                fork_url: Some(url.to_string()),
246                fork_headers: headers.map(<[String]>::to_vec),
247                rpc_jwt: jwt.map(str::to_string),
248                fork_block_number: selector,
249                ..Default::default()
250            };
251            let header = AnyHeader { number: block.number, ..Default::default() };
252            let block = AnyRpcBlock::new(
253                Block::new(AnyRpcHeader::from_sealed(header.seal(block.hash)), Default::default())
254                    .into(),
255            );
256            Self::new(&opts, opts.fork_rpc_client(url).unwrap(), block, context)
257        }
258    }
259
260    fn context(block_number: BlockNumber) -> ForkContext {
261        ForkContext {
262            execution_chain_id: 1,
263            source_chain_id: 1,
264            network: NetworkVariant::Ethereum,
265            network_profile: NetworkConfigs::default(),
266            block_number,
267            hardfork: None,
268            instance_id: None,
269            source_fork_block_number: None,
270            source_fork_block_hash: None,
271        }
272    }
273
274    #[test]
275    fn exact_block_id_serializes_as_eip_1898_object() {
276        let hash = B256::with_last_byte(1);
277        let fork = Fork::test(
278            "http://localhost:8545",
279            None,
280            None,
281            None,
282            BlockNumHash::new(1, hash),
283            context(1),
284        );
285
286        assert_eq!(
287            serde_json::to_value(fork.exact_block_id()).unwrap(),
288            json!({
289                "blockHash": hash,
290                "requireCanonical": false,
291            })
292        );
293    }
294
295    #[test]
296    fn fork_state_by_number_preserves_anchor_and_separates_identity() {
297        let block = BlockNumHash::new(42, B256::with_last_byte(1));
298        let exact = Fork::test("http://localhost:8545", None, None, None, block, context(42));
299        let mut numbered = exact.clone();
300        numbered.state_by_number = true;
301        assert_eq!(serde_json::to_value(numbered.state_block_id()).unwrap(), json!("0x2a"));
302        assert_eq!(exact.state_block_id(), exact.exact_block_id());
303        assert_eq!(numbered.exact_block_id(), exact.exact_block_id());
304        assert_ne!(numbered, exact);
305        assert_eq!(numbered.source_id(), exact.source_id());
306        assert_ne!(numbered.fingerprint(), exact.fingerprint());
307    }
308
309    #[test]
310    fn endpoint_identity_participates_in_equality_and_hashing() {
311        let block = BlockNumHash::new(1, B256::with_last_byte(1));
312        let first = Fork::test("http://localhost:8545", None, None, None, block, context(1));
313        for changed_context in [
314            ForkContext { instance_id: Some(B256::with_last_byte(2)), ..context(1) },
315            ForkContext { network_profile: NetworkConfigs::with_celo(), ..context(1) },
316            ForkContext {
317                network: NetworkVariant::Tempo,
318                network_profile: NetworkConfigs::with_tempo(),
319                ..context(1)
320            },
321        ] {
322            let second =
323                Fork::test("http://localhost:8545", None, None, None, block, changed_context);
324
325            assert_ne!(first, second);
326            assert_ne!(first.fingerprint(), second.fingerprint());
327            assert_eq!(HashSet::from([first.clone(), second]).len(), 2);
328        }
329    }
330
331    #[test]
332    fn configured_source_identity_is_unambiguous() {
333        let block = BlockNumHash::new(1, B256::with_last_byte(1));
334        let context = context(1);
335        let plain = Fork::test("http://localhost:8545", None, None, None, block, context);
336        let header = Fork::test(
337            "http://localhost:8545",
338            Some(&["secret".to_string()]),
339            None,
340            None,
341            block,
342            context,
343        );
344        let jwt = Fork::test("http://localhost:8545", None, Some("secret"), None, block, context);
345
346        assert_ne!(plain.source_id, header.source_id);
347        assert_ne!(plain.source_id, jwt.source_id);
348        assert_ne!(header.source_id, jwt.source_id);
349        assert_ne!(plain.fingerprint(), header.fingerprint());
350        assert_ne!(plain.fingerprint(), jwt.fingerprint());
351    }
352
353    #[test]
354    fn resolved_fork_canonicalizes_equivalent_ethereum_profiles() {
355        let url = "http://localhost:8545";
356        let block = BlockNumHash::new(1, B256::with_last_byte(1));
357        let implicit = Fork::test(url, None, None, Some(1), block, context(1));
358        let explicit = Fork::test(
359            url,
360            None,
361            None,
362            Some(1),
363            block,
364            ForkContext { network_profile: NetworkConfigs::with_ethereum(), ..context(1) },
365        );
366
367        assert_eq!(implicit.context(), explicit.context());
368        assert_eq!(implicit.fingerprint(), explicit.fingerprint());
369        assert_eq!(HashSet::from([implicit, explicit]).len(), 1);
370    }
371}