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/// Build a [`StorageBatchFetchFn`] backed by call-override bulk extraction.
741///
742/// Install it with [`EvmCache::set_storage_batch_fetcher`](crate::cache::EvmCache::set_storage_batch_fetcher);
743/// every batch consumer (freshness verification, cold-start verify/probe,
744/// reactive point-read resyncs, prefetch) then loads storage through bulk
745/// `eth_call`s. Requires a multi-thread tokio runtime, like the default
746/// fetcher.
747///
748/// Failed slots are reported as errors; use
749/// [`bulk_call_storage_fetcher_with_fallback`] to repair them with classic
750/// point reads instead.
751pub fn bulk_call_storage_fetcher<P: Provider<AnyNetwork> + 'static>(
752    provider: Arc<P>,
753    config: BulkCallConfig,
754) -> StorageBatchFetchFn {
755    make_fetcher(provider, config, None)
756}
757
758/// [`bulk_call_storage_fetcher`] with a repair path.
759///
760/// `fallback` (typically the cache's default point-read fetcher, obtained via
761/// [`EvmCache::storage_batch_fetcher`](crate::cache::EvmCache::storage_batch_fetcher)
762/// before replacing it) is invoked for:
763/// - requests smaller than [`BulkCallConfig::point_read_threshold`], where a
764///   point read is cheaper than an `eth_call` on CU-metered providers; and
765/// - any pairs the bulk path reported as errors (provider without
766///   state-override support, precompile targets, transport failures).
767pub fn bulk_call_storage_fetcher_with_fallback<P: Provider<AnyNetwork> + 'static>(
768    provider: Arc<P>,
769    config: BulkCallConfig,
770    fallback: StorageBatchFetchFn,
771) -> StorageBatchFetchFn {
772    make_fetcher(provider, config, Some(fallback))
773}
774
775fn make_fetcher<P: Provider<AnyNetwork> + 'static>(
776    provider: Arc<P>,
777    config: BulkCallConfig,
778    fallback: Option<StorageBatchFetchFn>,
779) -> StorageBatchFetchFn {
780    let config = config.normalized();
781    // After this many *consecutive* batches where every slot failed with a
782    // provider-level error (the signature of an endpoint without
783    // state-override support), stop attempting bulk extraction and route
784    // straight to the fallback. Sticky for the fetcher's lifetime — install a
785    // fresh fetcher to retry bulk extraction. Only meaningful when a fallback
786    // exists; without one the bulk attempt is the only option anyway.
787    const OVERRIDE_FAILURE_LATCH: usize = 2;
788    let consecutive_failures = Arc::new(std::sync::atomic::AtomicUsize::new(0));
789    Arc::new(move |requests: Vec<(Address, U256)>, block: BlockId| {
790        use std::sync::atomic::Ordering;
791        if requests.is_empty() {
792            return Vec::new();
793        }
794        if let Some(fallback) = &fallback
795            && (requests.len() < config.point_read_threshold
796                || consecutive_failures.load(Ordering::Relaxed) >= OVERRIDE_FAILURE_LATCH)
797        {
798            return fallback(requests, block);
799        }
800
801        // Guard against panicking inside `block_in_place` on a current-thread
802        // runtime (or when no runtime is present): report an `Err` result for
803        // every requested slot instead, mirroring the default fetcher. The
804        // guard errors still flow through the fallback repair below — a
805        // synchronous fallback can serve them even where the bulk path can't
806        // run.
807        let bulk_results = match block_in_place_handle() {
808            Ok(handle) => tokio::task::block_in_place(|| {
809                handle.block_on(fetch_slots_bulk(provider.as_ref(), requests, block, config))
810            }),
811            Err(e) => requests
812                .into_iter()
813                .map(|(addr, slot)| (addr, slot, Err(StorageFetchError::Runtime(e.clone()))))
814                .collect(),
815        };
816
817        let Some(fallback) = &fallback else {
818            return bulk_results;
819        };
820
821        // Latch bookkeeping: any success resets the streak; a batch where
822        // *everything* failed at the provider level counts toward latching.
823        if bulk_results.iter().any(|(_, _, r)| r.is_ok()) {
824            consecutive_failures.store(0, Ordering::Relaxed);
825        } else if bulk_results
826            .iter()
827            .any(|(_, _, r)| matches!(r, Err(StorageFetchError::Provider { .. })))
828        {
829            let streak = consecutive_failures.fetch_add(1, Ordering::Relaxed) + 1;
830            if streak == OVERRIDE_FAILURE_LATCH {
831                warn!(
832                    streak,
833                    "bulk storage extraction failed consecutive batches with provider errors; \
834                     latching this fetcher to the point-read fallback (install a fresh fetcher \
835                     to retry bulk extraction)"
836                );
837            }
838        }
839
840        // Repair failed pairs (with multiplicity) through the fallback,
841        // preserving the one-result-per-request contract.
842        let mut repaired = Vec::with_capacity(bulk_results.len());
843        let mut failed: Vec<(Address, U256)> = Vec::new();
844        for (addr, slot, result) in bulk_results {
845            match result {
846                Ok(value) => repaired.push((addr, slot, Ok(value))),
847                Err(_) => failed.push((addr, slot)),
848            }
849        }
850        if !failed.is_empty() {
851            warn!(
852                failed = failed.len(),
853                "bulk storage extraction failed for some slots; repairing via fallback fetcher"
854            );
855            repaired.extend(fallback(failed, block));
856        }
857        repaired
858    })
859}
860
861// ---------------------------------------------------------------------------
862// Custom storage programs & companion extractors
863// ---------------------------------------------------------------------------
864
865/// A caller-supplied extraction program: arbitrary bytecode injected at
866/// `target` through a code override and executed by one `eth_call`.
867///
868/// The slot-list extractor requires the client to know every slot key up
869/// front. A custom program removes that constraint — it can *derive* what to
870/// read inside the EVM. Example: a Uniswap V3 loader that walks the
871/// `tickBitmap` words on-chain and returns every initialized tick's data in a
872/// single round trip, with no calldata at all (the two-phase
873/// bitmap-then-ticks pattern collapsed into one call). The program runs at
874/// the target's address, so `SLOAD` reads the target's real storage; the
875/// output format is whatever the program returns — decoding is the caller's
876/// contract with its own bytecode.
877///
878/// See `examples/bulk_storage_bench.rs` for a worked program (a one-shot
879/// Uniswap V3 observation-ring loader that reads the cardinality from
880/// `slot0` and returns the whole ring) and the offline revm tests in
881/// `tests/bulk_storage.rs` that execute it.
882#[derive(Debug, Clone, PartialEq, Eq)]
883pub struct StorageProgram {
884    /// Address whose storage the program reads (its code is replaced by
885    /// `code` for the duration of the call).
886    pub target: Address,
887    /// Runtime bytecode to inject at `target`.
888    pub code: Bytes,
889    /// Calldata passed to the program (may be empty).
890    pub calldata: Bytes,
891}
892
893/// Execute one [`StorageProgram`] via `eth_call` and return its raw output.
894pub async fn run_storage_program<P: Provider<AnyNetwork>>(
895    provider: &P,
896    block: BlockId,
897    program: &StorageProgram,
898) -> StorageFetchResult<Bytes> {
899    let mut overrides = StateOverride::default();
900    overrides.insert(
901        program.target,
902        AccountOverride::default().with_code(program.code.clone()),
903    );
904    let tx = TransactionRequest::default()
905        .to(program.target)
906        .input(program.calldata.clone().into());
907    provider
908        .client()
909        .request("eth_call", (tx, block, overrides))
910        .await
911        .map_err(|e| StorageFetchError::provider("eth_call", &e))
912}
913
914/// Execute several [`StorageProgram`]s, batching programs with distinct
915/// targets into a single Multicall3-dispatched `eth_call`.
916///
917/// Programs that share a target address (each needs its own code override at
918/// that key) or that target the dispatcher address run as individual calls.
919/// Results are returned in input order, one per program.
920pub async fn run_storage_programs<P: Provider<AnyNetwork>>(
921    provider: &P,
922    block: BlockId,
923    programs: &[StorageProgram],
924) -> Vec<StorageFetchResult<Bytes>> {
925    let mut seen = std::collections::HashSet::new();
926    let mut bundle: Vec<usize> = Vec::new();
927    let mut individual: Vec<usize> = Vec::new();
928    for (index, program) in programs.iter().enumerate() {
929        if program.target != MULTICALL3_ADDRESS && seen.insert(program.target) {
930            bundle.push(index);
931        } else {
932            individual.push(index);
933        }
934    }
935    // A bundle of one is just an ordinary call with dispatch overhead.
936    if bundle.len() == 1 {
937        individual.append(&mut bundle);
938    }
939
940    let mut out: Vec<Option<StorageFetchResult<Bytes>>> = vec![None; programs.len()];
941
942    if !bundle.is_empty() {
943        let mut overrides = StateOverride::default();
944        overrides.insert(
945            MULTICALL3_ADDRESS,
946            AccountOverride::default().with_code(multicall3_runtime_code().clone()),
947        );
948        let calls: Vec<IMulticall3::Call3> = bundle
949            .iter()
950            .map(|&index| {
951                let program = &programs[index];
952                overrides.insert(
953                    program.target,
954                    AccountOverride::default().with_code(program.code.clone()),
955                );
956                IMulticall3::Call3 {
957                    target: program.target,
958                    allowFailure: true,
959                    callData: program.calldata.clone(),
960                }
961            })
962            .collect();
963        let data: Bytes = IMulticall3::aggregate3Call { calls }.abi_encode().into();
964        let tx = TransactionRequest::default()
965            .to(MULTICALL3_ADDRESS)
966            .input(data.into());
967        let response: Result<Bytes, _> = provider
968            .client()
969            .request("eth_call", (tx, block, overrides))
970            .await;
971        match response
972            .map_err(|e| StorageFetchError::provider("eth_call", &e))
973            .and_then(|bytes| {
974                IMulticall3::aggregate3Call::abi_decode_returns(&bytes).map_err(|e| {
975                    StorageFetchError::custom(format!("failed to decode aggregate3 response: {e}"))
976                })
977            }) {
978            Ok(results) if results.len() == bundle.len() => {
979                for (&index, result) in bundle.iter().zip(results) {
980                    out[index] = Some(if result.success {
981                        Ok(result.returnData)
982                    } else {
983                        Err(StorageFetchError::custom(
984                            "storage program subcall failed (allowFailure=true)",
985                        ))
986                    });
987                }
988            }
989            Ok(results) => {
990                let err = StorageFetchError::custom(format!(
991                    "aggregate3 returned {} results for {} programs",
992                    results.len(),
993                    bundle.len()
994                ));
995                for &index in &bundle {
996                    out[index] = Some(Err(err.clone()));
997                }
998            }
999            Err(err) => {
1000                for &index in &bundle {
1001                    out[index] = Some(Err(err.clone()));
1002                }
1003            }
1004        }
1005    }
1006
1007    for index in individual {
1008        out[index] = Some(run_storage_program(provider, block, &programs[index]).await);
1009    }
1010
1011    out.into_iter()
1012        .map(|entry| entry.expect("every program resolved"))
1013        .collect()
1014}
1015
1016/// Account-fields extractor: calldata is a contiguous array of 32-byte
1017/// left-padded addresses; the return data is `[balance, extcodehash]` (two
1018/// words) per address, via the `BALANCE` and `EXTCODEHASH` opcodes.
1019///
1020/// ```text
1021/// [00] PUSH0            counter = 0
1022/// [01] JUMPDEST         loop: exit when counter == calldatasize
1023/// [08] DUP1 CALLDATALOAD                addr
1024/// [0a] DUP1 BALANCE     mem[2*counter]        = balance(addr)
1025/// [11] EXTCODEHASH      mem[2*counter + 32]   = extcodehash(addr)
1026/// [1a] counter += 32; loop
1027/// [20] JUMPDEST RETURN(0, 2*calldatasize)
1028/// ```
1029///
1030/// Requires `PUSH0` (Shanghai). Nonces and storage roots are **not**
1031/// EVM-visible — use `eth_getProof` (the [`AccountProofFetchFn`] path) when
1032/// those are needed.
1033///
1034/// [`AccountProofFetchFn`]: crate::cache::AccountProofFetchFn
1035pub const ACCOUNT_FIELDS_EXTRACTOR_CODE: &[u8] =
1036    &hex!("5f5b803614602057803580318260011b523f8160011b602001526020016001565b3660011b5ff3");
1037
1038/// Balance + code hash of one account, as sampled in-EVM by
1039/// [`ACCOUNT_FIELDS_EXTRACTOR_CODE`].
1040#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1041pub struct AccountFieldsSample {
1042    /// Native balance (`BALANCE`).
1043    pub balance: U256,
1044    /// `EXTCODEHASH` semantics (EIP-1052): zero for a non-existent account,
1045    /// `keccak256("")` for an existing code-less account (EOA).
1046    pub code_hash: B256,
1047}
1048
1049/// Fetch balance + code hash for many accounts in **one** `eth_call`.
1050///
1051/// `BALANCE`/`EXTCODEHASH` read *other* accounts, so only one code override
1052/// (the extractor host, at [`MULTICALL3_ADDRESS`]) is injected and the
1053/// queried accounts are untouched. Costs ~5.3k gas per address (two cold
1054/// account accesses), so thousands of accounts fit in one call. Querying the
1055/// host address itself reports the extractor's own code hash — give it a
1056/// dedicated `eth_getProof` instead.
1057pub async fn fetch_account_fields_bulk<P: Provider<AnyNetwork>>(
1058    provider: &P,
1059    addresses: &[Address],
1060    block: BlockId,
1061) -> StorageFetchResult<Vec<(Address, AccountFieldsSample)>> {
1062    if addresses.is_empty() {
1063        return Ok(Vec::new());
1064    }
1065    let mut calldata = Vec::with_capacity(addresses.len() * 32);
1066    for address in addresses {
1067        calldata.extend_from_slice(&[0u8; 12]);
1068        calldata.extend_from_slice(address.as_slice());
1069    }
1070    let program = StorageProgram {
1071        target: MULTICALL3_ADDRESS,
1072        code: Bytes::from_static(ACCOUNT_FIELDS_EXTRACTOR_CODE),
1073        calldata: calldata.into(),
1074    };
1075    let bytes = run_storage_program(provider, block, &program).await?;
1076    if bytes.len() != addresses.len() * 64 {
1077        return Err(StorageFetchError::custom(format!(
1078            "account-fields extractor returned {} bytes, expected {}",
1079            bytes.len(),
1080            addresses.len() * 64
1081        )));
1082    }
1083    Ok(addresses
1084        .iter()
1085        .enumerate()
1086        .map(|(i, address)| {
1087            (
1088                *address,
1089                AccountFieldsSample {
1090                    balance: U256::from_be_slice(&bytes[i * 64..i * 64 + 32]),
1091                    code_hash: B256::from_slice(&bytes[i * 64 + 32..i * 64 + 64]),
1092                },
1093            )
1094        })
1095        .collect())
1096}
1097
1098/// Block-context extractor: no calldata; returns seven words —
1099/// `NUMBER`, `TIMESTAMP`, `BASEFEE`, `COINBASE`, `PREVRANDAO`, `GASLIMIT`,
1100/// `CHAINID` — straight from the EVM environment of the queried block.
1101/// Piggybacks block-header context onto the same transport as slot loads
1102/// without an `eth_getBlockByNumber`. Requires `PUSH0` (Shanghai).
1103pub const BLOCK_CONTEXT_EXTRACTOR_CODE: &[u8] =
1104    &hex!("435f52426020524860405241606052446080524560a0524660c05260e05ff3");
1105
1106/// One block's EVM-visible context, as sampled by
1107/// [`BLOCK_CONTEXT_EXTRACTOR_CODE`].
1108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1109pub struct BlockContextSample {
1110    /// `NUMBER`.
1111    pub number: u64,
1112    /// `TIMESTAMP`.
1113    pub timestamp: u64,
1114    /// `BASEFEE` (wei).
1115    pub basefee: U256,
1116    /// `COINBASE`.
1117    pub coinbase: Address,
1118    /// `PREVRANDAO` (post-merge mix hash).
1119    pub prevrandao: B256,
1120    /// `GASLIMIT`.
1121    pub gas_limit: u64,
1122    /// `CHAINID`.
1123    pub chain_id: u64,
1124}
1125
1126/// Sample a block's EVM context in one `eth_call` (see
1127/// [`BLOCK_CONTEXT_EXTRACTOR_CODE`]).
1128pub async fn fetch_block_context<P: Provider<AnyNetwork>>(
1129    provider: &P,
1130    block: BlockId,
1131) -> StorageFetchResult<BlockContextSample> {
1132    let program = StorageProgram {
1133        target: MULTICALL3_ADDRESS,
1134        code: Bytes::from_static(BLOCK_CONTEXT_EXTRACTOR_CODE),
1135        calldata: Bytes::new(),
1136    };
1137    let bytes = run_storage_program(provider, block, &program).await?;
1138    if bytes.len() != 7 * 32 {
1139        return Err(StorageFetchError::custom(format!(
1140            "block-context extractor returned {} bytes, expected 224",
1141            bytes.len()
1142        )));
1143    }
1144    let word = |i: usize| U256::from_be_slice(&bytes[i * 32..(i + 1) * 32]);
1145    let to_u64 = |v: U256| u64::try_from(v).unwrap_or(u64::MAX);
1146    Ok(BlockContextSample {
1147        number: to_u64(word(0)),
1148        timestamp: to_u64(word(1)),
1149        basefee: word(2),
1150        coinbase: Address::from_slice(&bytes[3 * 32 + 12..4 * 32]),
1151        prevrandao: B256::from_slice(&bytes[4 * 32..5 * 32]),
1152        gas_limit: to_u64(word(5)),
1153        chain_id: to_u64(word(6)),
1154    })
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159    use super::*;
1160
1161    fn addr(byte: u8) -> Address {
1162        Address::repeat_byte(byte)
1163    }
1164
1165    fn cfg(max_slots: usize, max_targets: usize) -> BulkCallConfig {
1166        BulkCallConfig {
1167            max_slots_per_call: max_slots,
1168            max_targets_per_call: max_targets,
1169            ..BulkCallConfig::default()
1170        }
1171    }
1172
1173    #[test]
1174    fn pack_and_decode_roundtrip() {
1175        let slots = vec![U256::ZERO, U256::from(1u64), U256::MAX];
1176        let packed = pack_slots_calldata(&slots);
1177        assert_eq!(packed.len(), 96);
1178        assert_eq!(&packed[32..64], &U256::from(1u64).to_be_bytes::<32>());
1179        let decoded = decode_packed_values(&packed, 3).expect("exact length");
1180        assert_eq!(decoded, slots);
1181        assert!(decode_packed_values(&packed, 2).is_none());
1182        assert!(decode_packed_values(&packed[..95], 3).is_none());
1183    }
1184
1185    #[test]
1186    fn extractor_constants_are_wellformed() {
1187        // Anchor the exact published bytecode; the EVM-level behavior of both
1188        // variants is exercised end-to-end in tests/bulk_storage.rs.
1189        assert_eq!(STORAGE_EXTRACTOR_CODE.len(), 23);
1190        assert_eq!(STORAGE_EXTRACTOR_CODE[0], 0x5f, "PUSH0 entry");
1191        assert_eq!(STORAGE_EXTRACTOR_CODE_PRE_SHANGHAI.len(), 25);
1192        assert!(
1193            !STORAGE_EXTRACTOR_CODE_PRE_SHANGHAI.contains(&0x5f),
1194            "pre-Shanghai variant must not use PUSH0"
1195        );
1196        assert!(multicall3_runtime_code().len() > 1_000);
1197    }
1198
1199    #[test]
1200    fn planning_single_small_group() {
1201        let requests = vec![
1202            (addr(0xaa), U256::from(1u64)),
1203            (addr(0xaa), U256::from(2u64)),
1204        ];
1205        let plans = plan_calls(&requests, &cfg(100, 10));
1206        assert_eq!(
1207            plans,
1208            vec![CallPlan::Single {
1209                target: addr(0xaa),
1210                slots: vec![U256::from(1u64), U256::from(2u64)],
1211            }]
1212        );
1213    }
1214
1215    #[test]
1216    fn planning_splits_oversized_target_and_packs_remainder() {
1217        // 7 slots with a 3-slot budget: two full single-target chunks + the
1218        // 1-slot remainder packed with the other small target.
1219        let mut requests: Vec<_> = (0..7u64).map(|i| (addr(0x01), U256::from(i))).collect();
1220        requests.push((addr(0x02), U256::from(99u64)));
1221        let plans = plan_calls(&requests, &cfg(3, 10));
1222        assert_eq!(plans.len(), 3);
1223        assert_eq!(
1224            plans[0],
1225            CallPlan::Single {
1226                target: addr(0x01),
1227                slots: (0..3u64).map(U256::from).collect(),
1228            }
1229        );
1230        assert_eq!(
1231            plans[1],
1232            CallPlan::Single {
1233                target: addr(0x01),
1234                slots: (3..6u64).map(U256::from).collect(),
1235            }
1236        );
1237        assert_eq!(
1238            plans[2],
1239            CallPlan::Multi {
1240                targets: vec![
1241                    (addr(0x01), vec![U256::from(6u64)]),
1242                    (addr(0x02), vec![U256::from(99u64)]),
1243                ],
1244            }
1245        );
1246        let planned: usize = plans.iter().map(CallPlan::request_slot_count).sum();
1247        assert_eq!(planned, requests.len());
1248    }
1249
1250    #[test]
1251    fn planning_respects_target_budget() {
1252        let requests: Vec<_> = (0..5u8)
1253            .map(|i| (addr(i + 1), U256::from(i as u64)))
1254            .collect();
1255        let plans = plan_calls(&requests, &cfg(100, 2));
1256        // 5 single-slot targets with a 2-target budget: 2 + 2 + 1.
1257        assert_eq!(plans.len(), 3);
1258        assert!(matches!(&plans[0], CallPlan::Multi { targets } if targets.len() == 2));
1259        assert!(matches!(&plans[1], CallPlan::Multi { targets } if targets.len() == 2));
1260        assert!(matches!(&plans[2], CallPlan::Single { .. }));
1261    }
1262
1263    #[test]
1264    fn planning_lone_remainder_degrades_to_single_call() {
1265        let requests: Vec<_> = (0..4u64).map(|i| (addr(0x01), U256::from(i))).collect();
1266        let plans = plan_calls(&requests, &cfg(3, 10));
1267        assert_eq!(plans.len(), 2);
1268        assert!(matches!(&plans[1], CallPlan::Single { slots, .. } if slots.len() == 1));
1269    }
1270
1271    #[test]
1272    fn planning_isolates_dispatcher_address_collision() {
1273        let requests = vec![
1274            (MULTICALL3_ADDRESS, U256::from(1u64)),
1275            (addr(0x02), U256::from(2u64)),
1276            (addr(0x03), U256::from(3u64)),
1277        ];
1278        let plans = plan_calls(&requests, &cfg(100, 10));
1279        assert_eq!(
1280            plans[0],
1281            CallPlan::Single {
1282                target: MULTICALL3_ADDRESS,
1283                slots: vec![U256::from(1u64)],
1284            }
1285        );
1286        assert!(matches!(&plans[1], CallPlan::Multi { targets } if targets.len() == 2));
1287    }
1288
1289    #[test]
1290    fn multi_target_overrides_include_dispatcher_and_extractors() {
1291        let plan = CallPlan::Multi {
1292            targets: vec![
1293                (addr(0x02), vec![U256::from(1u64)]),
1294                (addr(0x03), vec![U256::from(2u64)]),
1295            ],
1296        };
1297        let extractor = Bytes::from_static(STORAGE_EXTRACTOR_CODE);
1298        let overrides = overrides_for_plan(&plan, &extractor);
1299        assert_eq!(overrides.len(), 3);
1300        assert_eq!(
1301            overrides[&MULTICALL3_ADDRESS].code.as_ref(),
1302            Some(multicall3_runtime_code())
1303        );
1304        assert_eq!(overrides[&addr(0x02)].code.as_ref(), Some(&extractor));
1305        assert_eq!(overrides[&addr(0x03)].code.as_ref(), Some(&extractor));
1306    }
1307
1308    #[test]
1309    fn call_many_grouping_respects_request_budget() {
1310        let plans = vec![
1311            CallPlan::Single {
1312                target: addr(0x01),
1313                slots: (0..6u64).map(U256::from).collect(),
1314            },
1315            CallPlan::Single {
1316                target: addr(0x02),
1317                slots: (0..6u64).map(U256::from).collect(),
1318            },
1319            CallPlan::Single {
1320                target: addr(0x03),
1321                slots: (0..2u64).map(U256::from).collect(),
1322            },
1323        ];
1324        let groups = group_plans_for_call_many(plans, 10);
1325        // 6 + 6 > 10 → split; 6 + 2 ≤ 10 → packed together.
1326        assert_eq!(groups.len(), 2);
1327        assert_eq!(groups[0].len(), 1);
1328        assert_eq!(groups[1].len(), 2);
1329        let total: usize = groups
1330            .iter()
1331            .flatten()
1332            .map(CallPlan::request_slot_count)
1333            .sum();
1334        assert_eq!(total, 14);
1335    }
1336
1337    #[test]
1338    fn multi_target_response_decodes_per_target_failures() {
1339        let targets = vec![
1340            (addr(0x02), vec![U256::from(1u64), U256::from(2u64)]),
1341            (addr(0x03), vec![U256::from(3u64)]),
1342        ];
1343        let response = IMulticall3::aggregate3Call::abi_encode_returns(&vec![
1344            IMulticall3::Result {
1345                success: true,
1346                returnData: pack_slots_calldata(&[U256::from(11u64), U256::from(22u64)]),
1347            },
1348            IMulticall3::Result {
1349                success: false,
1350                returnData: Bytes::new(),
1351            },
1352        ]);
1353        let results = decode_multi_target_response(&targets, &response);
1354        assert_eq!(results.len(), 3);
1355        assert!(matches!(results[0], (_, _, Ok(v)) if v == U256::from(11u64)));
1356        assert!(matches!(results[1], (_, _, Ok(v)) if v == U256::from(22u64)));
1357        assert!(results[2].2.is_err());
1358    }
1359}