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#[derive(Clone, Debug)]
27pub struct CreateFork {
28 pub enable_caching: bool,
30 pub url: String,
32 pub evm_opts: EvmOpts,
34}
35
36#[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 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 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 pub fn number(&self) -> BlockNumber {
96 self.block.header.number()
97 }
98
99 pub fn hash(&self) -> B256 {
101 self.block.header.hash
102 }
103
104 pub const fn context(&self) -> ForkContext {
106 self.context
107 }
108
109 pub fn exact_block_id(&self) -> BlockId {
111 BlockId::from((self.hash(), Some(false)))
112 }
113
114 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 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 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 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}