Skip to main content

evm_fork_cache/
state_update.rs

1//! Targeted state-mutation vocabulary and the structured diff it produces
2//! (Pillar B.1 — the *writer half* of the event → state pipeline).
3//!
4//! This module defines the small, generic vocabulary a future event decoder
5//! emits and [`EvmCache::apply_update`](crate::cache::EvmCache::apply_update)
6//! consumes, plus the [`StateDiff`] that records what an apply actually changed.
7//! It is pure data and logic on itself: it carries no protocol or event
8//! knowledge and has no dependency on the cache implementation.
9//!
10//! # The vocabulary
11//!
12//! A [`StateUpdate`] is one targeted mutation:
13//!
14//! - [`StateUpdate::Slot`] — set a single storage slot, authoritative across
15//!   both cache layers.
16//! - [`StateUpdate::Account`] — apply a partial [`AccountPatch`]
17//!   (`balance`/`nonce`/`code`, each optional) to an already-known account.
18//! - [`StateUpdate::AccountUpsert`] — intentionally materialize a cold account
19//!   from a partial [`AccountPatch`].
20//! - [`StateUpdate::Purge`] — drop cached state at a [`PurgeScope`] so the next
21//!   read re-fetches.
22//!
23//! # The dual-layer write-through policy
24//!
25//! [`apply_update`](crate::cache::EvmCache::apply_update) applies `Slot` writes
26//! through with one consistent rule: the BlockchainDb backend (layer 2) is
27//! written **always**; the CacheDB overlay (layer 1) is written **only if an
28//! overlay account already exists** for the address. A new overlay account is
29//! never materialized for a slot write — the read path falls through to the
30//! backend for an absent overlay entry, so a backend-only write is authoritative,
31//! and materializing an overlay entry would pollute layer 1 and could shadow
32//! later RPC reads. (This mirrors the established
33//! [`inject_storage_batch_fresh`](crate::cache::EvmCache::inject_storage_batch_fresh)
34//! semantics.)
35//!
36//! `Account` patches follow the same overlay-if-present write-through policy
37//! once the account is already present in either layer. If the account is absent
38//! from **both** layers, the patch is skipped and surfaced in
39//! [`StateDiff::skipped_accounts`]. Use [`StateUpdate::AccountUpsert`] when the
40//! caller intentionally wants to materialize a cold/default account.
41//!
42//! # The output
43//!
44//! Every apply returns a [`StateDiff`] of the changes it actually made: the
45//! [`SlotChange`]s, [`AccountChange`]s, and [`PurgeRecord`]s. **Only real changes
46//! are recorded** — re-applying a value the cache already holds yields an empty
47//! diff, so idempotence is observable.
48//!
49//! # Relative updates / cold-aware read-modify-write
50//!
51//! Some callers learn only a *delta* (an ERC-20 `Transfer` log carries the
52//! transferred `amount`, not the resulting balances), so the vocabulary also
53//! supports *relative* updates: [`StateUpdate::SlotDelta`] reads the current slot
54//! value, applies a saturating [`SlotDelta`] (`Add` clamps at `U256::MAX`, `Sub`
55//! at `U256::ZERO`), and writes the result back through both layers. The general
56//! closure form is
57//! [`EvmCache::modify_slot`](crate::cache::EvmCache::modify_slot).
58//!
59//! A relative update is only valid against a value the cache *actually holds*. An
60//! un-fetched ("cold") slot has no value, and applying a delta to it would compute
61//! `0 ± amount`, write a wrong value, and (write-through) make it authoritative —
62//! silently corrupting state. So relative application is **cold-aware**: a
63//! `SlotDelta` on a cold slot is **not applied**; it is recorded in
64//! [`StateDiff::skipped`] as a [`SkippedDelta`] so the caller can fetch+seed the
65//! true value (the next read otherwise lazily fetches it). `modify_slot` hands its
66//! closure an `Option<U256>` (`None` when cold) and lets the caller decide.
67//!
68//! # Masked writes to packed words
69//!
70//! A storage slot often packs several fields into one word. A decoder that
71//! learns only some of those fields must update *just* its bits without
72//! clobbering the rest. [`StateUpdate::SlotMasked`] is the
73//! cold-aware masked read-modify-write for exactly this: it computes
74//! `new = (old & !mask) | (value & mask)`, touching only the `mask` bits. Like
75//! [`SlotDelta`](StateUpdate::SlotDelta) it is cold-aware — the un-masked bits of
76//! a slot absent from both layers are unknown, so a masked write to a cold slot
77//! is **not** applied; it is surfaced in [`StateDiff::skipped_masks`] as a
78//! [`SkippedMask`]. (A full-mask `SlotMasked` matches an absolute
79//! [`Slot`](StateUpdate::Slot) write on a hot slot but still skips on a cold one.)
80//!
81//! The same relative, cold-aware rule extends to an account's **native balance**:
82//! [`StateUpdate::BalanceDelta`] (and the closure form
83//! [`EvmCache::modify_account_balance`](crate::cache::EvmCache::modify_account_balance))
84//! read-modify-write `AccountInfo::balance`, preserving nonce/code. "Cold" here
85//! means the account is absent from *both* layers (its balance is unknown); a
86//! `BalanceDelta` on a cold account is **not applied** — it is surfaced in
87//! [`StateDiff::skipped_balances`] as a [`SkippedBalanceDelta`]. This avoids
88//! materializing a default account that would mask the real on-chain one.
89//!
90//! # Checking for skips
91//!
92//! Because a cold-skipped relative update produces **no** change, it is invisible
93//! to the natural [`StateDiff::is_empty`] / [`StateDiff::len`] success check (those
94//! are changes-only). A caller applying relative updates **must** therefore check
95//! [`StateDiff::has_skipped`] (or inspect [`skipped`](StateDiff::skipped),
96//! [`skipped_balances`](StateDiff::skipped_balances),
97//! [`skipped_masks`](StateDiff::skipped_masks), or
98//! [`skipped_accounts`](StateDiff::skipped_accounts)) — a cold target was
99//! dropped, not applied, and a silently-dropped balance/account update can break
100//! conservation. [`StateDiff::is_fully_applied`] and
101//! [`StateDiff::skipped_len`] are the companions.
102//!
103//! # Boundary — events are Phase 4
104//!
105//! This is the vocabulary a Phase 4 `EventDecoder` will *emit into*; Phase 3
106//! does not decode events. Nothing here parses a `Log` or knows a protocol's
107//! storage layout — that is the *reader half* of Pillar B and lands later.
108
109use alloy_primitives::{Address, B256, Bytes, U256};
110
111use crate::freshness::SlotChange;
112
113/// A relative storage-slot mutation: read the current value, transform it, and
114/// write it back.
115///
116/// Both directions **saturate** rather than wrap: `Add` clamps at `U256::MAX`
117/// and `Sub` clamps at `U256::ZERO`. This is the delta a caller derives from an
118/// event (e.g. an ERC-20 `Transfer` amount) without knowing the resulting
119/// absolute value. It is applied by [`StateUpdate::SlotDelta`] (cold-aware — see
120/// the module docs).
121///
122/// ```
123/// use alloy_primitives::U256;
124/// use evm_fork_cache::SlotDelta;
125///
126/// assert_eq!(SlotDelta::Add(U256::from(50)).apply(U256::from(100)), U256::from(150));
127/// assert_eq!(SlotDelta::Sub(U256::from(50)).apply(U256::from(30)), U256::ZERO);
128/// assert_eq!(SlotDelta::Add(U256::from(10)).apply(U256::MAX), U256::MAX);
129/// ```
130#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
131pub enum SlotDelta {
132    /// Add to the current value, saturating at `U256::MAX`.
133    Add(U256),
134    /// Subtract from the current value, saturating at `U256::ZERO`.
135    Sub(U256),
136}
137
138impl SlotDelta {
139    /// Apply the (saturating) delta to a current value.
140    ///
141    /// `Add` uses `saturating_add` (clamps at `U256::MAX`); `Sub` uses
142    /// `saturating_sub` (clamps at `U256::ZERO`).
143    pub fn apply(self, current: U256) -> U256 {
144        match self {
145            SlotDelta::Add(amount) => current.saturating_add(amount),
146            SlotDelta::Sub(amount) => current.saturating_sub(amount),
147        }
148    }
149}
150
151/// A single targeted mutation to cached EVM state.
152///
153/// The vocabulary an event decoder (Phase 4) emits and
154/// [`EvmCache::apply_update`](crate::cache::EvmCache::apply_update) consumes.
155/// Generic: carries no protocol or event knowledge.
156///
157/// The enum is `#[non_exhaustive]`: new variants (e.g. a code-only convenience)
158/// may be added pre-1.0 without a breaking change.
159///
160/// ```
161/// use alloy_primitives::{Address, U256};
162/// use evm_fork_cache::{AccountPatch, PurgeScope, StateUpdate};
163///
164/// let contract = Address::repeat_byte(0x01);
165///
166/// // A storage-slot write (authoritative across both cache layers).
167/// let slot = StateUpdate::slot(contract, U256::from(0), U256::from(42));
168///
169/// // A balance-only account patch (nonce and code left untouched).
170/// let bal = StateUpdate::balance(contract, U256::from(1_000));
171/// assert_eq!(
172///     bal,
173///     StateUpdate::Account { address: contract, patch: AccountPatch::default().balance(U256::from(1_000)) },
174/// );
175///
176/// // Drop just two storage slots so the next read re-fetches them.
177/// let purge = StateUpdate::purge(contract, PurgeScope::Slots(vec![U256::from(0), U256::from(1)]));
178/// # let _ = (slot, purge);
179/// ```
180#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
181#[non_exhaustive]
182pub enum StateUpdate {
183    /// Set one storage slot to `value`, authoritative across both cache layers.
184    Slot {
185        /// Contract whose storage is written.
186        address: Address,
187        /// Storage slot key.
188        slot: U256,
189        /// New slot value.
190        value: U256,
191    },
192    /// Apply a *relative* (saturating) mutation to one storage slot.
193    ///
194    /// Read-modify-write: the current value is read, the [`SlotDelta`] applied,
195    /// and the result written back through both layers. **Cold-aware** — a delta
196    /// on a slot absent from both layers is not applied; it is surfaced in
197    /// [`StateDiff::skipped`] instead (see the module docs).
198    SlotDelta {
199        /// Contract whose storage is written.
200        address: Address,
201        /// Storage slot key.
202        slot: U256,
203        /// The relative, saturating mutation to apply to the current value.
204        delta: SlotDelta,
205    },
206    /// Apply a *relative* (saturating) mutation to an account's **native balance**.
207    ///
208    /// Read-modify-write: the current `AccountInfo::balance` is read, the
209    /// [`SlotDelta`] applied, and the result written back through both layers
210    /// (nonce and code preserved). **Cold-aware** — "cold" here means the
211    /// account's info is unknown to the EVM: absent from *both* layers, **or**
212    /// present in the overlay as revm `NotExisting` (which the internal account
213    /// read also treats as cold, mirroring `DbAccount::info()`). A `BalanceDelta`
214    /// on a cold account is not applied; it is surfaced in
215    /// [`StateDiff::skipped_balances`] instead (so no default account is
216    /// materialized to mask the real on-chain one — see the module docs).
217    BalanceDelta {
218        /// Account whose native balance is mutated.
219        address: Address,
220        /// The relative, saturating mutation to apply to the current balance.
221        delta: SlotDelta,
222    },
223    /// Set only the `mask` bits of a storage slot to the corresponding bits of
224    /// `value`, preserving the rest: `new = (old & !mask) | (value & mask)`.
225    ///
226    /// A *masked* read-modify-write: it lets a pure decoder express a partial
227    /// update to a **packed** storage word without knowing or clobbering the bits
228    /// it does not own.
229    ///
230    /// **Cold-aware** — a masked write to a slot absent from *both* cache layers is
231    /// **not** applied (the un-masked bits are unknown, so the result cannot be
232    /// computed); it is surfaced in [`StateDiff::skipped_masks`] as a
233    /// [`SkippedMask`] instead. A masked write with `mask == U256::MAX` equals an
234    /// absolute [`Slot`](Self::Slot) write on a *hot* slot, but **still skips** on a
235    /// cold one (unlike [`Slot`](Self::Slot), which writes unconditionally). Use
236    /// [`Slot`](Self::Slot) for an unconditional absolute write; use `SlotMasked`
237    /// when neighbouring bits must be preserved.
238    SlotMasked {
239        /// Contract whose storage is written.
240        address: Address,
241        /// Storage slot key.
242        slot: U256,
243        /// Which bits of the slot to overwrite (1 = take from `value`).
244        mask: U256,
245        /// The bits to write (only the bits selected by `mask` are applied).
246        value: U256,
247    },
248    /// Patch an already-known account's balance/nonce/code (partial — see
249    /// [`AccountPatch`]).
250    ///
251    /// Cold-aware: if the account's info is unknown to the EVM (absent from
252    /// **both** layers, or present in the overlay as revm `NotExisting`), the patch
253    /// is not applied and is surfaced in [`StateDiff::skipped_accounts`] as a
254    /// [`SkippedAccountPatch`]. Use [`StateUpdate::AccountUpsert`] when
255    /// materializing a cold/default account is intentional.
256    Account {
257        /// Account to patch.
258        address: Address,
259        /// The partial mutation: each `Some` field overwrites, `None` leaves it.
260        patch: AccountPatch,
261    },
262    /// Apply an [`AccountPatch`], materializing a cold account when needed.
263    ///
264    /// This is the explicit escape hatch for callers that really do want a
265    /// default account to become authoritative in the backend (for example a
266    /// synthetic test account). Normal event-derived account patches should use
267    /// [`StateUpdate::Account`] so a cold account is skipped instead of masking a
268    /// future RPC fetch.
269    AccountUpsert {
270        /// Account to patch or create.
271        address: Address,
272        /// The partial mutation: each `Some` field overwrites, `None` leaves it.
273        patch: AccountPatch,
274    },
275    /// Purge cached state for `address` at `scope`; the next read re-fetches.
276    Purge {
277        /// Account whose cached state is purged.
278        address: Address,
279        /// What part of the cached state to remove.
280        scope: PurgeScope,
281    },
282}
283
284impl StateUpdate {
285    /// Construct a [`StateUpdate::Slot`] that sets `(address, slot)` to `value`.
286    pub fn slot(address: Address, slot: U256, value: U256) -> Self {
287        Self::Slot {
288            address,
289            slot,
290            value,
291        }
292    }
293
294    /// Construct a [`StateUpdate::SlotDelta`] that applies `delta` relative to the
295    /// current value of `(address, slot)`.
296    pub fn slot_delta(address: Address, slot: U256, delta: SlotDelta) -> Self {
297        Self::SlotDelta {
298            address,
299            slot,
300            delta,
301        }
302    }
303
304    /// Construct a [`StateUpdate::SlotMasked`] that sets only the `mask` bits of
305    /// `(address, slot)` to the corresponding bits of `value`.
306    pub fn slot_masked(address: Address, slot: U256, mask: U256, value: U256) -> Self {
307        Self::SlotMasked {
308            address,
309            slot,
310            mask,
311            value,
312        }
313    }
314
315    /// Construct a [`StateUpdate::BalanceDelta`] that applies `delta` relative to
316    /// the account's current native balance.
317    pub fn balance_delta(address: Address, delta: SlotDelta) -> Self {
318        Self::BalanceDelta { address, delta }
319    }
320
321    /// Construct a [`StateUpdate::Account`] that patches only the balance.
322    pub fn balance(address: Address, value: U256) -> Self {
323        Self::Account {
324            address,
325            patch: AccountPatch::default().balance(value),
326        }
327    }
328
329    /// Construct a [`StateUpdate::Account`] that patches only the nonce.
330    pub fn nonce(address: Address, nonce: u64) -> Self {
331        Self::Account {
332            address,
333            patch: AccountPatch::default().nonce(nonce),
334        }
335    }
336
337    /// Construct a [`StateUpdate::Account`] that patches only the runtime code
338    /// (the code hash is recomputed from `code` when applied).
339    pub fn code(address: Address, code: Bytes) -> Self {
340        Self::Account {
341            address,
342            patch: AccountPatch::default().code(code),
343        }
344    }
345
346    /// Construct a [`StateUpdate::Account`] from a prebuilt [`AccountPatch`].
347    pub fn account(address: Address, patch: AccountPatch) -> Self {
348        Self::Account { address, patch }
349    }
350
351    /// Construct a [`StateUpdate::AccountUpsert`] from a prebuilt
352    /// [`AccountPatch`].
353    ///
354    /// Use this only when materializing an account absent from both layers is the
355    /// desired behavior. For normal patches to known accounts, use
356    /// [`account`](Self::account).
357    pub fn account_upsert(address: Address, patch: AccountPatch) -> Self {
358        Self::AccountUpsert { address, patch }
359    }
360
361    /// Construct a [`StateUpdate::Purge`] for `address` at `scope`.
362    pub fn purge(address: Address, scope: PurgeScope) -> Self {
363        Self::Purge { address, scope }
364    }
365}
366
367/// A partial account mutation: each `Some` field overwrites the cached value,
368/// each `None` leaves it unchanged. Setting `code` recomputes the code hash;
369/// `Some(empty bytes)` clears code to the empty-code hash.
370///
371/// Partial (rather than a full revm `AccountInfo`) because the Pillar B driver
372/// is events, which usually carry *one* field (a `Transfer` changes a balance,
373/// not nonce/code). This avoids forcing a caller to reconstruct a full
374/// `AccountInfo` and keeps revm's type out of the public vocabulary.
375///
376/// The struct is `#[non_exhaustive]`: new fields may be added pre-1.0 without a
377/// breaking change. Construct it via [`AccountPatch::default`] + the builders
378/// ([`balance`](Self::balance) / [`nonce`](Self::nonce) / [`code`](Self::code)),
379/// never a struct literal.
380///
381/// # Warning
382///
383/// Applying an absolute patch with [`StateUpdate::Account`] on an address absent
384/// from **both** cache layers is skipped and surfaced in
385/// [`StateDiff::skipped_accounts`]. Use [`StateUpdate::AccountUpsert`] only when
386/// default values for un-patched fields (e.g. nonce `0`, empty code) should become
387/// authoritative in the backend.
388///
389/// ```
390/// use alloy_primitives::{Bytes, U256};
391/// use evm_fork_cache::AccountPatch;
392///
393/// let patch = AccountPatch::default()
394///     .balance(U256::from(42))
395///     .nonce(7)
396///     .code(Bytes::from_static(&[0x60, 0x00]));
397/// assert_eq!(patch.balance, Some(U256::from(42)));
398/// assert_eq!(patch.nonce, Some(7));
399/// assert_eq!(AccountPatch::default().balance, None);
400/// ```
401#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
402#[non_exhaustive]
403pub struct AccountPatch {
404    /// New balance, if set.
405    pub balance: Option<U256>,
406    /// New nonce, if set.
407    pub nonce: Option<u64>,
408    /// New runtime code, if set. Setting it recomputes the code hash; empty
409    /// bytes clear the code to the empty-code hash.
410    pub code: Option<Bytes>,
411}
412
413impl AccountPatch {
414    /// Set the balance to overwrite (builder style).
415    pub fn balance(mut self, balance: U256) -> Self {
416        self.balance = Some(balance);
417        self
418    }
419
420    /// Set the nonce to overwrite (builder style).
421    pub fn nonce(mut self, nonce: u64) -> Self {
422        self.nonce = Some(nonce);
423        self
424    }
425
426    /// Set the runtime code to overwrite (builder style). The code hash is
427    /// recomputed from these bytes when the patch is applied.
428    pub fn code(mut self, code: Bytes) -> Self {
429        self.code = Some(code);
430        self
431    }
432}
433
434/// What part of an address's cached state a purge removes.
435///
436/// The enum is `#[non_exhaustive]`: new scopes may be added pre-1.0 without a
437/// breaking change.
438///
439/// # `StorageCleared`/`NotExisting` accounts
440/// Purging *storage* ([`AllStorage`](Self::AllStorage) / [`Slots`](Self::Slots))
441/// removes the slot from the backend so a normal forked account re-fetches it on
442/// the next read. For an account revm marks `StorageCleared`/`NotExisting` (a
443/// locally-created/cleared account, e.g. after a `CREATE`/selfdestruct), the EVM
444/// reads a missing slot as **zero without re-fetching** — its storage is locally
445/// complete — so a purged slot reads `0`, not a fresh RPC value. This is correct
446/// (such an account has no on-chain storage to refetch), but it means
447/// [`Slots`](Self::Slots) does not force a refetch for those accounts. Use
448/// [`Account`](Self::Account) (or `purge_account`) to fully drop the account.
449#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
450#[non_exhaustive]
451pub enum PurgeScope {
452    /// Full account: `AccountInfo` (balance/nonce/code) **and** all storage.
453    /// Equivalent to
454    /// [`EvmCache::purge_account`](crate::cache::EvmCache::purge_account).
455    Account,
456    /// All storage slots; account info preserved. Equivalent to
457    /// [`EvmCache::purge_contract_storage`](crate::cache::EvmCache::purge_contract_storage).
458    AllStorage,
459    /// Only the listed storage slots. Equivalent to
460    /// [`EvmCache::purge_contract_slots`](crate::cache::EvmCache::purge_contract_slots).
461    Slots(Vec<U256>),
462}
463
464/// What an `apply_*` call actually changed.
465///
466/// Returned by [`EvmCache::apply_update`](crate::cache::EvmCache::apply_update)
467/// and [`apply_updates`](crate::cache::EvmCache::apply_updates). Only real
468/// changes are recorded, so a no-op write yields a [`Default`] (empty) diff.
469///
470/// The struct is `#[non_exhaustive]`: it has grown fields pre-1.0
471/// ([`skipped`](Self::skipped), [`skipped_balances`](Self::skipped_balances),
472/// [`skipped_masks`](Self::skipped_masks), and
473/// [`skipped_accounts`](Self::skipped_accounts)) and may grow more. Construct it
474/// via [`Default`] + field assignment, never an exhaustive struct literal.
475///
476/// # Checking for skips
477///
478/// [`is_empty`](Self::is_empty) / [`len`](Self::len) are **changes-only**, so a
479/// cold-skipped update ([`SlotDelta`](StateUpdate::SlotDelta) /
480/// [`BalanceDelta`](StateUpdate::BalanceDelta) /
481/// [`SlotMasked`](StateUpdate::SlotMasked) / [`Account`](StateUpdate::Account))
482/// is invisible to them. After applying cold-aware updates, check
483/// [`has_skipped`](Self::has_skipped) (or inspect the `skipped_*` fields) — a cold
484/// target was dropped, not applied.
485#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
486#[non_exhaustive]
487pub struct StateDiff {
488    /// Storage slots whose value changed (`old != new`).
489    pub slots: Vec<SlotChange>,
490    /// Accounts whose balance/nonce/code-hash changed.
491    pub accounts: Vec<AccountChange>,
492    /// Purges performed, with what they removed.
493    pub purged: Vec<PurgeRecord>,
494    /// Relative slot updates ([`StateUpdate::SlotDelta`]) that were **not** applied
495    /// because the target slot's current value was unknown (cold). This is
496    /// informational metadata, not a change: it does **not** affect
497    /// [`is_empty`](Self::is_empty) / [`len`](Self::len).
498    pub skipped: Vec<SkippedDelta>,
499    /// Relative balance updates ([`StateUpdate::BalanceDelta`]) that were **not**
500    /// applied because the target account was absent from both layers (its balance
501    /// was unknown). Like [`skipped`](Self::skipped) this is informational
502    /// metadata, not a change.
503    pub skipped_balances: Vec<SkippedBalanceDelta>,
504    /// Masked slot updates ([`StateUpdate::SlotMasked`]) that were **not** applied
505    /// because the target slot's current value was unknown (cold) — the un-masked
506    /// bits could not be preserved. Like [`skipped`](Self::skipped) this is
507    /// informational metadata, not a change.
508    pub skipped_masks: Vec<SkippedMask>,
509    /// Account patches ([`StateUpdate::Account`]) that were **not** applied
510    /// because the account was absent from both layers. Like
511    /// [`skipped`](Self::skipped) this is informational metadata, not a change.
512    pub skipped_accounts: Vec<SkippedAccountPatch>,
513}
514
515impl StateDiff {
516    /// Whether the diff recorded no change at all.
517    ///
518    /// Changes-only: counts `slots` + `accounts` + `purged`. Any skipped update
519    /// (any of the `skipped_*` fields) is informational metadata, not a change, so
520    /// it does not affect this.
521    pub fn is_empty(&self) -> bool {
522        self.slots.is_empty() && self.accounts.is_empty() && self.purged.is_empty()
523    }
524
525    /// Total number of changed entries (slots + accounts + purges).
526    ///
527    /// Changes-only: skipped updates (any `skipped_*` field) are not counted (a
528    /// skip is not a change). See [`skipped_len`](Self::skipped_len) for the skip
529    /// count.
530    pub fn len(&self) -> usize {
531        self.slots.len() + self.accounts.len() + self.purged.len()
532    }
533
534    /// Whether any cold-aware update was skipped (relative slot, balance, masked
535    /// slot, **or** cold account patch).
536    ///
537    /// `true` iff any of [`skipped`](Self::skipped),
538    /// [`skipped_balances`](Self::skipped_balances),
539    /// [`skipped_masks`](Self::skipped_masks), or
540    /// [`skipped_accounts`](Self::skipped_accounts) is non-empty. A cold-skipped
541    /// update produces no change, so it is invisible to
542    /// [`is_empty`](Self::is_empty) — callers applying cold-aware updates should
543    /// check this to avoid silently dropping an update.
544    pub fn has_skipped(&self) -> bool {
545        !self.skipped.is_empty()
546            || !self.skipped_balances.is_empty()
547            || !self.skipped_masks.is_empty()
548            || !self.skipped_accounts.is_empty()
549    }
550
551    /// Total number of skipped relative/masked updates (`skipped` +
552    /// `skipped_balances` + `skipped_masks` + `skipped_accounts`).
553    pub fn skipped_len(&self) -> usize {
554        self.skipped.len()
555            + self.skipped_balances.len()
556            + self.skipped_masks.len()
557            + self.skipped_accounts.len()
558    }
559
560    /// Whether every relative update in the apply was applied (none skipped).
561    ///
562    /// The inverse of [`has_skipped`](Self::has_skipped).
563    pub fn is_fully_applied(&self) -> bool {
564        !self.has_skipped()
565    }
566
567    /// Fold `other` into `self`, concatenating each category.
568    ///
569    /// Used by [`apply_updates`](crate::cache::EvmCache::apply_updates) to merge
570    /// per-update diffs; the concatenation preserves order, so two writes to the
571    /// same slot record their `old → new` history in sequence. The `skipped`,
572    /// `skipped_balances`, `skipped_masks`, and `skipped_accounts` metadata are
573    /// concatenated too.
574    pub fn merge(&mut self, other: StateDiff) {
575        self.slots.extend(other.slots);
576        self.accounts.extend(other.accounts);
577        self.purged.extend(other.purged);
578        self.skipped.extend(other.skipped);
579        self.skipped_balances.extend(other.skipped_balances);
580        self.skipped_masks.extend(other.skipped_masks);
581        self.skipped_accounts.extend(other.skipped_accounts);
582    }
583}
584
585/// An account field delta. Each field is `Some((old, new))` only when it changed.
586#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
587pub struct AccountChange {
588    /// Account whose fields changed.
589    pub address: Address,
590    /// Balance delta `(old, new)`, present only if the balance changed.
591    pub balance: Option<(U256, U256)>,
592    /// Nonce delta `(old, new)`, present only if the nonce changed.
593    pub nonce: Option<(u64, u64)>,
594    /// Code-hash delta `(old, new)`, present only if the code changed.
595    pub code_hash: Option<(B256, B256)>,
596}
597
598/// Record of a purge: how much of each layer it removed.
599#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
600pub struct PurgeRecord {
601    /// Account that was purged.
602    pub address: Address,
603    /// The scope that was applied.
604    pub scope: PurgeScope,
605    /// Storage slots removed from the BlockchainDb backend (layer 2).
606    pub slots_removed: usize,
607    /// Whether an `AccountInfo` was removed (only the [`PurgeScope::Account`] scope).
608    pub account_removed: bool,
609}
610
611/// A relative update ([`StateUpdate::SlotDelta`]) that could not be applied
612/// because the slot's current value is unknown (not cached in either layer).
613///
614/// A delta against a cold slot is skipped rather than applied (applying `0 ±
615/// amount` would corrupt an unknown value and, write-through, make it
616/// authoritative). It is surfaced here so the caller can fetch+seed the true
617/// value and retry; otherwise the next read lazily fetches it.
618#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
619pub struct SkippedDelta {
620    /// Contract whose storage slot the delta targeted.
621    pub address: Address,
622    /// Storage slot key that was cold.
623    pub slot: U256,
624    /// The delta that was not applied.
625    pub delta: SlotDelta,
626}
627
628/// A relative balance update ([`StateUpdate::BalanceDelta`]) that could not be
629/// applied because the account's info is unknown to the EVM — absent from
630/// **both** cache layers, or present in the overlay as revm `NotExisting` (so its
631/// native balance is unknown).
632///
633/// A delta against a cold account is skipped rather than applied (applying it
634/// against an assumed-zero balance would corrupt an unknown value, and
635/// materializing a default account would mask the real on-chain one). It is
636/// surfaced here so the caller can fetch+seed the account and retry.
637///
638/// Deliberately **not** `#[non_exhaustive]`: it is a stable, fully-determined leaf
639/// record routinely constructed as a struct literal in equality assertions by the
640/// test suite and downstream users testing against a returned diff.
641#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
642pub struct SkippedBalanceDelta {
643    /// Account whose native balance the delta targeted.
644    pub address: Address,
645    /// The delta that was not applied.
646    pub delta: SlotDelta,
647}
648
649/// A masked write ([`StateUpdate::SlotMasked`]) that could not be applied because
650/// the target slot's current value is unknown (not cached in either layer).
651///
652/// A masked write needs the slot's current value to preserve the un-masked bits
653/// (`new = (old & !mask) | (value & mask)`); without it the result cannot be
654/// computed, so the write is skipped rather than applied against an assumed value.
655/// It is surfaced here so the caller can fetch+seed the slot and retry; otherwise
656/// the next read lazily fetches the true value.
657///
658/// Deliberately **not** `#[non_exhaustive]`: it is a stable, fully-determined leaf
659/// record routinely constructed as a struct literal in equality assertions by the
660/// test suite and downstream users testing against a returned diff.
661#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
662pub struct SkippedMask {
663    /// Contract whose storage slot the masked write targeted.
664    pub address: Address,
665    /// Storage slot key that was cold.
666    pub slot: U256,
667    /// The mask that was not applied.
668    pub mask: U256,
669    /// The value bits that were not applied.
670    pub value: U256,
671}
672
673/// An account patch ([`StateUpdate::Account`]) that could not be applied because
674/// the account's info is unknown to the EVM — absent from **both** cache layers,
675/// or present in the overlay as revm `NotExisting`.
676///
677/// A partial patch against a cold account is skipped rather than applied against
678/// [`AccountInfo::default`](revm::state::AccountInfo::default), because default
679/// nonce/code would become authoritative and mask a later RPC fetch. It is
680/// surfaced here so the caller can fetch+seed the account and retry, or opt in to
681/// materialization with [`StateUpdate::AccountUpsert`].
682#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
683pub struct SkippedAccountPatch {
684    /// Account whose patch was skipped.
685    pub address: Address,
686    /// The patch that was not applied.
687    pub patch: AccountPatch,
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693
694    fn addr(n: u8) -> Address {
695        Address::repeat_byte(n)
696    }
697
698    #[test]
699    fn account_patch_default_is_all_none() {
700        let p = AccountPatch::default();
701        assert_eq!(p.balance, None);
702        assert_eq!(p.nonce, None);
703        assert_eq!(p.code, None);
704    }
705
706    #[test]
707    fn account_patch_builders_compose() {
708        let p = AccountPatch::default()
709            .balance(U256::from(42))
710            .nonce(7)
711            .code(Bytes::from_static(&[0x60, 0x00]));
712        assert_eq!(p.balance, Some(U256::from(42)));
713        assert_eq!(p.nonce, Some(7));
714        assert_eq!(p.code, Some(Bytes::from_static(&[0x60, 0x00])));
715    }
716
717    #[test]
718    fn state_update_constructors_produce_expected_variants() {
719        let a = addr(0xaa);
720
721        assert_eq!(
722            StateUpdate::slot(a, U256::from(1), U256::from(2)),
723            StateUpdate::Slot {
724                address: a,
725                slot: U256::from(1),
726                value: U256::from(2),
727            }
728        );
729        assert_eq!(
730            StateUpdate::balance(a, U256::from(9)),
731            StateUpdate::Account {
732                address: a,
733                patch: AccountPatch::default().balance(U256::from(9)),
734            }
735        );
736        assert_eq!(
737            StateUpdate::account_upsert(a, AccountPatch::default().balance(U256::from(9))),
738            StateUpdate::AccountUpsert {
739                address: a,
740                patch: AccountPatch::default().balance(U256::from(9)),
741            }
742        );
743        assert_eq!(
744            StateUpdate::purge(a, PurgeScope::Account),
745            StateUpdate::Purge {
746                address: a,
747                scope: PurgeScope::Account,
748            }
749        );
750    }
751
752    #[test]
753    fn state_diff_default_is_empty() {
754        let d = StateDiff::default();
755        assert!(d.is_empty());
756        assert_eq!(d.len(), 0);
757    }
758
759    #[test]
760    fn state_diff_merge_concatenates_and_counts() {
761        let a = addr(0xbb);
762        let mut left = StateDiff::default();
763        left.slots.push(SlotChange {
764            address: a,
765            slot: U256::from(1),
766            old: U256::ZERO,
767            new: U256::from(5),
768        });
769
770        let mut right = StateDiff::default();
771        right.accounts.push(AccountChange {
772            address: a,
773            balance: Some((U256::ZERO, U256::from(3))),
774            nonce: None,
775            code_hash: None,
776        });
777        right.purged.push(PurgeRecord {
778            address: a,
779            scope: PurgeScope::AllStorage,
780            slots_removed: 2,
781            account_removed: false,
782        });
783
784        left.merge(right);
785        assert!(!left.is_empty());
786        assert_eq!(left.len(), 3);
787        assert_eq!(left.slots.len(), 1);
788        assert_eq!(left.accounts.len(), 1);
789        assert_eq!(left.purged.len(), 1);
790        // Concatenation preserves the merged-in slot value.
791        assert_eq!(left.slots[0].new, U256::from(5));
792    }
793
794    #[test]
795    fn slot_delta_add_applies_saturating() {
796        assert_eq!(
797            SlotDelta::Add(U256::from(50)).apply(U256::from(100)),
798            U256::from(150)
799        );
800        // Saturates at U256::MAX rather than wrapping.
801        assert_eq!(
802            SlotDelta::Add(U256::from(10)).apply(U256::MAX - U256::from(1)),
803            U256::MAX
804        );
805        assert_eq!(SlotDelta::Add(U256::from(5)).apply(U256::MAX), U256::MAX);
806    }
807
808    #[test]
809    fn slot_delta_sub_applies_saturating() {
810        assert_eq!(
811            SlotDelta::Sub(U256::from(30)).apply(U256::from(100)),
812            U256::from(70)
813        );
814        // Saturates at zero rather than underflowing.
815        assert_eq!(
816            SlotDelta::Sub(U256::from(50)).apply(U256::from(30)),
817            U256::ZERO
818        );
819        assert_eq!(SlotDelta::Sub(U256::from(1)).apply(U256::ZERO), U256::ZERO);
820    }
821
822    #[test]
823    fn state_update_slot_delta_constructor() {
824        let a = addr(0xcc);
825        assert_eq!(
826            StateUpdate::slot_delta(a, U256::from(1), SlotDelta::Add(U256::from(2))),
827            StateUpdate::SlotDelta {
828                address: a,
829                slot: U256::from(1),
830                delta: SlotDelta::Add(U256::from(2)),
831            }
832        );
833    }
834
835    #[test]
836    fn state_diff_merge_extends_skipped_without_counting_it() {
837        let a = addr(0xdd);
838        let mut left = StateDiff::default();
839        let mut right = StateDiff::default();
840        right.skipped.push(SkippedDelta {
841            address: a,
842            slot: U256::from(1),
843            delta: SlotDelta::Sub(U256::from(3)),
844        });
845
846        left.merge(right);
847        assert_eq!(left.skipped.len(), 1);
848        // A skip is metadata, not a change.
849        assert!(left.is_empty());
850        assert_eq!(left.len(), 0);
851    }
852
853    #[test]
854    fn slot_masked_constructor_produces_variant() {
855        let a = addr(0xee);
856        assert_eq!(
857            StateUpdate::slot_masked(a, U256::from(1), U256::from(0xFF), U256::from(0x42)),
858            StateUpdate::SlotMasked {
859                address: a,
860                slot: U256::from(1),
861                mask: U256::from(0xFF),
862                value: U256::from(0x42),
863            }
864        );
865    }
866
867    #[test]
868    fn state_diff_merge_extends_skipped_masks_without_counting_it() {
869        let a = addr(0xef);
870        let mut left = StateDiff::default();
871        let mut right = StateDiff::default();
872        right.skipped_masks.push(SkippedMask {
873            address: a,
874            slot: U256::from(1),
875            mask: U256::from(0xFF),
876            value: U256::from(0x42),
877        });
878
879        left.merge(right);
880        assert_eq!(left.skipped_masks.len(), 1);
881        // A masked skip is metadata, not a change.
882        assert!(left.is_empty());
883        assert_eq!(left.len(), 0);
884        // But it is discoverable through the skip accessors.
885        assert!(left.has_skipped());
886        assert_eq!(left.skipped_len(), 1);
887        assert!(!left.is_fully_applied());
888    }
889
890    #[test]
891    fn slot_masked_serde_round_trips() {
892        let a = addr(0xf0);
893        let update = StateUpdate::slot_masked(a, U256::from(5), U256::from(0xFF), U256::from(3));
894        let json = serde_json::to_string(&update).expect("serialize");
895        let back: StateUpdate = serde_json::from_str(&json).expect("deserialize");
896        assert_eq!(update, back);
897
898        let mask = SkippedMask {
899            address: a,
900            slot: U256::from(1),
901            mask: U256::from(0xFF),
902            value: U256::from(2),
903        };
904        let json = serde_json::to_string(&mask).expect("serialize");
905        let back: SkippedMask = serde_json::from_str(&json).expect("deserialize");
906        assert_eq!(mask, back);
907    }
908}