Skip to main content

EvmCache

Struct EvmCache 

Source
pub struct EvmCache { /* private fields */ }
Expand description

EVM cache with lazy-loading RPC backend.

Uses foundry-fork-db for intelligent caching and request deduplication. Storage and account data is fetched on-demand when accessed during EVM execution, eliminating the need for expensive access list prefetching.

Implementations§

Source§

impl EvmCache

Source

pub fn builder<P>(provider: Arc<P>) -> EvmCacheBuilder<P>
where P: Provider<AnyNetwork> + 'static,

Start a fluent EvmCacheBuilder over the given provider.

Preferred over the positional with_cache / new constructors for readability.

Source

pub async fn new<P>(provider: Arc<P>) -> Self
where P: Provider<AnyNetwork> + 'static,

Create a new EvmCache with a SharedBackend that lazily fetches from RPC.

The backend spawns a background handler task that manages RPC requests and deduplicates concurrent requests for the same data.

§Runtime requirement

RPC-backed operation requires a multi-thread tokio runtime (#[tokio::main(flavor = "multi_thread")] or tokio::runtime::Builder::new_multi_thread()). The direct RPC callbacks (eth_call and batch eth_getStorageAt) drive async work synchronously via tokio::task::block_in_place, which is unsupported on a current-thread runtime. On a current-thread runtime those callbacks degrade to typed errors rather than panicking.

Source

pub async fn at_block<P>(provider: Arc<P>, block: BlockId) -> Self
where P: Provider<AnyNetwork> + 'static,

Create a new EvmCache pinned to an explicit block.

Prefer this over new when reproducibility matters and the caller has already chosen the fork block.

Source

pub async fn with_cache<P>( provider: Arc<P>, block: BlockId, cache_config: Option<CacheConfig>, spec_id: SpecId, ) -> Self
where P: Provider<AnyNetwork> + 'static,

Create a new EvmCache with disk-based caching.

This enables several caching features:

  1. Unified EVM state: Accounts + storage loaded from evm_state.bin (bincode)
  2. Bytecode caching: Contract bytecodes from bytecodes.bin
  3. Immutable data: Token decimals
§Runtime requirement

RPC-backed operation requires a multi-thread tokio runtime (#[tokio::main(flavor = "multi_thread")] or tokio::runtime::Builder::new_multi_thread()). The direct RPC callbacks (eth_call and batch eth_getStorageAt) drive async work synchronously via tokio::task::block_in_place, which is unsupported on a current-thread runtime. On a current-thread runtime those callbacks degrade to typed errors rather than panicking.

Source

pub async fn with_cache_capacity<P>( provider: Arc<P>, block: BlockId, cache_config: Option<CacheConfig>, spec_id: SpecId, shared_memory_capacity: SharedMemoryCapacity, ) -> Self
where P: Provider<AnyNetwork> + 'static,

Like with_cache but takes an explicit SharedMemoryCapacity controlling per-context EVM working-memory pre-allocation. This is what EvmCacheBuilder::build calls; prefer the builder. With SharedMemoryCapacity::Auto the buffer is sized from the layer-2 storage loaded at construction (e.g. a bincode state file).

Source

pub fn from_backend( backend: SharedBackend, blockchain_db: BlockchainDb, block: BlockId, chain_id: u64, block_number: Option<u64>, basefee: Option<u64>, spec_id: SpecId, ) -> Self

Create a new EvmCache from an existing SharedBackend.

Useful when you want to share a backend between multiple caches (e.g. parallel simulation threads).

Shared pinned block. A SharedBackend owns a single pinned fork height. Calling set_block / repin_to_block on any cache built from the same backend re-pins the RPC fork height for all of them. Sibling caches sharing one backend should agree on a block and not re-pin independently; build separate backends if they must fork at different heights.

Source

pub fn flush(&self) -> Result<()>

Flush the cache state to disk.

This persists:

  1. Unified EVM state (accounts + storage) to evm_state.bin (bincode)
  2. Contract bytecodes to bytecodes.bin
  3. Immutable data (token decimals) to immutable_data.bin

Call this after loading hot contract state and running simulations to speed up subsequent runs. The cache is also automatically flushed when the EvmCache is dropped.

Source

pub fn cache_config(&self) -> Option<&CacheConfig>

Get the cache configuration, if any.

Returns None when the cache is purely in-memory (no disk persistence), i.e. constructed without a CacheConfig or via from_backend.

Source

pub fn with_blockchain_db_mut<R>( &mut self, f: impl FnOnce(&BlockchainDb) -> R, ) -> R

Run a synchronous direct mutation against the underlying BlockchainDb and invalidate the memoized snapshot base afterwards.

This is the preferred escape hatch for unavoidable layer-2 map writes such as accounts().write().insert(...) or storage().write().insert(...). The closure still bypasses the CacheDB overlay and the normal write funnel, so use higher-level mutators when they can express the change. Unlike unchecked_blockchain_db, this wrapper keeps the copy-on-write snapshot base honest automatically after in-place overwrites whose map cardinality does not change.

Source

pub fn unchecked_blockchain_db(&self) -> &BlockchainDb

Get an unchecked reference to the underlying BlockchainDb (the layer-2 backend store of accounts, storage, and bytecodes).

This exposes an internal store and bypasses the cache’s two-layer consistency model: reads here see only the backend layer, not the CacheDB overlay, and any writes performed through it skip the overlay. Prefer higher-level accessors or with_blockchain_db_mut for direct synchronous writes.

§Snapshot base

Writing layer 2 directly through this unchecked handle also bypasses the memoized copy-on-write snapshot base (Pillar A). The next snapshot only performs a count/absence growth scan over layer 2, which catches lazy RPC-populated accounts/slots because that path only appends at a fixed block. It does not catch direct in-place changes where cardinality is unchanged: overwriting an existing storage slot, or changing an existing account’s info/code/balance without adding a new account, can leave a stale snapshot base. After such a direct write, call invalidate_snapshot_base (or re-pin via set_block) before the next snapshot. Writes via the crate’s own mutators (inject_storage_batch, apply_update, the inject_* helpers, the purges) keep the base honest automatically.

Source

pub fn unchecked_backend(&self) -> &SharedBackend

Get an unchecked reference to the underlying SharedBackend (the lazy RPC-backed fetcher shared across clones).

This exposes an internal handle and bypasses the cache’s two-layer consistency model: it reads/fetches directly without consulting the CacheDB overlay. Prefer the higher-level accessors; use with care.

§Snapshot base

Lazy RPC fetches through this backend only append missing accounts/slots at the pinned block, so the snapshot growth scan catches them without an explicit invalidation. Direct SharedBackend::insert_or_update_storage / insert_or_update_address calls are different: they enqueue a background handler request that can rewrite layer-2 entries in place, leaving the memoized copy-on-write base stale at an unchanged slot/account count.

If you use those helpers directly, first synchronize with the backend handler by reading back the updated account/slot through SharedBackend (for example via basic_ref / storage_ref), then call invalidate_snapshot_base before the next snapshot. Calling invalidate_snapshot_base immediately after insert_or_update_* is not, by itself, a guarantee that the queued update has been applied before the next snapshot.

Source

pub fn db_mut(&mut self) -> &mut ForkCacheDB

Get a mutable reference to the underlying ForkCacheDB (the layer-1 CacheDB overlay).

This exposes an internal and bypasses the cache’s two-layer consistency model: writes made here land only in the overlay and are not mirrored into the BlockchainDb backend, so parallel tasks sharing the backend will not see them. Prefer the higher-level mutators; use with care.

Source

pub fn rpc_call( &self, to: Address, calldata: Bytes, ) -> Option<Result<Bytes, RpcError>>

Make a direct RPC eth_call to the node, bypassing revm simulation.

This is much faster than call_raw for batch operations because the RPC node has all state in memory and doesn’t need lazy storage fetching. Returns None if no RPC caller is available (e.g. from_backend constructor).

§Panics

Must be called from within a multi-thread tokio runtime: the callback drives the async eth_call to completion via tokio::task::block_in_place. On a current-thread runtime (or with no runtime), the callback degrades to an Err rather than panicking, but block_in_place itself will panic if invoked from a non-worker thread of a multi-thread runtime.

Source

pub fn storage_batch_fetcher(&self) -> Option<&StorageBatchFetchFn>

Get the batch storage fetcher, if available.

Returns None when constructed via from_backend (no provider available).

§Panics

The returned StorageBatchFetchFn must be invoked from within a multi-thread tokio runtime: it drives concurrent eth_getStorageAt calls to completion via tokio::task::block_in_place. On a current-thread runtime (or with no runtime) it degrades to an Err result for every requested slot rather than panicking, but block_in_place itself will panic if invoked from a non-worker thread of a multi-thread runtime.

Source

pub fn account_proof_fetcher(&self) -> Option<&AccountProofFetchFn>

Get the account/root proof fetcher, if available.

Returns None when constructed via from_backend (no provider available) unless a fetcher was injected via set_account_proof_fetcher.

§Panics

The returned AccountProofFetchFn must be invoked from within a multi-thread tokio runtime: it drives eth_getProof calls to completion via tokio::task::block_in_place. On a current-thread runtime (or with no runtime) it degrades to an Err result for every requested address rather than panicking, but block_in_place itself will panic if invoked from a non-worker thread of a multi-thread runtime.

Source

pub fn block_state_diff_fetcher(&self) -> Option<&BlockStateDiffFetchFn>

Get the block state-diff fetcher, if available.

Returns None when constructed via from_backend (no provider available) unless a fetcher was injected via set_block_state_diff_fetcher.

Source

pub fn inject_storage_batch(&mut self, results: &[(Address, U256, U256)])

Inject batch-fetched storage values directly into BlockchainDb (layer 2).

This bypasses SharedBackend and makes values available for subsequent storage_ref() calls and EVM SLOADs. Used after StorageBatchFetchFn returns results to populate the cache in bulk.

Takes &mut self (as of Pillar A) so it can mark each touched address dirty for the memoized copy-on-write base; the write itself is still a direct layer-2 backend write. Overwriting an existing slot at an unchanged slot count is invalidated here too, since the refresh_base growth scan only catches length changes.

Source

pub fn inject_storage_batch_fresh(&mut self, results: &[(Address, U256, U256)])

Inject freshly-fetched storage values, healing both cache layers.

Like inject_storage_batch this writes each value into the BlockchainDb backend (layer 2). Additionally, for any address that already has a CacheDB overlay entry (layer 1), it writes the slot into that overlay too.

This matters because both snapshot and the synchronous EVM SLOAD path let the overlay win over the backend. A correction written only to layer 2 would be shadowed by a stale layer-1 slot, so the cache could never converge — the freshness validator would re-detect the same change and re-correct it every cycle. Writing through the overlay keeps the layer that wins authoritative.

It deliberately does not create a new overlay account for an address that has none: such a slot is layer-2-only (e.g. cold prefetch), where the backend write is already authoritative and materializing an overlay entry would pollute layer 1 and could shadow later RPC reads.

Source

pub fn prewarm_slots(&mut self, requests: &[(Address, U256)]) -> PrewarmReport

Bulk-load the given slots into the cache at its pinned block.

Fetches through the installed StorageBatchFetchFn — bulk eth_call extraction by default, so thousands of slots (across many contracts) arrive in a handful of calls — and injects every successfully fetched value into layer 2 via inject_storage_batch, the cold-prefetch write. Use it to prewarm a declared working set (an AMM pool’s tick range, a protocol’s config slots) before entering a simulation or reactive loop, complementing the recorded working sets that prefetch_registry replays.

Duplicate pairs are fetched once each and injected idempotently. Returns how many slots loaded and which pairs failed; failures leave the cache unchanged (those slots lazily point-read later as usual).

Source

pub fn apply_update(&mut self, update: &StateUpdate) -> StateDiff

Apply a single targeted StateUpdate, returning a StateDiff of what actually changed.

This is the single primitive that writes the state-update vocabulary across both cache layers with one consistent, documented policy. It is synchronous and infallible — a write, not a fetch, so it never touches RPC and never errors. See the state_update module for the dual-layer write-through policy and the diff semantics.

  • StateUpdate::Slot — write value into the backend (layer 2) always, and into the overlay (layer 1) only if an overlay account already exists. Records a SlotChange only when the value actually changes (old.unwrap_or(ZERO) != value).
  • StateUpdate::SlotDeltarelative, cold-aware. If the slot has a cached value, write the saturating delta through the same path and record a SlotChange iff it changed; if the slot is cold (absent from both layers), apply nothing and surface a SkippedDelta in diff.skipped.
  • StateUpdate::BalanceDeltarelative, cold-aware native-balance update. If the account is present in either layer, apply the saturating delta to its balance (nonce/code preserved) write-through and record an AccountChange iff it changed; if the account is cold (absent from both layers), apply nothing and surface a SkippedBalanceDelta in diff.skipped_balances (no default account is materialized).
  • StateUpdate::Account — load the current AccountInfo from the cached layers (no RPC), apply each Some patch field (recomputing the code hash when code is set), then write through with the same layer policy. Records an AccountChange with Some((old, new)) only for fields that changed. If the account is cold (absent from both layers), apply nothing and surface a SkippedAccountPatch in diff.skipped_accounts.
  • StateUpdate::AccountUpsert — same patch semantics, but intentionally materializes a cold/default account when absent from both layers.
  • StateUpdate::Purge — dispatch to the matching purge layer logic and record a PurgeRecord.
§Warning — relative updates can be skipped

A cold-aware update targeting a cold address is dropped, not applied unless it is an explicit StateUpdate::AccountUpsert. Because a skip produces no change, it is invisible to the changes-only StateDiff::is_empty / StateDiff::len success check, so after applying cold-aware updates the caller must inspect StateDiff::has_skipped (or the skipped_* fields) and fetch+seed the cold target.

let contract = Address::repeat_byte(0x01);
let diff = cache.apply_update(&StateUpdate::slot(contract, U256::from(0), U256::from(42)));
assert_eq!(diff.slots.len(), 1);
Source

pub fn apply_updates(&mut self, updates: &[StateUpdate]) -> StateDiff

Apply a batch of StateUpdates left-to-right, merging each per-update StateDiff.

Later updates observe the effect of earlier ones: two Slot writes to the same key record old → a then a → b. Like apply_update this is synchronous and infallible.

§Performance — batched single-lock fast-path

Consecutive Slot/SlotDelta writes are processed holding the backend storage write-guard once for the run (the overlay map is lock-free), so a bulk slot seed pays one lock acquisition instead of one read + one write lock per slot. Apply order is preserved: when an Account/BalanceDelta/ Purge update is reached the guard is dropped first (those take the accounts() / storage() locks themselves — holding the storage write-guard across them would deadlock the non-reentrant RwLock), the update is processed via apply_update, then the guard is lazily re-acquired on the next slot run. The result is byte-identical to folding apply_update over the batch.

§Warning — relative updates can be skipped

See apply_update: a cold relative update is dropped, not applied, and is invisible to StateDiff::is_empty / StateDiff::len. After a batch with relative updates, check StateDiff::has_skipped.

Source

pub fn modify_slot( &mut self, address: Address, slot: U256, f: impl FnOnce(Option<U256>) -> Option<U256>, ) -> Option<SlotChange>

Read-modify-write one storage slot through a caller-supplied transform.

The general closure escape hatch behind StateUpdate::SlotDelta (the data-level form flows through apply_update; this is for arbitrary transforms). f is called with the current cached value (overlay ▸ backend ▸ None when the slot is cold) and decides the new value:

  • Some(new) writes new through both layers (the same write path as StateUpdate::Slot) and returns a SlotChange iff it changed (old.unwrap_or(ZERO) != new);
  • None writes nothing and returns None.

The caller owns the cold/overflow policy. To skip cold slots (the cold-aware read-modify-write rule), map through the Option: |cur| cur.map(|v| v.saturating_add(amount)) leaves a cold slot untouched. To write an absolute value regardless, ignore the argument: |_| Some(v).

let token = Address::repeat_byte(0x01);
let slot = U256::from(0);
// Saturating +100, but only if the slot is already hot.
let change = cache.modify_slot(token, slot, |cur| cur.map(|v| v.saturating_add(U256::from(100))));
Source

pub fn modify_account_balance( &mut self, address: Address, f: impl FnOnce(Option<U256>) -> Option<U256>, ) -> Option<AccountChange>

Read-modify-write an account’s native balance through a caller-supplied transform.

The closure analog of StateUpdate::BalanceDelta (the data-level form flows through apply_update; this is for arbitrary transforms). f is called with the account’s current native balance (overlay ▸ backend ▸ None when the account is absent from both layers) and decides the new balance:

  • Some(new) writes new through both layers — backend always, overlay only if an overlay account already exists — preserving the account’s nonce and code, and returns an AccountChange (balance only) iff the balance changed;
  • None writes nothing (no account is materialized) and returns None.

“Cold” for a balance is the account being absent from both layers — or present in the overlay as revm NotExisting (absent to the EVM), which the internal account read also treats as cold, mirroring DbAccount::info(). To skip cold accounts, map through the Option: |cur| cur.map(|v| v.saturating_add(amount)).

let acct = Address::repeat_byte(0x01);
// Saturating +100, but only if the account's balance is already known.
let change = cache.modify_account_balance(acct, |cur| cur.map(|v| v.saturating_add(U256::from(100))));
Source

pub fn set_storage_batch_fetcher(&mut self, f: StorageBatchFetchFn)

Set (or replace) the batch storage fetcher.

This is the seam the freshness controller and tests use to drive re-verification without a live provider: a stubbed StorageBatchFetchFn can be injected over a mocked-provider cache. Production callers can also inject their own transport, retry, batching, or rate-limiting strategy here. Once replaced, the cache’s StorageBatchConfig no longer controls batching; the custom fetcher is responsible for honoring the StorageBatchFetchFn contract.

Source

pub fn set_account_proof_fetcher(&mut self, f: AccountProofFetchFn)

Set (or replace) the account/root proof fetcher.

This is the seam account-target resyncs and account-level freshness use to drive eth_getProof fetches without a live provider: a stubbed AccountProofFetchFn can be injected over a mocked-provider cache, mirroring set_storage_batch_fetcher.

Source

pub fn set_block_state_diff_fetcher(&mut self, f: BlockStateDiffFetchFn)

Set (or replace) the block state-diff fetcher.

This is the seam trace-backed reactive resync uses to resolve matching targets from one block-level debug trace before falling back to storage or account proof point reads.

Source

pub fn set_account_fields_fetcher(&mut self, f: AccountFieldsFetchFn)

Set (or replace) the bulk account-fields fetcher.

This is the seam verify_code_seeds (and the cold-start verify_code phase) reads through: a stubbed AccountFieldsFetchFn can be injected over a mocked-provider cache, mirroring set_storage_batch_fetcher.

Source

pub fn account_fields_fetcher(&self) -> Option<&AccountFieldsFetchFn>

The installed bulk account-fields fetcher, if any.

Some on provider-backed caches (default-wired to fetch_account_fields_bulk); None on from_backend caches until one is installed via set_account_fields_fetcher.

Source

pub fn cached_storage_value(&self, address: Address, slot: U256) -> Option<U256>

Return the currently-cached value for a storage slot, if any.

Mirrors what the EVM would SLOAD from the cached layers (it never touches RPC, unlike read_storage_slot):

  1. The CacheDB overlay (layer 1) wins: if the overlay account holds the slot, return it.
  2. Match revm’s CacheDB::storage_ref: if the overlay account exists but does not hold the slot, and its account_state is StorageCleared or NotExisting, the live EVM reads the slot as ZERO and never consults the backend — so return Some(U256::ZERO), not the (shadowed) backend value. Returning the backend value here would let a SlotDelta/modify_slot compute a delta against a base the EVM never sees (silent corruption) and would mis-record apply_slot’s old.
  3. Otherwise fall through to the BlockchainDb backend (layer 2); None when neither layer has seen the slot.
Source

pub fn verify_slots( &mut self, slots: &[(Address, U256)], ) -> Result<Vec<SlotChange>>

Re-fetch the given slots via the batch fetcher, compare to the currently cached values, and inject the ones that changed.

For each slot whose freshly-fetched value differs from the cached value, the fresh value is written into the cache via inject_storage_batch_fresh and a SlotChange is recorded. Slots that are unchanged, or that the fetcher fails to return, are left as-is. Returns the set of changed slots.

Requires a batch fetcher (set at construction or via set_storage_batch_fetcher); errors if none is available. This is the synchronous main-thread primitive; the background validator performs the equivalent comparison against a snapshot.

Source

pub fn reconcile_slots( &mut self, slots: &[(Address, U256)], ) -> Result<Vec<SlotChange>>

Reconciliation re-read used by EventPipeline::reconcile.

Like verify_slots it fetches the requested slots, injects the ones that changed, and returns the changed set — but it is honest about reachability: it errors not only when no batch fetcher is configured, but also when a non-empty request could not fetch any slot (a total fetch failure — e.g. the default RPC fetcher invoked with no usable runtime, or an unreachable provider). Reconciliation that silently “verified nothing” would be a false all-clear, so it surfaces as an error for the caller to retry. A partially-successful fetch returns Ok with whatever changed.

Source

pub fn purge_account(&mut self, addr: Address)

Purge an account fully from both cache layers: its AccountInfo (balance/nonce/code hash) and all of its storage.

Removes addr from the CacheDB overlay accounts map, the BlockchainDb accounts map, and the BlockchainDb storage map, so the next access re-fetches a clean account from RPC. This is the account-level counterpart to the storage-only purge_contract_storage: use it when an address is fully volatile (no pinned slots) and even its balance/nonce/code can no longer be trusted.

Source

pub fn chain_id(&self) -> u64

Get the chain ID used for EVM simulations (the CHAINID opcode).

Source

pub fn set_chain_id(&mut self, chain_id: u64)

Set the chain ID reported to simulations via the CHAINID opcode.

Prefer setting this at construction through EvmCacheBuilder::chain_id. This setter exists for cases where the chain ID must change after construction. It takes effect on the next snapshot / build_evm; existing snapshots and overlays keep the chain ID captured when they were created.

Source

pub fn checkpoint(&self) -> Cache

Take a low-level, same-thread checkpoint of the CacheDB overlay for in-place restore.

Clones the inner revm::database::Cache (the layer-1 overlay’s accounts and storage) only — not the underlying database wrapper or the BlockchainDb backend. Pair with restore to roll the overlay back on the same EvmCache after speculative mutations (this is how the balance-slot scan probes and rewinds).

For cross-thread fan-out use snapshot instead: it merges both layers into an Arc<EvmSnapshot> that is Send + Sync and can be shared with parallel simulators via EvmOverlay.

Source

pub fn restore(&mut self, checkpoint: Cache)

Restore the CacheDB overlay from a checkpoint taken with checkpoint.

Overwrites the layer-1 overlay wholesale with checkpoint, discarding any overlay mutations made since it was taken. The BlockchainDb backend is untouched. This is the in-place counterpart to the cross-thread snapshot / EvmOverlay path.

Source

pub fn session(&mut self) -> EvmSession<'_>

Create a new session for executing multiple operations.

Changes made within the session are only committed to the underlying database when session.commit() is called. Dropping the session without calling commit discards all changes made during the session.

Source

pub fn snapshot(&mut self) -> Arc<EvmSnapshot>

Create an immutable, Send + Sync snapshot of the current EVM state for cross-thread fan-out (the copy-on-write two-tier view, Pillar A).

Rather than deep-copying both layers, this memoizes the cold layer-2 (BlockchainDb) index as an Arc-shared base — reused as a cheap Arc::clone when layer 2 is unchanged, rebuilt copy-on-write only for the addresses that changed — and folds the hot layer-1 (CacheDB overlay) delta over it. Layer-1 values shadow the base on reads, reproducing the live cache’s layered semantics; the resulting EvmSnapshot is shared across threads via Arc. Its cost tracks changed state, not total state. (The retained snapshot_deep_clone is the read-equivalent O(total) reference, kept for benchmarking/testing.)

Takes &mut self because it refreshes and memoizes the base. For cheap same-thread save/restore of just the overlay, prefer checkpoint / restore instead.

Source

pub fn invalidate_snapshot_base(&mut self)

Force the next snapshot to rebuild the memoized copy-on-write base from scratch (Pillar A).

The crate’s own mutators keep the base honest automatically. This is the escape-hatch re-honest hook: call it after writing layer 2 directly through unchecked_blockchain_db or unchecked_backend — those bypass the write funnel, and in-place changes at unchanged cardinality are invisible to the snapshot growth scan. That includes overwriting an existing storage slot and changing an existing account’s info/code/balance without adding a new account. Lazy RPC-populated data does not need this call because it only appends accounts/slots, which the growth scan catches.

When using SharedBackend::insert_or_update_* through unchecked_backend, remember those helpers only enqueue a background update. Synchronize/read back the update through SharedBackend before the next snapshot; invalidate_snapshot_base alone is not a backend-handler synchronization point. Once the direct write is present, calling this before the next snapshot guarantees it reflects that write rather than a stale memoized value. Over-invalidation is always safe (Decision D2); the only cost is one full base rebuild on the next snapshot.

Source

pub fn set_block(&mut self, block: BlockId)

Update the block that RPC fetches are pinned to.

This re-pins the SharedBackend and the batch storage fetcher to block, so subsequent RPC fetches read state at the new block.

§Block-context contract

To prevent the EVM block context from silently diverging from the pinned block, when block is a concrete BlockId::Number(Number(n)) this also updates block_number (the NUMBER opcode) to n. For tag-based block ids (latest, pending, hashes, etc.), the height is not statically known, so block_number is cleared.

basefee (the BASEFEE opcode) is cleared on every block change and on every non-concrete tag/hash pin call because deriving it requires fetching the block header, which this synchronous method cannot do. Callers that change blocks should refresh it via set_block_context after fetching the new header. Prefer repin_to_block when re-pinning to a concrete height, since it keeps block_number and the pinned block in lockstep.

Source

pub fn block(&self) -> BlockId

Get the block that RPC fetches are currently pinned to.

Source

pub fn snapshot_generation(&self) -> u64

Monotonic generation counter for snapshot consistency (G6).

Increments on every targeted state write (apply_update, apply_updates, modify_slot — and therefore everything built on them: reactive ingestion, freshness corrections, fresh injections) and on block re-pins (set_block, advance_block). Cold prefetch (inject_storage_batch) and lazy backend fetches do not increment it: they materialize the pinned block’s existing state rather than changing it.

The magnitude is opaque — how much one call increments it is unspecified — so compare values for equality only.

The fan-out pattern: read the generation, take the snapshot, read the generation again. If the two reads differ, state mutated in between (e.g. your event loop applied part of a block between the reads) — discard and re-snapshot to avoid fanning out simulations over a mid-block state.

let snapshot = loop {
    let generation = cache.snapshot_generation();
    let snapshot = cache.snapshot();
    if cache.snapshot_generation() == generation {
        break snapshot;
    }
    // A mutation interleaved: try again at the next stable point.
};
Source

pub fn set_timestamp(&mut self, timestamp: Option<u64>)

Set a custom timestamp for EVM simulations.

When set, all EVM executions will use this timestamp instead of the current system time. This is useful for simulating future blocks to predict when time-dependent opportunities (like yield farming rewards) become profitable.

Pass None to use the current system time (default behavior).

Source

pub fn timestamp(&self) -> Option<u64>

Get the current timestamp override, if any.

Returns None if the cache is using the current system time (default).

Source

pub fn block_number(&self) -> Option<u64>

Get the block number used for EVM simulations (the NUMBER opcode).

Fetched from the pinned block’s header at construction. Concrete-number pins set it via set_block / repin_to_block; tag/hash pins clear it because their height is not statically known. None means revm falls back to 0, which can steer contracts that branch on block.number down a different code path. Override directly via set_block_context.

Source

pub fn basefee(&self) -> Option<u64>

Get the base fee per gas used for EVM simulations (the BASEFEE opcode).

Fetched from the pinned block’s header at construction. None means revm falls back to 0. This is cleared by set_block / repin_to_block when the pin changes, and by non-concrete tag/hash pin calls because those can drift without a concrete number in the API. Refresh it with set_block_context after fetching a new header if BASEFEE accuracy matters.

Source

pub fn set_block_context( &mut self, block_number: Option<u64>, basefee: Option<u64>, )

Update the block context for EVM simulations.

Call this when the simulation block changes (e.g. at the start of each search cycle) to keep NUMBER and BASEFEE opcodes accurate.

Source

pub fn set_basefee(&mut self, basefee: U256)

Set the block base fee (the BASEFEE opcode) for subsequent simulations, propagated into the next snapshot.

Offline caches built over a mocked provider have no fetched block header, so the base fee is unset (and the BASEFEE opcode reads 0). Use this to install one explicitly — it determines the priority fee (gas_price − basefee) credited to the beneficiary, and thus the coinbase_payment a simulate_bundle reports.

The cache stores the base fee as a u64 (matching the block header and the EvmSnapshot field), so a U256 larger than u64::MAX is saturated.

Source

pub fn set_coinbase(&mut self, coinbase: Option<Address>)

Override the block beneficiary (the COINBASE opcode) for subsequent simulations.

Set this when simulating logic that reads block.coinbase (e.g. MEV/builder tip accounting). None lets revm use its default beneficiary.

Source

pub fn set_prevrandao(&mut self, prevrandao: Option<B256>)

Override prevrandao (the PREVRANDAO opcode, the post-merge header mix hash) for subsequent simulations.

Set this when reproducing contracts that source on-chain randomness from block.prevrandao. None leaves revm’s default in place.

Source

pub fn set_block_gas_limit(&mut self, gas_limit: Option<u64>)

Override the block gas limit (the GASLIMIT opcode) for subsequent simulations.

Set this when simulating logic that reads block.gaslimit. None lets revm use its default.

Source

pub fn coinbase(&self) -> Option<Address>

Get the block beneficiary used for EVM simulations (the COINBASE opcode).

Fetched from the pinned block’s header at construction, refreshed by advance_block, or overridden via set_coinbase. None means revm uses its default beneficiary.

Source

pub fn prevrandao(&self) -> Option<B256>

Get prevrandao used for EVM simulations (the PREVRANDAO opcode, the post-merge header mix hash).

Fetched from the pinned block’s header at construction, refreshed by advance_block, or overridden via set_prevrandao. None leaves revm’s default in place.

Source

pub fn block_gas_limit(&self) -> Option<u64>

Get the block gas limit used for EVM simulations (the GASLIMIT opcode).

Fetched from the pinned block’s header at construction, refreshed by advance_block, or overridden via set_block_gas_limit. None lets revm use its default.

Source

pub fn set_block_context_requirements(&mut self, reqs: BlockContextRequirements)

Set which block-context header fields subsequent advance_block calls require to be present.

See BlockContextRequirements. Under strict enforcement, advance_block rejects a header missing a required field rather than silently defaulting it.

Source

pub fn advance_block<H: BlockHeader>( &mut self, header: &H, ) -> Result<(), BlockContextError>

Engine-driven per-block env refresh from a canonical block header.

First validates the header against the configured BlockContextRequirements (set via set_block_context_requirements or the strict builder path). Under strict/partial requirements a header missing a required field is rejected with BlockContextError instead of being silently defaulted; under the lenient default this never errors.

On success it refreshes the full EVM block env from the header — block number (NUMBER), base fee (BASEFEE), beneficiary (COINBASE), prevrandao (PREVRANDAO), gas limit (GASLIMIT) and timestamp — and re-pins every RPC fetch path (the SharedBackend lazy fallback, the batch storage fetcher, and the account-proof fetcher) to the header’s block number, so a lazy miss after the advance reads state at the advanced block, in lockstep with the env. Intended to be driven once per canonical block (e.g. by the reactive runtime as new headers arrive).

Unlike set_block, this does not invalidate the memoized COW snapshot base: an advance is a forward roll of the same live view (canonical mutations flow through the write funnel, which already marks the base dirty; lazy fetches stay insert-only-on-miss, which the base’s growth scan catches), whereas set_block is a wholesale re-fork that must rebuild layer 2. Re-pinning to an older block is a re-fork, not an advance — use set_block for that.

Source

pub fn repin_to_block(&mut self, block_number: u64)

Re-pin the cache to a specific block number.

Updates the SharedBackend pinned block, the batch fetcher block, and the EVM block context (NUMBER opcode) in lockstep. The current basefee is cleared because it cannot be refreshed synchronously; callers should set it via set_block_context after fetching the new block header if BASEFEE accuracy matters.

Source

pub async fn ensure_account(&mut self, address: Address) -> Result<()>

Ensure an account is loaded into the cache.

With the lazy-loading backend, this is optional - accounts are fetched automatically when accessed. However, you can use this to pre-warm the cache for specific accounts.

Source

pub fn read_storage_slot( &mut self, address: Address, slot: U256, ) -> Result<U256>

Read a single storage slot through the SharedBackend (BlockchainDb -> RPC fallback).

After purge_contract_slots removes a slot from BlockchainDb, this method fetches fresh data from RPC and caches it in BlockchainDb. Subsequent EVM SLOADs find the value there without additional RPC calls.

Source

pub fn insert_storage_slot( &mut self, address: Address, slot: U256, value: U256, ) -> Result<()>

Write a raw storage slot value directly into the CacheDB layer.

Subsequent EVM SLOADs for this (address, slot) will read the injected value without any RPC call. Used for hot-state injection where we already know the current on-chain value from WebSocket events.

Source

pub fn seed_erc20_balance_slots( &mut self, slots: impl IntoIterator<Item = (Address, U256)>, )

Pre-seed known ERC20 balanceOf mapping base slots, keyed by token.

Each (token, slot) records the storage slot of the token’s mapping(address => uint256) balances, letting set_erc20_balance_with_slot_scan skip its discovery pass for that token and write the balance directly. Seeded slots are assumed to use Solidity’s keccak(key‖slot) layout (use seed_erc20_balance_layouts for Vyper/Solady tokens). Seeding a wrong slot is self-correcting: the write is verified and a fresh discovery pass runs (evicting the bad seed) if it fails. Later entries overwrite earlier ones for the same token.

Source

pub fn seed_erc20_balance_layouts( &mut self, mappings: impl IntoIterator<Item = TrackedMapping>, )

Pre-seed known ERC20 balance mapping descriptors (base slot and layout), keyed by TrackedMapping::contract.

The layout-aware companion to seed_erc20_balance_slots: use this for Vyper (keccak(slot‖key)) or Solady (packed) tokens whose layout is known up front, so set_erc20_balance_with_slot_scan writes the correct slot without a discovery pass.

Source

pub fn insert_mapping_storage_slot( &mut self, contract: Address, slot: U256, slot_address: Address, value: U256, ) -> Result<()>

Write a value into a Solidity mapping(address => ...) entry on contract, at the mapping declared at base slot slot.

Computes the entry’s storage key as keccak256(abi.encode(slot_address, slot)) — Solidity’s layout for an address-keyed mapping — and writes value there in the CacheDB overlay. Used to forge ERC20 balances and allowances without an on-chain transfer.

§Errors

Returns an error if the underlying CacheDB storage insert fails (e.g. the account cannot be loaded from the backend).

Source

pub fn call_raw_with_inspector<I>( &mut self, from: Address, to: Address, calldata: Bytes, tx: &TxConfig, inspector: I, ) -> Result<(ExecutionResult, I)>
where I: for<'a> Inspector<Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>>,

Run a call with a composable revm::Inspector attached, without committing state (the journal is reverted after execution), returning the execution result and the inspector moved back out so you can read what it captured.

This is the cache-level counterpart to EvmOverlay::call_raw_with_inspector. Unlike the overlay form it runs directly against the cache’s database, so any missing state is fetched lazily during the call — discovery works on a cold fork with no pre-warming.

Source

pub fn trace_hashed_slots( &mut self, from: Address, to: Address, calldata: Bytes, known_keys: &[B256], ) -> Result<Vec<HashSlotAccess>>

Discover every hash-derived storage slot a call reads, factored into HashSlotAccesses (mapping keys, base slot, layout, exact slot, value).

This is the general, ERC-20-agnostic entry point: it works for any mapping (balances, allowances, protocol positions, …) and any layout (Solidity / Vyper / Solady / nested), in a single simulation. known_keys are words — typically addresses via Address::into_word — used to anchor key/slot disambiguation; pass &[] to rely on the magnitude heuristic.

§Limitations

Discovery only sees slots the call actually SLOADs. A getter that returns a computed value without reading a per-key backing slot — a rebasing balance derived from shares (stETH, Aave aTokens), or a value served purely from memory/calldata — yields no matching access. Callers that need a slot regardless (e.g. set_erc20_balance_with_slot_scan) fall through to a brute-force fallback rather than acting on a wrong slot.

Source

pub fn discover_erc20_balance_slot( &mut self, token: Address, owner: Address, ) -> Result<Option<HashSlotAccess>>

Discover a token’s balanceOf mapping slot for owner from a single simulated call — layout-agnostic (Solidity / Vyper / Solady), with no max_slot bound and no repeated probing.

Returns the HashSlotAccess whose loaded value matches the getter’s return and whose key is owner, or None if the token exposes no hashed balance read — a rebasing/computed getter that does not SLOAD a per-owner slot (stETH, aTokens), or unavailable code. Capture the result with HashSlotAccess::as_tracked to reuse the layout for other holders without re-simulating.

Source

pub fn track_erc20_balances( &mut self, token: Address, holders: impl IntoIterator<Item = Address>, ) -> Result<Option<TrackedBalances>>

Derive a token’s balance mapping layout once, then compute the exact storage slot for each of holders — the “discover, then track these addresses” primitive.

Reuses a cached/seeded TrackedMapping for token if present; otherwise discovers it from a single balanceOf simulation (using the first holder as the probe) and caches it. Returns the reusable descriptor plus (holder, slot) pairs — feed the slots to a FreshnessRegistry (pin_slot/mark_volatile_slot) or a PrefetchRegistry to keep them warm and fresh. Returns None if the layout can’t be discovered (e.g. an empty holders set with no cached descriptor, or a token with no hashed balance read).

Source

pub fn set_erc20_allowance( &mut self, token: Address, owner: Address, spender: Address, amount: U256, ) -> Result<bool>

Forge an ERC-20 allowance: discover the (nested) allowance mapping entry for (owner, spender) from a single traced allowance call, write amount to the exact slot, and verify.

This is the approval counterpart to set_erc20_balance_with_slot_scan — newly feasible because nested-mapping discovery can locate keccak(spender ‖ keccak(owner ‖ base)) (and its Vyper/packed variants) without a scan. Pass U256::MAX for an “unlimited” approval.

Returns Ok(true) if set and verified, Ok(false) if the token exposes no discoverable hashed allowance entry keyed by (owner, spender).

Source

pub fn write_mapping_entry( &mut self, tracked: &TrackedMapping, key: B256, value: U256, ) -> Result<B256>

Write value into a mapping entry using a discovered TrackedMapping layout, returning the exact storage slot written.

Unlike insert_mapping_storage_slot, which always assumes Solidity keccak(key‖slot) order, this honors the tracked layout, so it writes the correct slot for Vyper and Solady tokens too. The (contract, layout, base slot) all come from the tracked descriptor.

Source

pub fn mock_overlay(&mut self) -> EvmOverlay

Create a throwaway EvmOverlay over the current snapshot, wired to this cache’s backend for lazy fetch.

This is the entry point for overlay-scoped mocking: mock balances, approvals, and getter returns on the returned overlay (EvmOverlay::mock_balance, EvmOverlay::mock_allowance, EvmOverlay::mock_call) and run your simulations on that overlay. The mocks live only in the overlay’s dirty layer and are dropped with it — the cache is never mutated, so a mocked balance can’t leak into a later simulation. (For a persistent cache-level balance override, use set_erc20_balance_with_slot_scan.)

let mut sim = cache.mock_overlay();
sim.mock_balance(usdc, alice, U256::from(1_000_000_000_000u64))?; // 1M USDC (6 dp)
sim.mock_allowance(usdc, alice, router, U256::MAX)?;              // unlimited approve
let out = sim.call_raw(alice, router, swap)?;  // simulate against the mocked state
// drop `sim` → mocks discarded; `cache` is untouched.
Source

pub fn set_erc20_balance_with_slot_scan( &mut self, token: Address, owner: Address, amount: U256, max_slot: u16, ) -> Result<bool>

Set an ERC20 balance, discovering the token’s balance mapping slot and layout if not already known, then writing amount there.

Resolution order:

  1. A cached/seeded TrackedMapping for the token (fast path).
  2. Trace-based discovery — a single simulated balanceOf(owner), layout-agnostic (Solidity / Vyper / Solady) and unbounded by max_slot (see discover_erc20_balance_slot).
  3. The legacy brute-force scan of 0..=max_slot (Solidity order only), kept as a fallback for the rare token whose balanceOf reads no hashed slot the trace can attribute.

Every path verifies the write via balanceOf before caching, so a wrong guess is self-correcting. Returns Ok(true) if set and verified, Ok(false) if nothing worked, and Err on EVM/cache failures.

Source

pub fn call( &mut self, from: Address, to: Address, calldata: Bytes, commit: bool, ) -> Result<ExecutionResult>

Execute a call with automatic account/storage fetching.

Unlike the old implementation, this does NOT prefetch via access lists. The SharedBackend lazily fetches any missing data during execution.

Source

pub fn call_raw( &mut self, from: Address, to: Address, calldata: Bytes, commit: bool, ) -> Result<ExecutionResult>

Execute a call without any prefetching.

Data is fetched lazily by the SharedBackend as needed during execution.

Source

pub fn call_sol<C>(&mut self, to: Address, call: C) -> Result<C::Return>
where C: SolCall,

Execute a non-committing typed Solidity call from Address::ZERO.

This is the typed equivalent of encoding a SolCall, passing it to call_raw with commit = false, and decoding the successful return data with SolCall::abi_decode_returns.

sol! {
    function balanceOf(address account) external view returns (uint256);
}

let balance = cache.call_sol(token, balanceOfCall { account: owner })?;
Source

pub fn call_sol_from<C>( &mut self, from: Address, to: Address, call: C, ) -> Result<C::Return>
where C: SolCall,

Execute a non-committing typed Solidity call from an explicit sender.

Uses the default TxConfig, so native value, gas limit/price, nonce, and access list are left at the same defaults as call_raw.

Source

pub fn call_sol_with<C>( &mut self, from: Address, to: Address, call: C, tx: &TxConfig, ) -> Result<C::Return>
where C: SolCall,

Execute a non-committing typed Solidity call with explicit tx overrides.

This is the typed equivalent of call_raw_with with commit = false.

Source

pub fn transact_sol<C>( &mut self, from: Address, to: Address, call: C, tx: &TxConfig, ) -> Result<C::Return>
where C: SolCall,

Execute a typed Solidity call and commit its state changes.

This is the typed equivalent of call_raw_with with commit = true; the call’s state changes are persisted through the same path as the raw committing API before the return data is decoded.

Source

pub fn call_raw_with( &mut self, from: Address, to: Address, calldata: Bytes, commit: bool, tx: &TxConfig, ) -> Result<ExecutionResult>

Execute a call with explicit transaction-environment overrides (TxConfig): native value, gas limit/price, nonce, and an input access list. This is the entry point for value-bearing and gas-bounded simulation; call_raw is the zero-value shorthand.

Source

pub fn call_raw_with_access_list( &mut self, from: Address, to: Address, calldata: Bytes, ) -> Result<(ExecutionResult, StorageAccessList)>

Execute a non-committing call and extract the access list of touched accounts and storage slots before reverting.

Used for EIP-2929 marginal gas estimation in batched simulations.

Source

pub fn call_logs( &mut self, from: Address, to: Address, calldata: Bytes, commit: bool, ) -> Result<(Vec<Log>, u64)>

Execute a call and return its emitted logs and gas used.

A thin wrapper over call that requires success and discards the return data. When commit is true the call’s state changes are persisted to the CacheDB overlay; otherwise they are reverted.

§Errors

Returns an error if the underlying transact fails, or if the call did not Success (i.e. it reverted or halted).

Source

pub fn erc20_balance_of( &mut self, token: Address, owner: Address, ) -> Result<U256>

Read an ERC20 token balance by simulating a balanceOf(owner) call.

Non-committing: the read is reverted, so it never mutates cache state.

§Errors

Returns an error if the simulated call fails or does not Success (e.g. token is not a contract or reverts), or if the returned data cannot be ABI-decoded as a uint256.

Source

pub fn erc20_allowance( &mut self, token: Address, owner: Address, spender: Address, ) -> Result<U256>

Read an ERC20 allowance by simulating an allowance(owner, spender) call.

Non-committing: the read is reverted, so it never mutates cache state.

§Errors

Returns an error if the simulated call fails or does not Success (e.g. token is not a contract or reverts), or if the returned data cannot be ABI-decoded as a uint256.

Source

pub fn erc20_decimals(&mut self, token: Address) -> Result<u8>

Read an ERC20 token’s decimals by simulating a decimals() call.

Memoized: a hit in the in-memory token-decimals map returns immediately without simulating. On a miss the value is resolved by a non-committing decimals() call.

§Side effects

On a miss the resolved value is cached in both the in-memory token-decimals map (process lifetime) and the immutable data cache (so it is persisted to disk on the next flush).

§Errors

Returns an error if the simulated call fails or does not Success (e.g. token is not a contract or reverts), or if the returned data cannot be ABI-decoded as a uint8.

Source

pub fn immutable_cache(&self) -> &ImmutableDataCache

Get a reference to the immutable data cache (token decimals).

Source

pub fn immutable_cache_mut(&mut self) -> &mut ImmutableDataCache

Get a mutable reference to the immutable data cache.

Use this to pre-populate token decimals that would otherwise be discovered lazily. Entries are persisted on the next flush (and on drop) when a CacheConfig is set.

Source

pub fn has_contract_storage(&self, address: Address) -> bool

Check if an address has storage slots pre-loaded in the BlockchainDb.

This is useful to determine if we loaded the EVM state from the unified evm_state.bin cache and an address already has reusable storage.

§Arguments
  • address - The contract address to check
§Returns

true if the address has any storage slots in the underlying BlockchainDb, false otherwise

Source

pub fn contract_storage_slot_count(&self, address: Address) -> usize

Get the number of storage slots loaded for a contract address.

Useful for debugging and logging to understand cache state.

Source

pub fn shared_memory_stats(&self) -> (usize, usize)

Get memory statistics for the shared memory buffer used during EVM simulations.

Returns a tuple of (current_capacity_bytes, current_length_bytes).

The capacity represents the high-water mark of memory usage across all simulations since the buffer grows but doesn’t shrink. The length is typically 0 between simulations (cleared after each use).

§Use Case

Call this after running a batch of simulations to understand memory usage and inform the optimal initial capacity for SharedMemory.

§Example
let (capacity, _len) = cache.shared_memory_stats();
println!("Peak memory usage: {} KB", capacity / 1024);
Source

pub fn log_shared_memory_stats(&self)

Log the current shared memory buffer statistics.

Useful for profiling after running a batch of simulations.

Source

pub fn reserve_shared_memory(&mut self, capacity: usize)

Pre-allocate the shared memory buffer to a specific capacity.

Use this after measuring peak usage to avoid reallocation overhead during simulations. The buffer will grow beyond this if needed, but pre-sizing to the expected peak eliminates allocations.

§Arguments
  • capacity - The capacity in bytes to reserve
§Example
// After profiling shows peak usage is ~32KB
cache.reserve_shared_memory(32 * 1024);
Source

pub fn shared_memory_capacity(&self) -> usize

The resolved per-context EVM shared-memory pre-allocation, in bytes.

This is the SharedMemoryCapacity configured on the EvmCacheBuilder resolved to a concrete size (with SharedMemoryCapacity::Auto resolved against the state loaded at construction), raised by any later reserve_shared_memory. Each snapshot copies it onto the snapshot so snapshot-backed EvmOverlays pre-allocate the same amount.

Source

pub fn storage_batch_config(&self) -> StorageBatchConfig

The cache-side storage batch-fetch configuration for this instance.

Source

pub fn purge_contract_storage(&mut self, address: Address) -> usize

Purge all storage slots for a specific contract from both cache layers.

This clears:

  1. CacheDB overlay (self.db.cache.accounts[addr].storage) - the in-memory layer that caches storage slots fetched during EVM execution. Without clearing this layer, subsequent EVM calls return stale values even after the backend is purged.
  2. BlockchainDb backend (self.blockchain_db.storage()) - the persistent layer that caches RPC responses and is loaded from evm_state.bin.

After purging both layers, the next EVM read for this contract’s storage will go all the way to the RPC for fresh data.

Source

pub fn purge_contract_slots( &mut self, address: Address, slots: &[U256], ) -> usize

Purge specific storage slots for a contract from both cache layers.

Unlike purge_contract_storage() which removes ALL storage, this only removes the specified slots. This is useful when only a narrow subset of hot storage became stale and the rest of the contract’s cached storage should be kept.

Returns the number of slots removed from the BlockchainDb backend.

Source

pub fn purge_contracts_storage( &mut self, addresses: impl IntoIterator<Item = Address>, ) -> usize

Purge storage slots for multiple contracts from both cache layers.

See purge_contract_storage() for details on what each layer contains.

Source

pub fn purge_all_storage(&mut self) -> usize

Purge ALL storage slots from both cache layers while preserving bytecodes.

Use this for periodic full cache refresh (e.g., every 48 hours) to ensure any stale data like strategy swap paths, proxy implementations, reward rates, etc. are re-fetched from the actual on-chain state.

This preserves:

  • Account info (nonce, balance, code hash)
  • Contract bytecodes (immutable)

This purges:

  • All storage slots from CacheDB overlay (layer 1)
  • All storage slots from BlockchainDb backend (layer 2)
§Returns

The total number of storage slots that were removed from the BlockchainDb

Source

pub fn enumerate_contract_slots(&self, address: Address) -> Vec<U256>

Enumerate all cached storage slots for a contract address.

Returns the union of slot keys from both CacheDB overlay (layer 1) and BlockchainDb backend (layer 2). Used by the slot observation tracker to selectively purge only slots likely to have changed.

Source

pub fn all_cached_contract_addresses(&self) -> Vec<Address>

Return all contract addresses that have cached storage in either layer.

Used by the observation-aware full purge to enumerate what needs checking.

Source

pub fn cache_db_storage_slot_count(&self, address: Address) -> usize

Get the number of storage slots in the CacheDB overlay for a contract.

This is useful for diagnostics: if a contract has slots in the CacheDB overlay, they will be served on EVM reads without going to the backend.

Source

pub fn simulate_call_with_balance_deltas( &mut self, from: Address, to: Address, calldata: Bytes, owner: Address, tokens: impl IntoIterator<Item = Address>, commit: bool, ) -> Result<CallSimulationResult>

Simulate a call and compute owner’s net balance change for each token in tokens by reading balanceOf(owner) immediately before and after.

Each delta is the signed post - pre difference (see CallSimulationResult::token_deltas). When commit is true the call’s state changes are persisted to the CacheDB overlay; otherwise they are reverted. Unlike simulate_with_transfer_tracking, this measures deltas via pre/post balance reads (not transfer-event inspection). The returned access_list includes the accounts and slots touched by the pre/post balanceOf reads and the simulated call.

§Errors

Returns an error if building the tx env fails, if a pre/post balanceOf read fails, or if the call does not Success (i.e. it reverted or halted). On error the simulation is reverted.

Source

pub fn simulate_with_transfer_tracking( &mut self, from: Address, to: Address, calldata: Bytes, owner: Address, tokens: Option<impl IntoIterator<Item = Address>>, commit: bool, ) -> SimulationResult<CallSimulationResult>

Simulate a call and track token balance changes using a TransferInspector.

This method uses EVM inspection to capture ERC20 Transfer events during execution, eliminating the need for manual balance reads before/after the transaction.

Returns:

  • Ok(CallSimulationResult) on successful execution
  • Err(SimError::Revert(_)) when the transaction reverts (graceful failure)
  • Err(SimError::Other(_)) for unexpected errors (should be propagated)
Source

pub fn simulate_with_transfer_tracking_raw( &mut self, from: Address, to: Address, calldata: Bytes, owner: Address, tokens: Option<impl IntoIterator<Item = Address>>, commit: bool, ) -> SimulationResult<CallSimulationResult>

Simulate a call with transfer tracking without any prefetching.

This is identical to simulate_with_transfer_tracking since we no longer do access list prefetching. Kept for API compatibility.

Source

pub fn simulate_bundle( &mut self, txs: &[BundleTx], opts: &BundleOptions, ) -> SimulationResult<BundleResult>

Simulate an ordered transaction bundle over cumulative block state, with a revert policy and coinbase/miner-payment accounting (Phase 6 Track A+B).

This is a convenience wrapper: it snapshots the cache and runs the bundle on a fresh transient EvmOverlay via EvmOverlay::simulate_bundle, which carries the full semantics (ordered cumulative state, the RevertPolicy, and coinbase accounting).

The cache itself is never mutated — even when opts.commit is true. commit controls only whether the bundle’s cumulative state is folded into the transient overlay (and is therefore moot here, since that overlay is dropped when this call returns). Snapshot the cache yourself and drive EvmOverlay::simulate_bundle directly when you need the committed overlay state to outlive the call (e.g. to chain a follow-up read).

§Errors

Returns SimError if a transaction environment cannot be built or revm fails to transact. A transaction reverting is reported through the per-transaction outcome and the revert policy, not as an error.

Source

pub fn deploy_contract( &mut self, from: Address, creation_code: Bytes, ) -> Result<Address>

Deploy a contract via CREATE transaction and return the deployed address.

The creation_code should include the init code with ABI-encoded constructor arguments appended. Nonce checks are disabled, so any from address works.

Note: This commits the deployment to the CacheDB. Use a throw-away deployer address (e.g., Address::ZERO) to avoid side effects on real accounts.

§Errors

Returns an error if the CREATE tx env cannot be built, if the deployment reverts or halts, or if it succeeds but the EVM returns no contract address.

Source

pub fn override_account_code( &mut self, source: Address, target: Address, ) -> Result<()>

Override the bytecode at target address with bytecode from source address.

Copies only non-empty runtime code and code_hash; storage, balance, and nonce at target remain unchanged. target must already have non-empty runtime bytecode. Both the CacheDB overlay and BlockchainDb backend are updated, ensuring the override is visible to parallel EVM tasks sharing the same backend.

§Errors

Returns an error if source has no cached bytecode or its code is empty, if target cannot be loaded (it must already exist on the backend), or if target has no existing runtime bytecode to override. For synthetic target addresses that may not exist, use override_or_create_account_code.

Source

pub fn override_or_create_account_code( &mut self, source: Address, target: Address, ) -> Result<()>

Override the bytecode at target, creating a default target account when absent.

Use this for synthetic addresses in local simulations. For live forked accounts where storage/balance/nonce must be preserved, prefer Self::override_account_code.

Source

pub fn override_account_code_with_missing_target( &mut self, source: Address, target: Address, missing_target: MissingTargetBehavior, ) -> Result<()>

Override code at target, with explicit behavior for missing target accounts.

This is intentionally not folded onto apply_update’s Account code patch: it copies code from a source account, preserves the target’s existing balance/nonce/ storage, and unconditionally materializes the target in the CacheDB overlay (the primary read path for EVM execution, required for the Create synthetic-target case). The generic primitive writes the overlay only when an account is already present, so the two are not behavior-equivalent. For a plain code overwrite that follows the dual-layer write-through policy, use apply_update(StateUpdate::Account { patch: AccountPatch::default().code(..) }).

Source

pub fn verify_code_seeds(&mut self) -> Result<CodeVerifyReport>

Verify every CodeSeedState::Pending canonical code claim against the chain at the pinned block — one bulk eth_call for the whole set.

Per-address outcomes (see CodeVerifyReport):

  • match ⇒ marked CodeSeedState::Verified (never re-checked; post-EIP-6780 code is immutable) and the account’s real balance is patched in from the same response — pure materialization of pinned-block truth, so it does not bump the snapshot generation;
  • mismatch / not-deployed / code-lesspurge_account (both layers and the mark; the purge path bumps the generation) — the next touch refetches authoritative chain state;
  • transport failure (the whole call, an omitted address, or the MULTICALL3_ADDRESS extractor-host caveat) ⇒ still Pending, reported unverifiable — a failed read proves nothing, so it never promotes and never destroys a seed.

With no pending seeds this is a no-op that needs no fetcher. Verified seeds are skipped forever, so calling this repeatedly (or from every cold-start round) costs nothing once the set is settled.

§Errors

CacheError::MissingAccountFieldsFetcher when pending seeds exist but no AccountFieldsFetchFn is installed (a from_backend cache without set_account_fields_fetcher).

Source

pub fn seed_account_code( &mut self, address: Address, code: Bytes, ) -> Result<B256>

Seed canonical runtime code for address without fetching it.

The claim is marked CodeSeedState::Pending until verify_code_seeds confirms it against the on-chain EXTCODEHASH (or the cold-start driver’s verify_code phase does). Because the account is materialized in both cache layers, the lazy backend never fires its balance/nonce/code RPC triple for it.

Defaults: nonce 1 (the EIP-161 contract minimum — exact for any contract that never CREATEs) and balance ZERO until verification patches the real value from the same response. Use seed_account_code_with to supply both explicitly.

Conflict rules (chain-fetched state is authoritative over templates): seeding an unmarked address that already holds RPC-origin code with the same hash marks it Verified immediately (zero RPC — the warm-cache fast path); a differing hash (including a code-less EOA) is CacheError::CodeSeedConflict and leaves the cached code untouched. Re-seeding a marked address overwrites and restarts the claim as Pending.

Returns the keccak256 hash recorded for the claim.

§Errors

CacheError::CodeSeedEmpty for empty code; CacheError::CodeSeedConflict as above.

Source

pub fn seed_account_code_with( &mut self, address: Address, code: Bytes, nonce: u64, balance: U256, ) -> Result<B256>

seed_account_code with explicit nonce and provisional balance for the materialized account. Verification still overwrites the balance with the on-chain value on a match; the nonce keeps the supplied value (an exact nonce needs the eth_getProof path and only matters for contracts that CREATE).

Source

pub fn etch_account_code( &mut self, address: Address, code: Bytes, ) -> Result<B256>

Etch deliberately-local runtime code at address — the raw-bytes sibling of override_or_create_account_code, with no source account needed.

Marks CodeSeedState::Etched: never verified, excluded from all canonical machinery, and reported via etched_accounts so local divergence stays visible. Preserves the existing balance/nonce/storage when the account is already present; creates a default account otherwise. Overwrites any prior code or mark — divergence is the caller’s explicit intent.

Returns the keccak256 hash of the etched code.

§Errors

CacheError::CodeSeedEmpty for empty code.

Source

pub fn code_seed_state(&self, address: &Address) -> Option<&CodeSeedState>

The code-seed mark for address, if any. None means RPC-origin: the code (if present) was fetched from the provider and is trusted as chain state.

Source

pub fn pending_code_seeds(&self) -> Vec<Address>

Addresses whose canonical code claims still await verification (CodeSeedState::Pending), sorted for deterministic iteration. This is the implicit work set of verify_code_seeds and the cold-start verify_code phase.

Source

pub fn etched_accounts(&self) -> Vec<Address>

Addresses whose code deliberately diverges from the chain (CodeSeedState::Etched), sorted for deterministic iteration. This is the health surface for local divergence: everything written through etch_account_code, override_account_code and friends, or deploy_contract appears here.

Source§

impl EvmCache

Source

pub fn execute_cold_start_round(&mut self, plan: &ColdStartPlan) -> RoundOutcome

Execute a single cold-start round and return its (possibly partial) outcome.

Fixed phase order: verify_code → accounts → verify → probe → probe_roots → discover.

Per-round fetcher guards: if the plan declares any verify or probe slots and the cache has no storage batch fetcher, the round short-circuits with ColdStartError::NoBatchFetcher before issuing any read; likewise a probe_roots-bearing round with no account proof fetcher short-circuits with ColdStartError::NoAccountProofFetcher, and a round with pending code seeds but no account-fields fetcher short-circuits with ColdStartError::NoAccountFieldsFetcher. A round declaring only accounts/discover (and holding no pending seeds) runs without any fetcher.

  • verify_code (first): every CodeSeedState Pending claim is settled via verify_code_seeds; the work set is the cache’s own pending marks, not a plan field. Matches are marked Verified; contradicted claims are purged, so an address also listed in plan.accounts is refetched by the very next phase. A transport failure is not a round hard error — it surfaces in the report’s unverifiable bucket (matching probe_roots’ per-address stance). Running first means no discover sim ever executes over an unverified claim, and results.code_verifications survives any later phase’s hard error. With no pending seeds the phase is a no-op (code_verifications: None).
  • accounts: each plan.accounts address is pre-seeded via ensure_account_blocking. A failure here is a hard error before the slot phases ran: every declared verify/probe slot is marked SlotFetch::NotAttempted, every declared probe_roots address is synthesized as root: None, and the round returns with error: Some(..) (the already-computed code_verifications is preserved). This is the only producer of NotAttempted.
  • verify: each verify slot is re-fetched, classified into results.fetched, and (when changed) injected and recorded in results.verified.
  • probe: each probe slot is re-fetched at the pinned block and classified into results.probed via the same shared Result<U256> classification verify uses. Unlike verify, a probe injects nothing and records no SlotChange: it is the archive-miss classification for slots a consumer does not want to warm.
  • probe_roots: each plan.probe_roots address is root-only probed ((addr, vec![])) through the account proof fetcher at the pinned block and recorded into results.probed_roots as a RootProbeOutcome. Nothing is injected; a per-address failure (or an address the fetcher omitted) is root: None, never a round hard error.
  • discover (last): each ColdStartCall is executed via call_raw_with_access_list, its access list filtered by restrict_to, and the result pushed to results.discovered. A discover failure preserves the verify/probe outcomes already computed this round (they ran earlier, so they are not NotAttempted); the failing call and all subsequent discover calls are dropped, and the round returns with error: Some(..).
Source

pub fn run_cold_start( &mut self, planner: &mut dyn ColdStartPlanner, config: ColdStartConfig, ) -> Result<ColdStartRunReport, ColdStartError>

Run a bounded multi-round cold start driven by planner.

Pin handling per config.pin: ColdStartPin::CachePinned is a no-op; ColdStartPin::Hash pins every round to BlockId::from((hash, Some(require_canonical))), capturing the prior block and restoring it on every exit path (success, budget-exceeded, and mid-round error).

The loop checks the round budget at the top: with max_rounds = N, rounds 0..N execute and a planner still returning Continue after round N yields RoundBudgetExceeded. Each round’s results are absorbed into the report before its error is checked; a round error propagates after restoring the pin and without calling on_results. Note the absorbed report is returned only on the Ok path — on error it is dropped and the Err carries only the cause; use execute_cold_start_round directly to observe a failed round’s partial outcomes.

Trait Implementations§

Source§

impl Drop for EvmCache

Automatically flush the cache to disk when the EvmCache is dropped.

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more
Source§

impl StateView for EvmCache

Read-only state view for the event pipeline (Pillar B.2): a decoder reads the current cached value of a slot through cached_storage_value, which never touches RPC and is account_state-aware (a cold slot reads None).

Source§

fn storage(&self, address: Address, slot: U256) -> Option<U256>

Current cached value of (address, slot) (overlay ▸ backend ▸ None), matching what the EVM would SLOAD (account_state-aware). None means the slot is cold — neither cache layer has seen it.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> Conv for T

Source§

fn conv<T>(self) -> T
where Self: Into<T>,

Converts self into T using Into<T>. Read more
Source§

impl<T> FmtForward for T

Source§

fn fmt_binary(self) -> FmtBinary<Self>
where Self: Binary,

Causes self to use its Binary implementation when Debug-formatted.
Source§

fn fmt_display(self) -> FmtDisplay<Self>
where Self: Display,

Causes self to use its Display implementation when Debug-formatted.
Source§

fn fmt_lower_exp(self) -> FmtLowerExp<Self>
where Self: LowerExp,

Causes self to use its LowerExp implementation when Debug-formatted.
Source§

fn fmt_lower_hex(self) -> FmtLowerHex<Self>
where Self: LowerHex,

Causes self to use its LowerHex implementation when Debug-formatted.
Source§

fn fmt_octal(self) -> FmtOctal<Self>
where Self: Octal,

Causes self to use its Octal implementation when Debug-formatted.
Source§

fn fmt_pointer(self) -> FmtPointer<Self>
where Self: Pointer,

Causes self to use its Pointer implementation when Debug-formatted.
Source§

fn fmt_upper_exp(self) -> FmtUpperExp<Self>
where Self: UpperExp,

Causes self to use its UpperExp implementation when Debug-formatted.
Source§

fn fmt_upper_hex(self) -> FmtUpperHex<Self>
where Self: UpperHex,

Causes self to use its UpperHex implementation when Debug-formatted.
Source§

fn fmt_list(self) -> FmtList<Self>
where &'a Self: for<'a> IntoIterator,

Formats each item in a sequence. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pipe for T
where T: ?Sized,

Source§

fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> R
where Self: Sized,

Pipes by value. This is generally the method you want to use. Read more
Source§

fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> R
where R: 'a,

Borrows self and passes that borrow into the pipe function. Read more
Source§

fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> R
where R: 'a,

Mutably borrows self and passes that borrow into the pipe function. Read more
Source§

fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
where Self: Borrow<B>, B: 'a + ?Sized, R: 'a,

Borrows self, then passes self.borrow() into the pipe function. Read more
Source§

fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
where Self: BorrowMut<B>, B: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.borrow_mut() into the pipe function. Read more
Source§

fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
where Self: AsRef<U>, U: 'a + ?Sized, R: 'a,

Borrows self, then passes self.as_ref() into the pipe function.
Source§

fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
where Self: AsMut<U>, U: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.as_mut() into the pipe function.
Source§

fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
where Self: Deref<Target = T>, T: 'a + ?Sized, R: 'a,

Borrows self, then passes self.deref() into the pipe function.
Source§

fn pipe_deref_mut<'a, T, R>( &'a mut self, func: impl FnOnce(&'a mut T) -> R, ) -> R
where Self: DerefMut<Target = T> + Deref, T: 'a + ?Sized, R: 'a,

Mutably borrows self, then passes self.deref_mut() into the pipe function.
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> Tap for T

Source§

fn tap(self, func: impl FnOnce(&Self)) -> Self

Immutable access to a value. Read more
Source§

fn tap_mut(self, func: impl FnOnce(&mut Self)) -> Self

Mutable access to a value. Read more
Source§

fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
where Self: Borrow<B>, B: ?Sized,

Immutable access to the Borrow<B> of a value. Read more
Source§

fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
where Self: BorrowMut<B>, B: ?Sized,

Mutable access to the BorrowMut<B> of a value. Read more
Source§

fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
where Self: AsRef<R>, R: ?Sized,

Immutable access to the AsRef<R> view of a value. Read more
Source§

fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
where Self: AsMut<R>, R: ?Sized,

Mutable access to the AsMut<R> view of a value. Read more
Source§

fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
where Self: Deref<Target = T>, T: ?Sized,

Immutable access to the Deref::Target of a value. Read more
Source§

fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
where Self: DerefMut<Target = T> + Deref, T: ?Sized,

Mutable access to the Deref::Target of a value. Read more
Source§

fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self

Calls .tap() only in debug builds, and is erased in release builds.
Source§

fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self

Calls .tap_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
where Self: Borrow<B>, B: ?Sized,

Calls .tap_borrow() only in debug builds, and is erased in release builds.
Source§

fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
where Self: BorrowMut<B>, B: ?Sized,

Calls .tap_borrow_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
where Self: AsRef<R>, R: ?Sized,

Calls .tap_ref() only in debug builds, and is erased in release builds.
Source§

fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
where Self: AsMut<R>, R: ?Sized,

Calls .tap_ref_mut() only in debug builds, and is erased in release builds.
Source§

fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
where Self: Deref<Target = T>, T: ?Sized,

Calls .tap_deref() only in debug builds, and is erased in release builds.
Source§

fn tap_deref_mut_dbg<T>(self, func: impl FnOnce(&mut T)) -> Self
where Self: DerefMut<Target = T> + Deref, T: ?Sized,

Calls .tap_deref_mut() only in debug builds, and is erased in release builds.
Source§

impl<T> TryConv for T

Source§

fn try_conv<T>(self) -> Result<T, Self::Error>
where Self: TryInto<T>,

Attempts to convert self into T using TryInto<T>. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more