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}