Skip to main content

evm_fork_cache/
bulk_storage.rs

1//! Bulk storage extraction over `eth_call` state overrides.
2//!
3//! The default [`StorageBatchFetchFn`] issues one `eth_getStorageAt` per slot
4//! (JSON-RPC-batched, but still one *billed* request per slot — 20 CU each on
5//! Alchemy). This module implements the "bulk storage extraction" technique
6//! described by Dedaub, which packs thousands of slot reads into a **single**
7//! `eth_call` (26 CU on Alchemy, flat):
8//!
9//! - blog: <https://dedaub.com/blog/bulk-storage-extraction/>
10//! - reference implementation: <https://github.com/Dedaub/storage-extractor>
11//!
12//! # Mechanism
13//!
14//! `eth_call` accepts a *state-override set* that can replace the **code** at
15//! any address while leaving its **storage** intact. We override the target
16//! contract with a 23-byte handwritten extractor ([`STORAGE_EXTRACTOR_CODE`],
17//! Dedaub's bytecode, credited above) that treats calldata as a raw array of
18//! 32-byte slot keys, `SLOAD`s each one, and returns the packed values —
19//! no function selector, no ABI:
20//!
21//! ```text
22//! [00] PUSH0            counter = 0
23//! [01] JUMPDEST         loop:
24//! [02] DUP1 CALLDATASIZE EQ
25//! [05] PUSH1 0x13 JUMPI   -> exit when counter == calldatasize
26//! [08] DUP1 CALLDATALOAD  slot key at calldata[counter]
27//! [0a] SLOAD
28//! [0b] DUP2 MSTORE        mem[counter] = value (counter doubles as mem offset)
29//! [0d] PUSH1 0x20 ADD     counter += 32
30//! [10] PUSH1 0x01 JUMP
31//! [13] JUMPDEST CALLDATASIZE PUSH0 RETURN
32//! ```
33//!
34//! Marginal cost is ~2,664 gas per slot (cold `SLOAD` 2,100 + calldata ~510 +
35//! loop ~30 + memory), so a default 50M-gas `eth_call` fits ~18,500 slots.
36//! [`BulkCallConfig::max_slots_per_call`] defaults to a conservative 10,000
37//! (~27M gas), splitting larger requests across concurrent calls.
38//!
39//! # Multi-contract batches
40//!
41//! `SLOAD` reads the storage of the *executing* contract, so each target must
42//! run the extractor at its own address. To read many contracts in one round
43//! trip we additionally override [`MULTICALL3_ADDRESS`] with the canonical
44//! Multicall3 runtime ([`multicall3_runtime_code`]) and dispatch one
45//! `aggregate3` call whose subcalls hit each overridden target. Overriding the
46//! dispatcher code unconditionally makes the scheme work on chains — and at
47//! historical blocks — where Multicall3 is not deployed.
48//!
49//! # Semantics & caveats
50//!
51//! - Results are identical to `eth_getStorageAt` at the same block: absent
52//!   slots (and slots of code-less accounts) read as zero.
53//! - The provider must support the state-override parameter of `eth_call`
54//!   (Geth-lineage nodes, Reth, Erigon, and the major hosted providers all
55//!   do). Providers that reject it surface a per-slot error; install a
56//!   fallback via [`bulk_call_storage_fetcher_with_fallback`] to repair those
57//!   with classic point reads.
58//! - True precompile addresses (`0x01..=0x11` on mainnet) execute the
59//!   precompile regardless of a code override; slots requested there fail the
60//!   response-length check and surface as errors (repaired by the fallback
61//!   when configured) rather than silently returning garbage.
62//! - [`STORAGE_EXTRACTOR_CODE`] uses `PUSH0` (Shanghai). For pre-Shanghai
63//!   chains set [`BulkCallConfig::pre_shanghai_extractor`] to use the
64//!   equivalent [`STORAGE_EXTRACTOR_CODE_PRE_SHANGHAI`].
65//!
66//! # Wiring it into a cache
67//!
68//! **Since 0.2.0 this is every provider-backed cache's default storage
69//! fetcher** — no wiring needed. Tune it with
70//! [`EvmCacheBuilder::bulk_call_config`](crate::cache::EvmCacheBuilder::bulk_call_config),
71//! opt out with
72//! [`StorageFetchStrategy::PointRead`](crate::cache::StorageFetchStrategy::PointRead),
73//! or compose it manually as below (e.g. over a custom fallback):
74//!
75//! ```no_run
76//! # use std::sync::Arc;
77//! # use alloy_provider::{ProviderBuilder, network::AnyNetwork};
78//! # use evm_fork_cache::cache::EvmCache;
79//! # use evm_fork_cache::bulk_storage::{BulkCallConfig, bulk_call_storage_fetcher_with_fallback};
80//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
81//! let provider = Arc::new(
82//!     ProviderBuilder::new()
83//!         .network::<AnyNetwork>()
84//!         .connect_http("https://example-rpc.invalid".parse()?),
85//! );
86//! let mut cache = EvmCache::builder(provider.clone()).build().await;
87//!
88//! // Keep the default point-read fetcher as a repair path, then route all
89//! // batch storage fetches through call-override bulk extraction.
90//! let fallback = cache
91//!     .storage_batch_fetcher()
92//!     .cloned()
93//!     .expect("provider-backed cache has a default fetcher");
94//! cache.set_storage_batch_fetcher(bulk_call_storage_fetcher_with_fallback(
95//!     provider,
96//!     BulkCallConfig::default(),
97//!     fallback,
98//! ));
99//! # Ok(())
100//! # }
101//! ```
102
103use std::collections::HashMap;
104use std::sync::{Arc, OnceLock};
105
106use alloy_eips::BlockId;
107use alloy_primitives::{Address, B256, Bytes, U256, hex};
108use alloy_provider::Provider;
109use alloy_provider::network::AnyNetwork;
110use alloy_rpc_types_eth::TransactionRequest;
111use alloy_rpc_types_eth::state::{AccountOverride, StateOverride};
112use alloy_sol_types::SolCall;
113use futures::stream::{self, StreamExt};
114use tracing::{debug, warn};
115
116use crate::cache::{StorageBatchFetchFn, block_in_place_handle};
117use crate::errors::{StorageFetchError, StorageFetchResult};
118use crate::multicall::{IMulticall3, MULTICALL3_ADDRESS};
119
120/// Dedaub's 23-byte storage extractor (see the module docs for the annotated
121/// disassembly). Calldata is a contiguous array of 32-byte slot keys; the
122/// return data is the corresponding array of 32-byte values. Requires
123/// `PUSH0` (Shanghai).
124///
125/// Source: <https://github.com/Dedaub/storage-extractor> (`extractor.hex`).
126pub const STORAGE_EXTRACTOR_CODE: &[u8] = &hex!("5f5b80361460135780355481526020016001565b365ff3");
127
128/// [`STORAGE_EXTRACTOR_CODE`] with both `PUSH0`s replaced by `PUSH1 0x00`
129/// (jump targets re-pointed), for chains that have not activated Shanghai.
130pub const STORAGE_EXTRACTOR_CODE_PRE_SHANGHAI: &[u8] =
131    &hex!("60005b80361460145780355481526020016002565b366000f3");
132
133/// Runtime bytecode of Multicall3 (`0xcA11bde05977b3631167028862bE2a173976CA11`),
134/// as deployed on Ethereum mainnet. Injected as a code override at
135/// [`MULTICALL3_ADDRESS`] for multi-contract extraction so the dispatcher
136/// exists on every chain and at every historical block.
137///
138/// The fixture was fetched via `eth_getCode` and verified byte-identical
139/// across independent providers; `fixtures/README.md` records provenance.
140pub fn multicall3_runtime_code() -> &'static Bytes {
141    static CODE: OnceLock<Bytes> = OnceLock::new();
142    CODE.get_or_init(|| {
143        let raw = include_str!("../fixtures/multicall3_runtime.hex");
144        Bytes::from(hex::decode(raw.trim()).expect("valid multicall3 runtime hex fixture"))
145    })
146}
147
148/// How planned extraction chunks are shipped to the provider.
149#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
150pub enum CallDispatch {
151    /// One `eth_call` per planned chunk. Universally supported; chunks run
152    /// concurrently up to [`BulkCallConfig::max_concurrent_calls`]. The
153    /// default.
154    #[default]
155    PerCall,
156    /// Ship many chunks as the transactions of a single `eth_callMany`
157    /// bundle (Erigon-lineage providers, including Alchemy — where it costs
158    /// 20 CU per *request* vs 26 per `eth_call`). Requests are bounded by
159    /// [`BulkCallConfig::max_slots_per_request`]; a request-level failure
160    /// (e.g. the method is unsupported) transparently re-dispatches that
161    /// request's chunks per-call. Hash-pinned blocks always dispatch
162    /// per-call (`eth_callMany` takes a number/tag block context).
163    CallMany,
164}
165
166/// Tuning knobs for the call-override bulk storage fetcher.
167///
168/// The defaults target Geth-default RPC limits (50M gas per `eth_call`):
169/// 10,000 slots ≈ 27M gas, comfortably under the cap while leaving headroom
170/// for multicall dispatch overhead. Providers with higher caps can raise
171/// `max_slots_per_call` substantially (measure before relying on it —
172/// Alchemy accepted 30k slots/call in testing, bounded by request body size
173/// rather than gas; see `docs/bulk-storage-extraction.md`).
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct BulkCallConfig {
176    /// Maximum storage slots packed into one `eth_call` (across all targets
177    /// in that call). ~2,664 gas per slot; keep the product under the
178    /// provider's `eth_call` gas cap.
179    pub max_slots_per_call: usize,
180    /// Maximum distinct target contracts dispatched through one multicall
181    /// (~8k gas of call/ABI overhead per target).
182    pub max_targets_per_call: usize,
183    /// Maximum `eth_call`s in flight at once when a request spans multiple
184    /// calls.
185    pub max_concurrent_calls: usize,
186    /// Requests with fewer than this many slots are routed to the fallback
187    /// fetcher when one is installed (an `eth_call` costs slightly more than
188    /// a single `eth_getStorageAt` on CU-metered providers). Ignored when no
189    /// fallback is available.
190    pub point_read_threshold: usize,
191    /// Use the `PUSH0`-free extractor for chains without Shanghai.
192    pub pre_shanghai_extractor: bool,
193    /// How chunks are shipped: one `eth_call` each, or batched through
194    /// `eth_callMany`.
195    pub dispatch: CallDispatch,
196    /// [`CallDispatch::CallMany`] only: maximum total slots per
197    /// `eth_callMany` request. Slot keys are incompressible calldata
198    /// (~64 bytes each in the JSON body), and providers cap request bodies —
199    /// Alchemy rejects ~2.5 MB with HTTP 413. The default (25,000 ≈ 1.6 MB)
200    /// stays inside that.
201    pub max_slots_per_request: usize,
202}
203
204impl Default for BulkCallConfig {
205    fn default() -> Self {
206        Self {
207            max_slots_per_call: 10_000,
208            max_targets_per_call: 250,
209            max_concurrent_calls: 4,
210            point_read_threshold: 2,
211            pre_shanghai_extractor: false,
212            dispatch: CallDispatch::PerCall,
213            max_slots_per_request: 25_000,
214        }
215    }
216}
217
218impl BulkCallConfig {
219    fn normalized(self) -> Self {
220        Self {
221            max_slots_per_call: self.max_slots_per_call.max(1),
222            max_targets_per_call: self.max_targets_per_call.max(1),
223            max_concurrent_calls: self.max_concurrent_calls.max(1),
224            max_slots_per_request: self.max_slots_per_request.max(1),
225            ..self
226        }
227    }
228
229    fn extractor(&self) -> Bytes {
230        if self.pre_shanghai_extractor {
231            Bytes::from_static(STORAGE_EXTRACTOR_CODE_PRE_SHANGHAI)
232        } else {
233            Bytes::from_static(STORAGE_EXTRACTOR_CODE)
234        }
235    }
236}
237
238/// Pack slot keys into extractor calldata: the raw concatenation of each
239/// key's 32-byte big-endian representation (no selector, no ABI).
240pub fn pack_slots_calldata(slots: &[U256]) -> Bytes {
241    let mut out = Vec::with_capacity(slots.len() * 32);
242    for slot in slots {
243        out.extend_from_slice(&slot.to_be_bytes::<32>());
244    }
245    out.into()
246}
247
248/// Decode extractor return data (packed 32-byte words) into values.
249///
250/// Returns `None` when the payload is not exactly `expected` words — the
251/// signature of a call that did not actually execute the extractor (e.g. a
252/// provider that ignored the code override, or a precompile target).
253pub fn decode_packed_values(data: &[u8], expected: usize) -> Option<Vec<U256>> {
254    if data.len() != expected * 32 {
255        return None;
256    }
257    Some(data.chunks_exact(32).map(U256::from_be_slice).collect())
258}
259
260/// ABI-encode one `aggregate3` dispatch whose subcalls run the extractor at
261/// each `(target, slots)` pair. Subcalls use `allowFailure = true` so one
262/// failing target degrades to per-target errors instead of reverting the
263/// whole batch.
264pub fn encode_multi_target_calldata(targets: &[(Address, Vec<U256>)]) -> Bytes {
265    let calls: Vec<IMulticall3::Call3> = targets
266        .iter()
267        .map(|(target, slots)| IMulticall3::Call3 {
268            target: *target,
269            allowFailure: true,
270            callData: pack_slots_calldata(slots),
271        })
272        .collect();
273    IMulticall3::aggregate3Call { calls }.abi_encode().into()
274}
275
276/// Decode an `aggregate3` response produced by [`encode_multi_target_calldata`]
277/// back into one result tuple per requested `(target, slot)` pair.
278pub fn decode_multi_target_response(
279    targets: &[(Address, Vec<U256>)],
280    response: &[u8],
281) -> Vec<(Address, U256, StorageFetchResult<U256>)> {
282    let decoded = match IMulticall3::aggregate3Call::abi_decode_returns(response) {
283        Ok(results) if results.len() == targets.len() => results,
284        Ok(results) => {
285            return per_target_errors(targets, || {
286                StorageFetchError::custom(format!(
287                    "aggregate3 returned {} results for {} extraction targets",
288                    results.len(),
289                    targets.len()
290                ))
291            });
292        }
293        Err(e) => {
294            return per_target_errors(targets, || {
295                StorageFetchError::custom(format!("failed to decode aggregate3 response: {e}"))
296            });
297        }
298    };
299
300    let mut out = Vec::with_capacity(targets.iter().map(|(_, s)| s.len()).sum());
301    for ((target, slots), result) in targets.iter().zip(decoded) {
302        if !result.success {
303            out.extend(slots.iter().map(|slot| {
304                (
305                    *target,
306                    *slot,
307                    Err(StorageFetchError::custom(
308                        "extractor subcall failed (allowFailure=true); the target may be a precompile",
309                    )),
310                )
311            }));
312            continue;
313        }
314        match decode_packed_values(&result.returnData, slots.len()) {
315            Some(values) => out.extend(
316                slots
317                    .iter()
318                    .zip(values)
319                    .map(|(slot, value)| (*target, *slot, Ok(value))),
320            ),
321            None => out.extend(slots.iter().map(|slot| {
322                (
323                    *target,
324                    *slot,
325                    Err(StorageFetchError::custom(format!(
326                        "extractor at {target} returned {} bytes, expected {}",
327                        result.returnData.len(),
328                        slots.len() * 32
329                    ))),
330                )
331            })),
332        }
333    }
334    out
335}
336
337fn per_target_errors(
338    targets: &[(Address, Vec<U256>)],
339    make: impl Fn() -> StorageFetchError,
340) -> Vec<(Address, U256, StorageFetchResult<U256>)> {
341    targets
342        .iter()
343        .flat_map(|(target, slots)| slots.iter().map(|slot| (*target, *slot, Err(make()))))
344        .collect()
345}
346
347/// One planned `eth_call`.
348#[derive(Debug, Clone, PartialEq, Eq)]
349enum CallPlan {
350    /// Direct extractor call: `to = target`, calldata = packed slots.
351    Single { target: Address, slots: Vec<U256> },
352    /// Multicall3 dispatch across several targets, each running the extractor.
353    Multi { targets: Vec<(Address, Vec<U256>)> },
354}
355
356impl CallPlan {
357    fn request_slot_count(&self) -> usize {
358        match self {
359            Self::Single { slots, .. } => slots.len(),
360            Self::Multi { targets } => targets.iter().map(|(_, s)| s.len()).sum(),
361        }
362    }
363}
364
365/// Split requests into `eth_call`-sized plans.
366///
367/// Groups by address in first-seen order; targets with more than
368/// `max_slots_per_call` slots are split into dedicated single-target calls,
369/// and the remaining small groups are greedily packed into multicall
370/// dispatches bounded by both the slot and target budgets. A target equal to
371/// [`MULTICALL3_ADDRESS`] always gets a dedicated call so its code override
372/// cannot collide with the dispatcher's.
373fn plan_calls(requests: &[(Address, U256)], config: &BulkCallConfig) -> Vec<CallPlan> {
374    let mut order: Vec<Address> = Vec::new();
375    let mut groups: HashMap<Address, Vec<U256>> = HashMap::new();
376    for (address, slot) in requests {
377        groups
378            .entry(*address)
379            .or_insert_with(|| {
380                order.push(*address);
381                Vec::new()
382            })
383            .push(*slot);
384    }
385
386    let mut plans = Vec::new();
387    let mut packable: Vec<(Address, Vec<U256>)> = Vec::new();
388    for address in order {
389        let slots = groups.remove(&address).expect("grouped above");
390        for chunk in slots.chunks(config.max_slots_per_call) {
391            let full = chunk.len() == config.max_slots_per_call;
392            // The dispatcher address must never share a multicall with other
393            // targets: its extractor override would clobber the dispatcher
394            // code override at the same key. Only the final chunk of a target
395            // can be partial, so at most one packable remainder per target.
396            if full || address == MULTICALL3_ADDRESS {
397                plans.push(CallPlan::Single {
398                    target: address,
399                    slots: chunk.to_vec(),
400                });
401            } else {
402                packable.push((address, chunk.to_vec()));
403            }
404        }
405    }
406
407    // Greedily pack the small per-target remainders into multicall dispatches.
408    let mut current: Vec<(Address, Vec<U256>)> = Vec::new();
409    let mut current_slots = 0usize;
410    let flush =
411        |current: &mut Vec<(Address, Vec<U256>)>, plans: &mut Vec<CallPlan>| match current.len() {
412            0 => {}
413            1 => {
414                let (target, slots) = current.pop().expect("len checked");
415                plans.push(CallPlan::Single { target, slots });
416            }
417            _ => plans.push(CallPlan::Multi {
418                targets: std::mem::take(current),
419            }),
420        };
421    for (address, slots) in packable {
422        let would_overflow = current_slots + slots.len() > config.max_slots_per_call
423            || current.len() >= config.max_targets_per_call;
424        if !current.is_empty() && would_overflow {
425            flush(&mut current, &mut plans);
426            current_slots = 0;
427        }
428        current_slots += slots.len();
429        current.push((address, slots));
430    }
431    flush(&mut current, &mut plans);
432
433    plans
434}
435
436/// Build the state-override set for one plan: the extractor at every target,
437/// plus the Multicall3 runtime at the dispatcher for multi-target plans.
438fn overrides_for_plan(plan: &CallPlan, extractor: &Bytes) -> StateOverride {
439    let mut overrides = StateOverride::default();
440    match plan {
441        CallPlan::Single { target, .. } => {
442            overrides.insert(
443                *target,
444                AccountOverride::default().with_code(extractor.clone()),
445            );
446        }
447        CallPlan::Multi { targets } => {
448            overrides.insert(
449                MULTICALL3_ADDRESS,
450                AccountOverride::default().with_code(multicall3_runtime_code().clone()),
451            );
452            for (target, _) in targets {
453                overrides.insert(
454                    *target,
455                    AccountOverride::default().with_code(extractor.clone()),
456                );
457            }
458        }
459    }
460    overrides
461}
462
463/// The `to` address and calldata for one planned call.
464fn plan_call_parts(plan: &CallPlan) -> (Address, Bytes) {
465    match plan {
466        CallPlan::Single { target, slots } => (*target, pack_slots_calldata(slots)),
467        CallPlan::Multi { targets } => (MULTICALL3_ADDRESS, encode_multi_target_calldata(targets)),
468    }
469}
470
471/// Decode one plan's successful call output into per-slot results.
472fn decode_plan_response(
473    plan: &CallPlan,
474    bytes: &[u8],
475) -> Vec<(Address, U256, StorageFetchResult<U256>)> {
476    match plan {
477        CallPlan::Single { target, slots } => match decode_packed_values(bytes, slots.len()) {
478            Some(values) => slots
479                .iter()
480                .zip(values)
481                .map(|(slot, value)| (*target, *slot, Ok(value)))
482                .collect(),
483            None => slots
484                .iter()
485                .map(|slot| {
486                    (
487                        *target,
488                        *slot,
489                        Err(StorageFetchError::custom(format!(
490                            "extractor at {target} returned {} bytes, expected {} — the \
491                             provider may not support eth_call state overrides, or the \
492                             target is a precompile",
493                            bytes.len(),
494                            slots.len() * 32
495                        ))),
496                    )
497                })
498                .collect(),
499        },
500        CallPlan::Multi { targets } => decode_multi_target_response(targets, bytes),
501    }
502}
503
504/// Report `err` for every slot the plan covers.
505fn plan_error_results(
506    plan: &CallPlan,
507    err: StorageFetchError,
508) -> Vec<(Address, U256, StorageFetchResult<U256>)> {
509    match plan {
510        CallPlan::Single { target, slots } => slots
511            .iter()
512            .map(|slot| (*target, *slot, Err(err.clone())))
513            .collect(),
514        CallPlan::Multi { targets } => targets
515            .iter()
516            .flat_map(|(target, slots)| {
517                slots.iter().map({
518                    let err = err.clone();
519                    move |slot| (*target, *slot, Err(err.clone()))
520                })
521            })
522            .collect(),
523    }
524}
525
526async fn execute_plan<P: Provider<AnyNetwork>>(
527    provider: &P,
528    block: BlockId,
529    plan: CallPlan,
530    extractor: &Bytes,
531) -> Vec<(Address, U256, StorageFetchResult<U256>)> {
532    let overrides = overrides_for_plan(&plan, extractor);
533    let (to, data) = plan_call_parts(&plan);
534    let tx = TransactionRequest::default().to(to).input(data.into());
535
536    let response: Result<Bytes, _> = provider
537        .client()
538        .request("eth_call", (tx, block, overrides))
539        .await;
540
541    match response {
542        Ok(bytes) => decode_plan_response(&plan, &bytes),
543        Err(e) => plan_error_results(&plan, StorageFetchError::provider("eth_call", &e)),
544    }
545}
546
547/// One entry of an `eth_callMany` response: `{"value": "0x.."}` on success,
548/// `{"error": ..}` on per-transaction failure (Erigon-style).
549#[derive(Debug, serde::Deserialize)]
550struct CallManyEntry {
551    value: Option<Bytes>,
552    error: Option<serde_json::Value>,
553}
554
555/// Execute several plans as the transactions of one `eth_callMany` bundle.
556///
557/// Returns `Err` only for request-level failures (method unsupported,
558/// transport error, malformed response) so the caller can re-dispatch the
559/// same plans per-call; per-transaction failures are mapped to per-slot
560/// errors in the `Ok` payload.
561async fn execute_plans_call_many<P: Provider<AnyNetwork>>(
562    provider: &P,
563    number: alloy_eips::BlockNumberOrTag,
564    plans: &[CallPlan],
565    extractor: &Bytes,
566) -> Result<Vec<(Address, U256, StorageFetchResult<U256>)>, StorageFetchError> {
567    // One shared override map across every plan in the bundle. Merging is
568    // safe: every target maps to the extractor and the dispatcher maps to
569    // Multicall3; plans targeting the dispatcher itself are routed per-call
570    // by the caller.
571    let mut overrides = StateOverride::default();
572    let mut transactions = Vec::with_capacity(plans.len());
573    for plan in plans {
574        for (address, account) in overrides_for_plan(plan, extractor) {
575            overrides.insert(address, account);
576        }
577        let (to, data) = plan_call_parts(plan);
578        transactions.push(serde_json::json!({ "to": to, "data": data }));
579    }
580
581    let bundles = serde_json::json!([{ "transactions": transactions }]);
582    let context = serde_json::json!({ "blockNumber": number, "transactionIndex": -1 });
583    let response: Vec<Vec<CallManyEntry>> = provider
584        .client()
585        .request("eth_callMany", (bundles, context, overrides))
586        .await
587        .map_err(|e| StorageFetchError::provider("eth_callMany", &e))?;
588
589    let entries: Vec<CallManyEntry> = response.into_iter().flatten().collect();
590    if entries.len() != plans.len() {
591        return Err(StorageFetchError::custom(format!(
592            "eth_callMany returned {} results for {} bundled calls",
593            entries.len(),
594            plans.len()
595        )));
596    }
597
598    let mut out = Vec::new();
599    for (plan, entry) in plans.iter().zip(entries) {
600        match entry.value {
601            Some(bytes) => out.extend(decode_plan_response(plan, &bytes)),
602            None => {
603                let detail = entry
604                    .error
605                    .map(|e| e.to_string())
606                    .unwrap_or_else(|| "no value returned".to_string());
607                out.extend(plan_error_results(
608                    plan,
609                    StorageFetchError::custom(format!("eth_callMany transaction failed: {detail}")),
610                ));
611            }
612        }
613    }
614    Ok(out)
615}
616
617/// Group plans into `eth_callMany` requests bounded by the per-request slot
618/// budget (the request *body* is the binding provider limit, not gas).
619fn group_plans_for_call_many(
620    plans: Vec<CallPlan>,
621    max_slots_per_request: usize,
622) -> Vec<Vec<CallPlan>> {
623    let mut requests: Vec<Vec<CallPlan>> = Vec::new();
624    let mut current: Vec<CallPlan> = Vec::new();
625    let mut current_slots = 0usize;
626    for plan in plans {
627        let slots = plan.request_slot_count();
628        if !current.is_empty() && current_slots + slots > max_slots_per_request {
629            requests.push(std::mem::take(&mut current));
630            current_slots = 0;
631        }
632        current_slots += slots;
633        current.push(plan);
634    }
635    if !current.is_empty() {
636        requests.push(current);
637    }
638    requests
639}
640
641/// Fetch storage slots in bulk via `eth_call` code overrides (async core).
642///
643/// Returns exactly one result tuple per requested `(address, slot)` pair
644/// (order not preserved, duplicates included), matching the
645/// [`StorageBatchFetchFn`] contract. Chunk-level failures (transport errors,
646/// providers without state-override support) surface as per-slot errors.
647///
648/// This is the direct entry point for async callers — e.g. loading an entire
649/// AMM pool's tick range during cold start — while
650/// [`bulk_call_storage_fetcher`] adapts it to the cache's synchronous fetcher
651/// seam.
652pub async fn fetch_slots_bulk<P: Provider<AnyNetwork>>(
653    provider: &P,
654    requests: Vec<(Address, U256)>,
655    block: BlockId,
656    config: BulkCallConfig,
657) -> Vec<(Address, U256, StorageFetchResult<U256>)> {
658    let config = config.normalized();
659    if requests.is_empty() {
660        return Vec::new();
661    }
662    let extractor = config.extractor();
663    let plans = plan_calls(&requests, &config);
664    debug!(
665        slots = requests.len(),
666        calls = plans.len(),
667        dispatch = ?config.dispatch,
668        "bulk storage extraction dispatch"
669    );
670
671    let extractor = &extractor;
672    // eth_callMany takes a number/tag block context; hash pins dispatch
673    // per-call instead.
674    let call_many_number = match (config.dispatch, block) {
675        (CallDispatch::CallMany, BlockId::Number(number)) => Some(number),
676        _ => None,
677    };
678
679    let Some(number) = call_many_number else {
680        let results: Vec<Vec<_>> = stream::iter(
681            plans
682                .into_iter()
683                .map(|plan| execute_plan(provider, block, plan, extractor)),
684        )
685        .buffer_unordered(config.max_concurrent_calls)
686        .collect()
687        .await;
688        return results.into_iter().flatten().collect();
689    };
690
691    // Plans targeting the dispatcher address itself cannot share a bundle's
692    // override map (their extractor override would clobber the dispatcher
693    // override); ship those per-call.
694    let (conflicting, bundleable): (Vec<_>, Vec<_>) = plans.into_iter().partition(
695        |plan| matches!(plan, CallPlan::Single { target, .. } if *target == MULTICALL3_ADDRESS),
696    );
697    let groups = group_plans_for_call_many(bundleable, config.max_slots_per_request);
698    let group_futs = groups.into_iter().map(|group| async move {
699        match execute_plans_call_many(provider, number, &group, extractor).await {
700            Ok(results) => results,
701            Err(e) => {
702                // Request-level failure (method unsupported, transport,
703                // malformed response): re-dispatch this request's chunks as
704                // plain eth_calls so the fetch still succeeds.
705                warn!(
706                    error = %e,
707                    chunks = group.len(),
708                    "eth_callMany dispatch failed; re-dispatching per-call"
709                );
710                let mut results = Vec::new();
711                for plan in group {
712                    results.extend(execute_plan(provider, block, plan, extractor).await);
713                }
714                results
715            }
716        }
717    });
718    let mut results: Vec<_> = stream::iter(group_futs)
719        .buffer_unordered(config.max_concurrent_calls)
720        .collect::<Vec<Vec<_>>>()
721        .await
722        .into_iter()
723        .flatten()
724        .collect();
725    for plan in conflicting {
726        results.extend(execute_plan(provider, block, plan, extractor).await);
727    }
728    results
729}
730
731/// Number of `eth_call`s a request set will be split into under `config`.
732///
733/// Useful for CU budgeting on metered providers: the bulk path costs
734/// `planned_call_count(..) × cost(eth_call)` (26 CU each on Alchemy) versus
735/// `requests.len() × cost(eth_getStorageAt)` (20 CU each) for point reads.
736pub fn planned_call_count(requests: &[(Address, U256)], config: &BulkCallConfig) -> usize {
737    plan_calls(requests, &config.normalized()).len()
738}
739
740/// A cheap, cloneable handle to a running bulk fetcher's fallback state.
741///
742/// Returned alongside the fetcher by [`bulk_call_storage_fetcher_with_status`];
743/// the handle and the fetcher closure share one counter, so polling the handle
744/// observes the live fetcher with no extra plumbing. Cloning is `Arc`-cheap and
745/// every clone reflects the same state.
746///
747/// It surfaces the one runtime **silent-degradation** signal the fetcher can
748/// hit: a bulk fetcher whose provider turns out not to support `eth_call` state
749/// overrides latches to its point-read fallback after
750/// [`latch_threshold`](Self::latch_threshold) consecutive batches in which
751/// *every* slot failed with a provider-level error (a `warn!` fires once at that
752/// point). Without observability a searcher can slide from "10,000 slots per
753/// call" to "10,000 point reads" and notice only via latency or CU spend; poll
754/// [`fallback_latched`](Self::fallback_latched) to alert on it directly.
755#[derive(Clone, Debug)]
756pub struct BulkFetcherStatus {
757    consecutive_failures: Arc<std::sync::atomic::AtomicUsize>,
758    latch_threshold: usize,
759}
760
761impl BulkFetcherStatus {
762    /// Consecutive batches so far in which **every** slot failed with a
763    /// provider-level error; any batch with at least one success resets it to
764    /// zero. Equals [`latch_threshold`](Self::latch_threshold) once the fetcher
765    /// has latched (and does not climb past it — latched batches skip the bulk
766    /// attempt, so the counter is not touched again).
767    pub fn consecutive_override_failures(&self) -> usize {
768        self.consecutive_failures
769            .load(std::sync::atomic::Ordering::Relaxed)
770    }
771
772    /// The number of consecutive all-provider-error batches that trips the latch.
773    pub fn latch_threshold(&self) -> usize {
774        self.latch_threshold
775    }
776
777    /// Whether the fetcher has latched to its point-read fallback. Sticky: once
778    /// `true` it stays `true` for the fetcher's lifetime — install a fresh
779    /// fetcher to retry bulk extraction. Only ever `true` for a fetcher built
780    /// with a fallback (via [`bulk_call_storage_fetcher_with_status`]); without
781    /// one there is nothing to latch to and the bulk path is always attempted.
782    pub fn fallback_latched(&self) -> bool {
783        self.consecutive_override_failures() >= self.latch_threshold
784    }
785}
786
787/// Build a [`StorageBatchFetchFn`] backed by call-override bulk extraction.
788///
789/// Install it with [`EvmCache::set_storage_batch_fetcher`](crate::cache::EvmCache::set_storage_batch_fetcher);
790/// every batch consumer (freshness verification, cold-start verify/probe,
791/// reactive point-read resyncs, prefetch) then loads storage through bulk
792/// `eth_call`s. Requires a multi-thread tokio runtime, like the default
793/// fetcher.
794///
795/// Failed slots are reported as errors; use
796/// [`bulk_call_storage_fetcher_with_fallback`] to repair them with classic
797/// point reads instead.
798pub fn bulk_call_storage_fetcher<P: Provider<AnyNetwork> + 'static>(
799    provider: Arc<P>,
800    config: BulkCallConfig,
801) -> StorageBatchFetchFn {
802    make_fetcher(provider, config, None).0
803}
804
805/// [`bulk_call_storage_fetcher`] with a repair path.
806///
807/// `fallback` (typically the cache's default point-read fetcher, obtained via
808/// [`EvmCache::storage_batch_fetcher`](crate::cache::EvmCache::storage_batch_fetcher)
809/// before replacing it) is invoked for:
810/// - requests smaller than [`BulkCallConfig::point_read_threshold`], where a
811///   point read is cheaper than an `eth_call` on CU-metered providers; and
812/// - any pairs the bulk path reported as errors (provider without
813///   state-override support, precompile targets, transport failures).
814pub fn bulk_call_storage_fetcher_with_fallback<P: Provider<AnyNetwork> + 'static>(
815    provider: Arc<P>,
816    config: BulkCallConfig,
817    fallback: StorageBatchFetchFn,
818) -> StorageBatchFetchFn {
819    make_fetcher(provider, config, Some(fallback)).0
820}
821
822/// [`bulk_call_storage_fetcher_with_fallback`] that also returns a
823/// [`BulkFetcherStatus`] handle for observing the fallback latch at runtime.
824///
825/// Use this instead of [`bulk_call_storage_fetcher_with_fallback`] when you want
826/// to alert on silent bulk→point-read degradation (e.g. a provider that stops
827/// honoring state overrides). Keep the returned handle — or a clone — and poll
828/// [`BulkFetcherStatus::fallback_latched`]; treat `true` as "bulk extraction is
829/// off, expect higher latency/CU until a fresh fetcher is installed". The
830/// fetcher itself behaves identically to the plain fallback constructor.
831pub fn bulk_call_storage_fetcher_with_status<P: Provider<AnyNetwork> + 'static>(
832    provider: Arc<P>,
833    config: BulkCallConfig,
834    fallback: StorageBatchFetchFn,
835) -> (StorageBatchFetchFn, BulkFetcherStatus) {
836    make_fetcher(provider, config, Some(fallback))
837}
838
839fn make_fetcher<P: Provider<AnyNetwork> + 'static>(
840    provider: Arc<P>,
841    config: BulkCallConfig,
842    fallback: Option<StorageBatchFetchFn>,
843) -> (StorageBatchFetchFn, BulkFetcherStatus) {
844    let config = config.normalized();
845    // After this many *consecutive* batches where every slot failed with a
846    // provider-level error (the signature of an endpoint without
847    // state-override support), stop attempting bulk extraction and route
848    // straight to the fallback. Sticky for the fetcher's lifetime — install a
849    // fresh fetcher to retry bulk extraction. Only meaningful when a fallback
850    // exists; without one the bulk attempt is the only option anyway.
851    const OVERRIDE_FAILURE_LATCH: usize = 2;
852    let consecutive_failures = Arc::new(std::sync::atomic::AtomicUsize::new(0));
853    let status = BulkFetcherStatus {
854        consecutive_failures: Arc::clone(&consecutive_failures),
855        latch_threshold: OVERRIDE_FAILURE_LATCH,
856    };
857    let fetcher: StorageBatchFetchFn =
858        Arc::new(move |requests: Vec<(Address, U256)>, block: BlockId| {
859            use std::sync::atomic::Ordering;
860            if requests.is_empty() {
861                return Vec::new();
862            }
863            if let Some(fallback) = &fallback
864                && (requests.len() < config.point_read_threshold
865                    || consecutive_failures.load(Ordering::Relaxed) >= OVERRIDE_FAILURE_LATCH)
866            {
867                return fallback(requests, block);
868            }
869
870            // Guard against panicking inside `block_in_place` on a current-thread
871            // runtime (or when no runtime is present): report an `Err` result for
872            // every requested slot instead, mirroring the default fetcher. The
873            // guard errors still flow through the fallback repair below — a
874            // synchronous fallback can serve them even where the bulk path can't
875            // run.
876            let bulk_results = match block_in_place_handle() {
877                Ok(handle) => tokio::task::block_in_place(|| {
878                    handle.block_on(fetch_slots_bulk(provider.as_ref(), requests, block, config))
879                }),
880                Err(e) => requests
881                    .into_iter()
882                    .map(|(addr, slot)| (addr, slot, Err(StorageFetchError::Runtime(e.clone()))))
883                    .collect(),
884            };
885
886            let Some(fallback) = &fallback else {
887                return bulk_results;
888            };
889
890            // Latch bookkeeping: any success resets the streak; a batch where
891            // *everything* failed at the provider level counts toward latching.
892            if bulk_results.iter().any(|(_, _, r)| r.is_ok()) {
893                consecutive_failures.store(0, Ordering::Relaxed);
894            } else if bulk_results
895                .iter()
896                .any(|(_, _, r)| matches!(r, Err(StorageFetchError::Provider { .. })))
897            {
898                let streak = consecutive_failures.fetch_add(1, Ordering::Relaxed) + 1;
899                if streak == OVERRIDE_FAILURE_LATCH {
900                    warn!(
901                        streak,
902                        "bulk storage extraction failed consecutive batches with provider errors; \
903                     latching this fetcher to the point-read fallback (install a fresh fetcher \
904                     to retry bulk extraction)"
905                    );
906                }
907            }
908
909            // Repair failed pairs (with multiplicity) through the fallback,
910            // preserving the one-result-per-request contract.
911            let mut repaired = Vec::with_capacity(bulk_results.len());
912            let mut failed: Vec<(Address, U256)> = Vec::new();
913            for (addr, slot, result) in bulk_results {
914                match result {
915                    Ok(value) => repaired.push((addr, slot, Ok(value))),
916                    Err(_) => failed.push((addr, slot)),
917                }
918            }
919            if !failed.is_empty() {
920                warn!(
921                    failed = failed.len(),
922                    "bulk storage extraction failed for some slots; repairing via fallback fetcher"
923                );
924                repaired.extend(fallback(failed, block));
925            }
926            repaired
927        });
928    (fetcher, status)
929}
930
931// ---------------------------------------------------------------------------
932// Custom storage programs & companion extractors
933// ---------------------------------------------------------------------------
934
935/// A caller-supplied extraction program: arbitrary bytecode injected at
936/// `target` through a code override and executed by one `eth_call`.
937///
938/// The slot-list extractor requires the client to know every slot key up
939/// front. A custom program removes that constraint — it can *derive* what to
940/// read inside the EVM. Example: a Uniswap V3 loader that walks the
941/// `tickBitmap` words on-chain and returns every initialized tick's data in a
942/// single round trip, with no calldata at all (the two-phase
943/// bitmap-then-ticks pattern collapsed into one call). The program runs at
944/// the target's address, so `SLOAD` reads the target's real storage; the
945/// output format is whatever the program returns — decoding is the caller's
946/// contract with its own bytecode.
947///
948/// See `examples/bulk_storage_bench.rs` for a worked program (a one-shot
949/// Uniswap V3 observation-ring loader that reads the cardinality from
950/// `slot0` and returns the whole ring) and the offline revm tests in
951/// `tests/bulk_storage.rs` that execute it.
952#[derive(Debug, Clone, PartialEq, Eq)]
953pub struct StorageProgram {
954    /// Address whose storage the program reads (its code is replaced by
955    /// `code` for the duration of the call).
956    pub target: Address,
957    /// Runtime bytecode to inject at `target`.
958    pub code: Bytes,
959    /// Calldata passed to the program (may be empty).
960    pub calldata: Bytes,
961}
962
963/// Execute one [`StorageProgram`] via `eth_call` and return its raw output.
964pub async fn run_storage_program<P: Provider<AnyNetwork>>(
965    provider: &P,
966    block: BlockId,
967    program: &StorageProgram,
968) -> StorageFetchResult<Bytes> {
969    let mut overrides = StateOverride::default();
970    overrides.insert(
971        program.target,
972        AccountOverride::default().with_code(program.code.clone()),
973    );
974    let tx = TransactionRequest::default()
975        .to(program.target)
976        .input(program.calldata.clone().into());
977    provider
978        .client()
979        .request("eth_call", (tx, block, overrides))
980        .await
981        .map_err(|e| StorageFetchError::provider("eth_call", &e))
982}
983
984/// Execute several [`StorageProgram`]s, batching programs with distinct
985/// targets into a single Multicall3-dispatched `eth_call`.
986///
987/// Programs that share a target address (each needs its own code override at
988/// that key) or that target the dispatcher address run as individual calls.
989/// Results are returned in input order, one per program.
990pub async fn run_storage_programs<P: Provider<AnyNetwork>>(
991    provider: &P,
992    block: BlockId,
993    programs: &[StorageProgram],
994) -> Vec<StorageFetchResult<Bytes>> {
995    let mut seen = std::collections::HashSet::new();
996    let mut bundle: Vec<usize> = Vec::new();
997    let mut individual: Vec<usize> = Vec::new();
998    for (index, program) in programs.iter().enumerate() {
999        if program.target != MULTICALL3_ADDRESS && seen.insert(program.target) {
1000            bundle.push(index);
1001        } else {
1002            individual.push(index);
1003        }
1004    }
1005    // A bundle of one is just an ordinary call with dispatch overhead.
1006    if bundle.len() == 1 {
1007        individual.append(&mut bundle);
1008    }
1009
1010    let mut out: Vec<Option<StorageFetchResult<Bytes>>> = vec![None; programs.len()];
1011
1012    if !bundle.is_empty() {
1013        let mut overrides = StateOverride::default();
1014        overrides.insert(
1015            MULTICALL3_ADDRESS,
1016            AccountOverride::default().with_code(multicall3_runtime_code().clone()),
1017        );
1018        let calls: Vec<IMulticall3::Call3> = bundle
1019            .iter()
1020            .map(|&index| {
1021                let program = &programs[index];
1022                overrides.insert(
1023                    program.target,
1024                    AccountOverride::default().with_code(program.code.clone()),
1025                );
1026                IMulticall3::Call3 {
1027                    target: program.target,
1028                    allowFailure: true,
1029                    callData: program.calldata.clone(),
1030                }
1031            })
1032            .collect();
1033        let data: Bytes = IMulticall3::aggregate3Call { calls }.abi_encode().into();
1034        let tx = TransactionRequest::default()
1035            .to(MULTICALL3_ADDRESS)
1036            .input(data.into());
1037        let response: Result<Bytes, _> = provider
1038            .client()
1039            .request("eth_call", (tx, block, overrides))
1040            .await;
1041        match response
1042            .map_err(|e| StorageFetchError::provider("eth_call", &e))
1043            .and_then(|bytes| {
1044                IMulticall3::aggregate3Call::abi_decode_returns(&bytes).map_err(|e| {
1045                    StorageFetchError::custom(format!("failed to decode aggregate3 response: {e}"))
1046                })
1047            }) {
1048            Ok(results) if results.len() == bundle.len() => {
1049                for (&index, result) in bundle.iter().zip(results) {
1050                    out[index] = Some(if result.success {
1051                        Ok(result.returnData)
1052                    } else {
1053                        Err(StorageFetchError::custom(
1054                            "storage program subcall failed (allowFailure=true)",
1055                        ))
1056                    });
1057                }
1058            }
1059            Ok(results) => {
1060                let err = StorageFetchError::custom(format!(
1061                    "aggregate3 returned {} results for {} programs",
1062                    results.len(),
1063                    bundle.len()
1064                ));
1065                for &index in &bundle {
1066                    out[index] = Some(Err(err.clone()));
1067                }
1068            }
1069            Err(err) => {
1070                for &index in &bundle {
1071                    out[index] = Some(Err(err.clone()));
1072                }
1073            }
1074        }
1075    }
1076
1077    for index in individual {
1078        out[index] = Some(run_storage_program(provider, block, &programs[index]).await);
1079    }
1080
1081    out.into_iter()
1082        .map(|entry| entry.expect("every program resolved"))
1083        .collect()
1084}
1085
1086/// Account-fields extractor: calldata is a contiguous array of 32-byte
1087/// left-padded addresses; the return data is `[balance, extcodehash]` (two
1088/// words) per address, via the `BALANCE` and `EXTCODEHASH` opcodes.
1089///
1090/// ```text
1091/// [00] PUSH0            counter = 0
1092/// [01] JUMPDEST         loop: exit when counter == calldatasize
1093/// [08] DUP1 CALLDATALOAD                addr
1094/// [0a] DUP1 BALANCE     mem[2*counter]        = balance(addr)
1095/// [11] EXTCODEHASH      mem[2*counter + 32]   = extcodehash(addr)
1096/// [1a] counter += 32; loop
1097/// [20] JUMPDEST RETURN(0, 2*calldatasize)
1098/// ```
1099///
1100/// Requires `PUSH0` (Shanghai). Nonces and storage roots are **not**
1101/// EVM-visible — use `eth_getProof` (the [`AccountProofFetchFn`] path) when
1102/// those are needed.
1103///
1104/// [`AccountProofFetchFn`]: crate::cache::AccountProofFetchFn
1105pub const ACCOUNT_FIELDS_EXTRACTOR_CODE: &[u8] =
1106    &hex!("5f5b803614602057803580318260011b523f8160011b602001526020016001565b3660011b5ff3");
1107
1108/// Balance + code hash of one account, as sampled in-EVM by
1109/// [`ACCOUNT_FIELDS_EXTRACTOR_CODE`].
1110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1111pub struct AccountFieldsSample {
1112    /// Native balance (`BALANCE`).
1113    pub balance: U256,
1114    /// `EXTCODEHASH` semantics (EIP-1052): zero for a non-existent account,
1115    /// `keccak256("")` for an existing code-less account (EOA).
1116    pub code_hash: B256,
1117}
1118
1119/// Fetch balance + code hash for many accounts in **one** `eth_call`.
1120///
1121/// `BALANCE`/`EXTCODEHASH` read *other* accounts, so only one code override
1122/// (the extractor host, at [`MULTICALL3_ADDRESS`]) is injected and the
1123/// queried accounts are untouched. Costs ~5.3k gas per address (two cold
1124/// account accesses), so thousands of accounts fit in one call. Querying the
1125/// host address itself reports the extractor's own code hash — give it a
1126/// dedicated `eth_getProof` instead.
1127pub async fn fetch_account_fields_bulk<P: Provider<AnyNetwork>>(
1128    provider: &P,
1129    addresses: &[Address],
1130    block: BlockId,
1131) -> StorageFetchResult<Vec<(Address, AccountFieldsSample)>> {
1132    if addresses.is_empty() {
1133        return Ok(Vec::new());
1134    }
1135    let mut calldata = Vec::with_capacity(addresses.len() * 32);
1136    for address in addresses {
1137        calldata.extend_from_slice(&[0u8; 12]);
1138        calldata.extend_from_slice(address.as_slice());
1139    }
1140    let program = StorageProgram {
1141        target: MULTICALL3_ADDRESS,
1142        code: Bytes::from_static(ACCOUNT_FIELDS_EXTRACTOR_CODE),
1143        calldata: calldata.into(),
1144    };
1145    let bytes = run_storage_program(provider, block, &program).await?;
1146    if bytes.len() != addresses.len() * 64 {
1147        return Err(StorageFetchError::custom(format!(
1148            "account-fields extractor returned {} bytes, expected {}",
1149            bytes.len(),
1150            addresses.len() * 64
1151        )));
1152    }
1153    Ok(addresses
1154        .iter()
1155        .enumerate()
1156        .map(|(i, address)| {
1157            (
1158                *address,
1159                AccountFieldsSample {
1160                    balance: U256::from_be_slice(&bytes[i * 64..i * 64 + 32]),
1161                    code_hash: B256::from_slice(&bytes[i * 64 + 32..i * 64 + 64]),
1162                },
1163            )
1164        })
1165        .collect())
1166}
1167
1168/// Block-context extractor: no calldata; returns seven words —
1169/// `NUMBER`, `TIMESTAMP`, `BASEFEE`, `COINBASE`, `PREVRANDAO`, `GASLIMIT`,
1170/// `CHAINID` — straight from the EVM environment of the queried block.
1171/// Piggybacks block-header context onto the same transport as slot loads
1172/// without an `eth_getBlockByNumber`. Requires `PUSH0` (Shanghai).
1173pub const BLOCK_CONTEXT_EXTRACTOR_CODE: &[u8] =
1174    &hex!("435f52426020524860405241606052446080524560a0524660c05260e05ff3");
1175
1176/// One block's EVM-visible context, as sampled by
1177/// [`BLOCK_CONTEXT_EXTRACTOR_CODE`].
1178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1179pub struct BlockContextSample {
1180    /// `NUMBER`.
1181    pub number: u64,
1182    /// `TIMESTAMP`.
1183    pub timestamp: u64,
1184    /// `BASEFEE` (wei).
1185    pub basefee: U256,
1186    /// `COINBASE`.
1187    pub coinbase: Address,
1188    /// `PREVRANDAO` (post-merge mix hash).
1189    pub prevrandao: B256,
1190    /// `GASLIMIT`.
1191    pub gas_limit: u64,
1192    /// `CHAINID`.
1193    pub chain_id: u64,
1194}
1195
1196/// Sample a block's EVM context in one `eth_call` (see
1197/// [`BLOCK_CONTEXT_EXTRACTOR_CODE`]).
1198pub async fn fetch_block_context<P: Provider<AnyNetwork>>(
1199    provider: &P,
1200    block: BlockId,
1201) -> StorageFetchResult<BlockContextSample> {
1202    let program = StorageProgram {
1203        target: MULTICALL3_ADDRESS,
1204        code: Bytes::from_static(BLOCK_CONTEXT_EXTRACTOR_CODE),
1205        calldata: Bytes::new(),
1206    };
1207    let bytes = run_storage_program(provider, block, &program).await?;
1208    if bytes.len() != 7 * 32 {
1209        return Err(StorageFetchError::custom(format!(
1210            "block-context extractor returned {} bytes, expected 224",
1211            bytes.len()
1212        )));
1213    }
1214    let word = |i: usize| U256::from_be_slice(&bytes[i * 32..(i + 1) * 32]);
1215    let to_u64 = |v: U256| u64::try_from(v).unwrap_or(u64::MAX);
1216    Ok(BlockContextSample {
1217        number: to_u64(word(0)),
1218        timestamp: to_u64(word(1)),
1219        basefee: word(2),
1220        coinbase: Address::from_slice(&bytes[3 * 32 + 12..4 * 32]),
1221        prevrandao: B256::from_slice(&bytes[4 * 32..5 * 32]),
1222        gas_limit: to_u64(word(5)),
1223        chain_id: to_u64(word(6)),
1224    })
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229    use super::*;
1230
1231    fn addr(byte: u8) -> Address {
1232        Address::repeat_byte(byte)
1233    }
1234
1235    fn cfg(max_slots: usize, max_targets: usize) -> BulkCallConfig {
1236        BulkCallConfig {
1237            max_slots_per_call: max_slots,
1238            max_targets_per_call: max_targets,
1239            ..BulkCallConfig::default()
1240        }
1241    }
1242
1243    #[test]
1244    fn pack_and_decode_roundtrip() {
1245        let slots = vec![U256::ZERO, U256::from(1u64), U256::MAX];
1246        let packed = pack_slots_calldata(&slots);
1247        assert_eq!(packed.len(), 96);
1248        assert_eq!(&packed[32..64], &U256::from(1u64).to_be_bytes::<32>());
1249        let decoded = decode_packed_values(&packed, 3).expect("exact length");
1250        assert_eq!(decoded, slots);
1251        assert!(decode_packed_values(&packed, 2).is_none());
1252        assert!(decode_packed_values(&packed[..95], 3).is_none());
1253    }
1254
1255    #[test]
1256    fn extractor_constants_are_wellformed() {
1257        // Anchor the exact published bytecode; the EVM-level behavior of both
1258        // variants is exercised end-to-end in tests/bulk_storage.rs.
1259        assert_eq!(STORAGE_EXTRACTOR_CODE.len(), 23);
1260        assert_eq!(STORAGE_EXTRACTOR_CODE[0], 0x5f, "PUSH0 entry");
1261        assert_eq!(STORAGE_EXTRACTOR_CODE_PRE_SHANGHAI.len(), 25);
1262        assert!(
1263            !STORAGE_EXTRACTOR_CODE_PRE_SHANGHAI.contains(&0x5f),
1264            "pre-Shanghai variant must not use PUSH0"
1265        );
1266        assert!(multicall3_runtime_code().len() > 1_000);
1267    }
1268
1269    #[test]
1270    fn planning_single_small_group() {
1271        let requests = vec![
1272            (addr(0xaa), U256::from(1u64)),
1273            (addr(0xaa), U256::from(2u64)),
1274        ];
1275        let plans = plan_calls(&requests, &cfg(100, 10));
1276        assert_eq!(
1277            plans,
1278            vec![CallPlan::Single {
1279                target: addr(0xaa),
1280                slots: vec![U256::from(1u64), U256::from(2u64)],
1281            }]
1282        );
1283    }
1284
1285    #[test]
1286    fn planning_splits_oversized_target_and_packs_remainder() {
1287        // 7 slots with a 3-slot budget: two full single-target chunks + the
1288        // 1-slot remainder packed with the other small target.
1289        let mut requests: Vec<_> = (0..7u64).map(|i| (addr(0x01), U256::from(i))).collect();
1290        requests.push((addr(0x02), U256::from(99u64)));
1291        let plans = plan_calls(&requests, &cfg(3, 10));
1292        assert_eq!(plans.len(), 3);
1293        assert_eq!(
1294            plans[0],
1295            CallPlan::Single {
1296                target: addr(0x01),
1297                slots: (0..3u64).map(U256::from).collect(),
1298            }
1299        );
1300        assert_eq!(
1301            plans[1],
1302            CallPlan::Single {
1303                target: addr(0x01),
1304                slots: (3..6u64).map(U256::from).collect(),
1305            }
1306        );
1307        assert_eq!(
1308            plans[2],
1309            CallPlan::Multi {
1310                targets: vec![
1311                    (addr(0x01), vec![U256::from(6u64)]),
1312                    (addr(0x02), vec![U256::from(99u64)]),
1313                ],
1314            }
1315        );
1316        let planned: usize = plans.iter().map(CallPlan::request_slot_count).sum();
1317        assert_eq!(planned, requests.len());
1318    }
1319
1320    #[test]
1321    fn planning_respects_target_budget() {
1322        let requests: Vec<_> = (0..5u8)
1323            .map(|i| (addr(i + 1), U256::from(i as u64)))
1324            .collect();
1325        let plans = plan_calls(&requests, &cfg(100, 2));
1326        // 5 single-slot targets with a 2-target budget: 2 + 2 + 1.
1327        assert_eq!(plans.len(), 3);
1328        assert!(matches!(&plans[0], CallPlan::Multi { targets } if targets.len() == 2));
1329        assert!(matches!(&plans[1], CallPlan::Multi { targets } if targets.len() == 2));
1330        assert!(matches!(&plans[2], CallPlan::Single { .. }));
1331    }
1332
1333    #[test]
1334    fn planning_lone_remainder_degrades_to_single_call() {
1335        let requests: Vec<_> = (0..4u64).map(|i| (addr(0x01), U256::from(i))).collect();
1336        let plans = plan_calls(&requests, &cfg(3, 10));
1337        assert_eq!(plans.len(), 2);
1338        assert!(matches!(&plans[1], CallPlan::Single { slots, .. } if slots.len() == 1));
1339    }
1340
1341    #[test]
1342    fn planning_isolates_dispatcher_address_collision() {
1343        let requests = vec![
1344            (MULTICALL3_ADDRESS, U256::from(1u64)),
1345            (addr(0x02), U256::from(2u64)),
1346            (addr(0x03), U256::from(3u64)),
1347        ];
1348        let plans = plan_calls(&requests, &cfg(100, 10));
1349        assert_eq!(
1350            plans[0],
1351            CallPlan::Single {
1352                target: MULTICALL3_ADDRESS,
1353                slots: vec![U256::from(1u64)],
1354            }
1355        );
1356        assert!(matches!(&plans[1], CallPlan::Multi { targets } if targets.len() == 2));
1357    }
1358
1359    #[test]
1360    fn multi_target_overrides_include_dispatcher_and_extractors() {
1361        let plan = CallPlan::Multi {
1362            targets: vec![
1363                (addr(0x02), vec![U256::from(1u64)]),
1364                (addr(0x03), vec![U256::from(2u64)]),
1365            ],
1366        };
1367        let extractor = Bytes::from_static(STORAGE_EXTRACTOR_CODE);
1368        let overrides = overrides_for_plan(&plan, &extractor);
1369        assert_eq!(overrides.len(), 3);
1370        assert_eq!(
1371            overrides[&MULTICALL3_ADDRESS].code.as_ref(),
1372            Some(multicall3_runtime_code())
1373        );
1374        assert_eq!(overrides[&addr(0x02)].code.as_ref(), Some(&extractor));
1375        assert_eq!(overrides[&addr(0x03)].code.as_ref(), Some(&extractor));
1376    }
1377
1378    #[test]
1379    fn call_many_grouping_respects_request_budget() {
1380        let plans = vec![
1381            CallPlan::Single {
1382                target: addr(0x01),
1383                slots: (0..6u64).map(U256::from).collect(),
1384            },
1385            CallPlan::Single {
1386                target: addr(0x02),
1387                slots: (0..6u64).map(U256::from).collect(),
1388            },
1389            CallPlan::Single {
1390                target: addr(0x03),
1391                slots: (0..2u64).map(U256::from).collect(),
1392            },
1393        ];
1394        let groups = group_plans_for_call_many(plans, 10);
1395        // 6 + 6 > 10 → split; 6 + 2 ≤ 10 → packed together.
1396        assert_eq!(groups.len(), 2);
1397        assert_eq!(groups[0].len(), 1);
1398        assert_eq!(groups[1].len(), 2);
1399        let total: usize = groups
1400            .iter()
1401            .flatten()
1402            .map(CallPlan::request_slot_count)
1403            .sum();
1404        assert_eq!(total, 14);
1405    }
1406
1407    #[test]
1408    fn multi_target_response_decodes_per_target_failures() {
1409        let targets = vec![
1410            (addr(0x02), vec![U256::from(1u64), U256::from(2u64)]),
1411            (addr(0x03), vec![U256::from(3u64)]),
1412        ];
1413        let response = IMulticall3::aggregate3Call::abi_encode_returns(&vec![
1414            IMulticall3::Result {
1415                success: true,
1416                returnData: pack_slots_calldata(&[U256::from(11u64), U256::from(22u64)]),
1417            },
1418            IMulticall3::Result {
1419                success: false,
1420                returnData: Bytes::new(),
1421            },
1422        ]);
1423        let results = decode_multi_target_response(&targets, &response);
1424        assert_eq!(results.len(), 3);
1425        assert!(matches!(results[0], (_, _, Ok(v)) if v == U256::from(11u64)));
1426        assert!(matches!(results[1], (_, _, Ok(v)) if v == U256::from(22u64)));
1427        assert!(results[2].2.is_err());
1428    }
1429}