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