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#[derive(Clone, Debug)]
25pub struct CreateFork {
26 pub enable_caching: bool,
28 pub url: String,
30 pub evm_opts: EvmOpts,
32}
33
34#[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 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 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 pub fn number(&self) -> BlockNumber {
94 self.block.header.number()
95 }
96
97 pub fn hash(&self) -> B256 {
99 self.block.header.hash
100 }
101
102 pub const fn context(&self) -> ForkContext {
104 self.context
105 }
106
107 pub fn exact_block_id(&self) -> BlockId {
109 BlockId::from((self.hash(), Some(false)))
110 }
111
112 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 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 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 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}