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