evm-fork-cache 0.2.1

Forked EVM state cache, snapshots, overlays, and simulation utilities for EVM search
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
//! Targeted state-mutation vocabulary and the structured diff it produces
//! (Pillar B.1 — the *writer half* of the event → state pipeline).
//!
//! This module defines the small, generic vocabulary a future event decoder
//! emits and [`EvmCache::apply_update`](crate::cache::EvmCache::apply_update)
//! consumes, plus the [`StateDiff`] that records what an apply actually changed.
//! It is pure data and logic on itself: it carries no protocol or event
//! knowledge and has no dependency on the cache implementation.
//!
//! # The vocabulary
//!
//! A [`StateUpdate`] is one targeted mutation:
//!
//! - [`StateUpdate::Slot`] — set a single storage slot, authoritative across
//!   both cache layers.
//! - [`StateUpdate::Account`] — apply a partial [`AccountPatch`]
//!   (`balance`/`nonce`/`code`, each optional) to an already-known account.
//! - [`StateUpdate::AccountUpsert`] — intentionally materialize a cold account
//!   from a partial [`AccountPatch`].
//! - [`StateUpdate::Purge`] — drop cached state at a [`PurgeScope`] so the next
//!   read re-fetches.
//!
//! # The dual-layer write-through policy
//!
//! [`apply_update`](crate::cache::EvmCache::apply_update) applies `Slot` writes
//! through with one consistent rule: the BlockchainDb backend (layer 2) is
//! written **always**; the CacheDB overlay (layer 1) is written **only if an
//! overlay account already exists** for the address. A new overlay account is
//! never materialized for a slot write — the read path falls through to the
//! backend for an absent overlay entry, so a backend-only write is authoritative,
//! and materializing an overlay entry would pollute layer 1 and could shadow
//! later RPC reads. (This mirrors the established
//! [`inject_storage_batch_fresh`](crate::cache::EvmCache::inject_storage_batch_fresh)
//! semantics.)
//!
//! `Account` patches follow the same overlay-if-present write-through policy
//! once the account is already present in either layer. If the account is absent
//! from **both** layers, the patch is skipped and surfaced in
//! [`StateDiff::skipped_accounts`]. Use [`StateUpdate::AccountUpsert`] when the
//! caller intentionally wants to materialize a cold/default account.
//!
//! # The output
//!
//! Every apply returns a [`StateDiff`] of the changes it actually made: the
//! [`SlotChange`]s, [`AccountChange`]s, and [`PurgeRecord`]s. **Only real changes
//! are recorded** — re-applying a value the cache already holds yields an empty
//! diff, so idempotence is observable.
//!
//! # Relative updates / cold-aware read-modify-write
//!
//! Some callers learn only a *delta* (an ERC-20 `Transfer` log carries the
//! transferred `amount`, not the resulting balances), so the vocabulary also
//! supports *relative* updates: [`StateUpdate::SlotDelta`] reads the current slot
//! value, applies a saturating [`SlotDelta`] (`Add` clamps at `U256::MAX`, `Sub`
//! at `U256::ZERO`), and writes the result back through both layers. The general
//! closure form is
//! [`EvmCache::modify_slot`](crate::cache::EvmCache::modify_slot).
//!
//! A relative update is only valid against a value the cache *actually holds*. An
//! un-fetched ("cold") slot has no value, and applying a delta to it would compute
//! `0 ± amount`, write a wrong value, and (write-through) make it authoritative —
//! silently corrupting state. So relative application is **cold-aware**: a
//! `SlotDelta` on a cold slot is **not applied**; it is recorded in
//! [`StateDiff::skipped`] as a [`SkippedDelta`] so the caller can fetch+seed the
//! true value (the next read otherwise lazily fetches it). `modify_slot` hands its
//! closure an `Option<U256>` (`None` when cold) and lets the caller decide.
//!
//! # Masked writes to packed words
//!
//! A storage slot often packs several fields into one word. A decoder that
//! learns only some of those fields must update *just* its bits without
//! clobbering the rest. [`StateUpdate::SlotMasked`] is the
//! cold-aware masked read-modify-write for exactly this: it computes
//! `new = (old & !mask) | (value & mask)`, touching only the `mask` bits. Like
//! [`SlotDelta`](StateUpdate::SlotDelta) it is cold-aware — the un-masked bits of
//! a slot absent from both layers are unknown, so a masked write to a cold slot
//! is **not** applied; it is surfaced in [`StateDiff::skipped_masks`] as a
//! [`SkippedMask`]. (A full-mask `SlotMasked` matches an absolute
//! [`Slot`](StateUpdate::Slot) write on a hot slot but still skips on a cold one.)
//!
//! The same relative, cold-aware rule extends to an account's **native balance**:
//! [`StateUpdate::BalanceDelta`] (and the closure form
//! [`EvmCache::modify_account_balance`](crate::cache::EvmCache::modify_account_balance))
//! read-modify-write `AccountInfo::balance`, preserving nonce/code. "Cold" here
//! means the account is absent from *both* layers (its balance is unknown); a
//! `BalanceDelta` on a cold account is **not applied** — it is surfaced in
//! [`StateDiff::skipped_balances`] as a [`SkippedBalanceDelta`]. This avoids
//! materializing a default account that would mask the real on-chain one.
//!
//! # Checking for skips
//!
//! Because a cold-skipped relative update produces **no** change, it is invisible
//! to the natural [`StateDiff::is_empty`] / [`StateDiff::len`] success check (those
//! are changes-only). A caller applying relative updates **must** therefore check
//! [`StateDiff::has_skipped`] (or inspect [`skipped`](StateDiff::skipped),
//! [`skipped_balances`](StateDiff::skipped_balances),
//! [`skipped_masks`](StateDiff::skipped_masks), or
//! [`skipped_accounts`](StateDiff::skipped_accounts)) — a cold target was
//! dropped, not applied, and a silently-dropped balance/account update can break
//! conservation. [`StateDiff::is_fully_applied`] and
//! [`StateDiff::skipped_len`] are the companions.
//!
//! # Boundary — events are Phase 4
//!
//! This is the vocabulary a Phase 4 `EventDecoder` will *emit into*; Phase 3
//! does not decode events. Nothing here parses a `Log` or knows a protocol's
//! storage layout — that is the *reader half* of Pillar B and lands later.

use alloy_primitives::{Address, B256, Bytes, U256};

use crate::freshness::SlotChange;

/// A relative storage-slot mutation: read the current value, transform it, and
/// write it back.
///
/// Both directions **saturate** rather than wrap: `Add` clamps at `U256::MAX`
/// and `Sub` clamps at `U256::ZERO`. This is the delta a caller derives from an
/// event (e.g. an ERC-20 `Transfer` amount) without knowing the resulting
/// absolute value. It is applied by [`StateUpdate::SlotDelta`] (cold-aware — see
/// the module docs).
///
/// ```
/// use alloy_primitives::U256;
/// use evm_fork_cache::SlotDelta;
///
/// assert_eq!(SlotDelta::Add(U256::from(50)).apply(U256::from(100)), U256::from(150));
/// assert_eq!(SlotDelta::Sub(U256::from(50)).apply(U256::from(30)), U256::ZERO);
/// assert_eq!(SlotDelta::Add(U256::from(10)).apply(U256::MAX), U256::MAX);
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SlotDelta {
    /// Add to the current value, saturating at `U256::MAX`.
    Add(U256),
    /// Subtract from the current value, saturating at `U256::ZERO`.
    Sub(U256),
}

impl SlotDelta {
    /// Apply the (saturating) delta to a current value.
    ///
    /// `Add` uses `saturating_add` (clamps at `U256::MAX`); `Sub` uses
    /// `saturating_sub` (clamps at `U256::ZERO`).
    pub fn apply(self, current: U256) -> U256 {
        match self {
            SlotDelta::Add(amount) => current.saturating_add(amount),
            SlotDelta::Sub(amount) => current.saturating_sub(amount),
        }
    }
}

/// A single targeted mutation to cached EVM state.
///
/// The vocabulary an event decoder (Phase 4) emits and
/// [`EvmCache::apply_update`](crate::cache::EvmCache::apply_update) consumes.
/// Generic: carries no protocol or event knowledge.
///
/// The enum is `#[non_exhaustive]`: new variants (e.g. a code-only convenience)
/// may be added pre-1.0 without a breaking change.
///
/// ```
/// use alloy_primitives::{Address, U256};
/// use evm_fork_cache::{AccountPatch, PurgeScope, StateUpdate};
///
/// let contract = Address::repeat_byte(0x01);
///
/// // A storage-slot write (authoritative across both cache layers).
/// let slot = StateUpdate::slot(contract, U256::from(0), U256::from(42));
///
/// // A balance-only account patch (nonce and code left untouched).
/// let bal = StateUpdate::balance(contract, U256::from(1_000));
/// assert_eq!(
///     bal,
///     StateUpdate::Account { address: contract, patch: AccountPatch::default().balance(U256::from(1_000)) },
/// );
///
/// // Drop just two storage slots so the next read re-fetches them.
/// let purge = StateUpdate::purge(contract, PurgeScope::Slots(vec![U256::from(0), U256::from(1)]));
/// # let _ = (slot, purge);
/// ```
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum StateUpdate {
    /// Set one storage slot to `value`, authoritative across both cache layers.
    Slot {
        /// Contract whose storage is written.
        address: Address,
        /// Storage slot key.
        slot: U256,
        /// New slot value.
        value: U256,
    },
    /// Apply a *relative* (saturating) mutation to one storage slot.
    ///
    /// Read-modify-write: the current value is read, the [`SlotDelta`] applied,
    /// and the result written back through both layers. **Cold-aware** — a delta
    /// on a slot absent from both layers is not applied; it is surfaced in
    /// [`StateDiff::skipped`] instead (see the module docs).
    SlotDelta {
        /// Contract whose storage is written.
        address: Address,
        /// Storage slot key.
        slot: U256,
        /// The relative, saturating mutation to apply to the current value.
        delta: SlotDelta,
    },
    /// Apply a *relative* (saturating) mutation to an account's **native balance**.
    ///
    /// Read-modify-write: the current `AccountInfo::balance` is read, the
    /// [`SlotDelta`] applied, and the result written back through both layers
    /// (nonce and code preserved). **Cold-aware** — "cold" here means the
    /// account's info is unknown to the EVM: absent from *both* layers, **or**
    /// present in the overlay as revm `NotExisting` (which the internal account
    /// read also treats as cold, mirroring `DbAccount::info()`). A `BalanceDelta`
    /// on a cold account is not applied; it is surfaced in
    /// [`StateDiff::skipped_balances`] instead (so no default account is
    /// materialized to mask the real on-chain one — see the module docs).
    BalanceDelta {
        /// Account whose native balance is mutated.
        address: Address,
        /// The relative, saturating mutation to apply to the current balance.
        delta: SlotDelta,
    },
    /// Set only the `mask` bits of a storage slot to the corresponding bits of
    /// `value`, preserving the rest: `new = (old & !mask) | (value & mask)`.
    ///
    /// A *masked* read-modify-write: it lets a pure decoder express a partial
    /// update to a **packed** storage word without knowing or clobbering the bits
    /// it does not own.
    ///
    /// **Cold-aware** — a masked write to a slot absent from *both* cache layers is
    /// **not** applied (the un-masked bits are unknown, so the result cannot be
    /// computed); it is surfaced in [`StateDiff::skipped_masks`] as a
    /// [`SkippedMask`] instead. A masked write with `mask == U256::MAX` equals an
    /// absolute [`Slot`](Self::Slot) write on a *hot* slot, but **still skips** on a
    /// cold one (unlike [`Slot`](Self::Slot), which writes unconditionally). Use
    /// [`Slot`](Self::Slot) for an unconditional absolute write; use `SlotMasked`
    /// when neighbouring bits must be preserved.
    SlotMasked {
        /// Contract whose storage is written.
        address: Address,
        /// Storage slot key.
        slot: U256,
        /// Which bits of the slot to overwrite (1 = take from `value`).
        mask: U256,
        /// The bits to write (only the bits selected by `mask` are applied).
        value: U256,
    },
    /// Patch an already-known account's balance/nonce/code (partial — see
    /// [`AccountPatch`]).
    ///
    /// Cold-aware: if the account's info is unknown to the EVM (absent from
    /// **both** layers, or present in the overlay as revm `NotExisting`), the patch
    /// is not applied and is surfaced in [`StateDiff::skipped_accounts`] as a
    /// [`SkippedAccountPatch`]. Use [`StateUpdate::AccountUpsert`] when
    /// materializing a cold/default account is intentional.
    Account {
        /// Account to patch.
        address: Address,
        /// The partial mutation: each `Some` field overwrites, `None` leaves it.
        patch: AccountPatch,
    },
    /// Apply an [`AccountPatch`], materializing a cold account when needed.
    ///
    /// This is the explicit escape hatch for callers that really do want a
    /// default account to become authoritative in the backend (for example a
    /// synthetic test account). Normal event-derived account patches should use
    /// [`StateUpdate::Account`] so a cold account is skipped instead of masking a
    /// future RPC fetch.
    AccountUpsert {
        /// Account to patch or create.
        address: Address,
        /// The partial mutation: each `Some` field overwrites, `None` leaves it.
        patch: AccountPatch,
    },
    /// Purge cached state for `address` at `scope`; the next read re-fetches.
    Purge {
        /// Account whose cached state is purged.
        address: Address,
        /// What part of the cached state to remove.
        scope: PurgeScope,
    },
}

impl StateUpdate {
    /// Construct a [`StateUpdate::Slot`] that sets `(address, slot)` to `value`.
    pub fn slot(address: Address, slot: U256, value: U256) -> Self {
        Self::Slot {
            address,
            slot,
            value,
        }
    }

    /// Construct a [`StateUpdate::SlotDelta`] that applies `delta` relative to the
    /// current value of `(address, slot)`.
    pub fn slot_delta(address: Address, slot: U256, delta: SlotDelta) -> Self {
        Self::SlotDelta {
            address,
            slot,
            delta,
        }
    }

    /// Construct a [`StateUpdate::SlotMasked`] that sets only the `mask` bits of
    /// `(address, slot)` to the corresponding bits of `value`.
    pub fn slot_masked(address: Address, slot: U256, mask: U256, value: U256) -> Self {
        Self::SlotMasked {
            address,
            slot,
            mask,
            value,
        }
    }

    /// Construct a [`StateUpdate::BalanceDelta`] that applies `delta` relative to
    /// the account's current native balance.
    pub fn balance_delta(address: Address, delta: SlotDelta) -> Self {
        Self::BalanceDelta { address, delta }
    }

    /// Construct a [`StateUpdate::Account`] that patches only the balance.
    pub fn balance(address: Address, value: U256) -> Self {
        Self::Account {
            address,
            patch: AccountPatch::default().balance(value),
        }
    }

    /// Construct a [`StateUpdate::Account`] that patches only the nonce.
    pub fn nonce(address: Address, nonce: u64) -> Self {
        Self::Account {
            address,
            patch: AccountPatch::default().nonce(nonce),
        }
    }

    /// Construct a [`StateUpdate::Account`] that patches only the runtime code
    /// (the code hash is recomputed from `code` when applied).
    pub fn code(address: Address, code: Bytes) -> Self {
        Self::Account {
            address,
            patch: AccountPatch::default().code(code),
        }
    }

    /// Construct a [`StateUpdate::Account`] from a prebuilt [`AccountPatch`].
    pub fn account(address: Address, patch: AccountPatch) -> Self {
        Self::Account { address, patch }
    }

    /// Construct a [`StateUpdate::AccountUpsert`] from a prebuilt
    /// [`AccountPatch`].
    ///
    /// Use this only when materializing an account absent from both layers is the
    /// desired behavior. For normal patches to known accounts, use
    /// [`account`](Self::account).
    pub fn account_upsert(address: Address, patch: AccountPatch) -> Self {
        Self::AccountUpsert { address, patch }
    }

    /// Construct a [`StateUpdate::Purge`] for `address` at `scope`.
    pub fn purge(address: Address, scope: PurgeScope) -> Self {
        Self::Purge { address, scope }
    }
}

/// A partial account mutation: each `Some` field overwrites the cached value,
/// each `None` leaves it unchanged. Setting `code` recomputes the code hash;
/// `Some(empty bytes)` clears code to the empty-code hash.
///
/// Partial (rather than a full revm `AccountInfo`) because the Pillar B driver
/// is events, which usually carry *one* field (a `Transfer` changes a balance,
/// not nonce/code). This avoids forcing a caller to reconstruct a full
/// `AccountInfo` and keeps revm's type out of the public vocabulary.
///
/// The struct is `#[non_exhaustive]`: new fields may be added pre-1.0 without a
/// breaking change. Construct it via [`AccountPatch::default`] + the builders
/// ([`balance`](Self::balance) / [`nonce`](Self::nonce) / [`code`](Self::code)),
/// never a struct literal.
///
/// # Warning
///
/// Applying an absolute patch with [`StateUpdate::Account`] on an address absent
/// from **both** cache layers is skipped and surfaced in
/// [`StateDiff::skipped_accounts`]. Use [`StateUpdate::AccountUpsert`] only when
/// default values for un-patched fields (e.g. nonce `0`, empty code) should become
/// authoritative in the backend.
///
/// ```
/// use alloy_primitives::{Bytes, U256};
/// use evm_fork_cache::AccountPatch;
///
/// let patch = AccountPatch::default()
///     .balance(U256::from(42))
///     .nonce(7)
///     .code(Bytes::from_static(&[0x60, 0x00]));
/// assert_eq!(patch.balance, Some(U256::from(42)));
/// assert_eq!(patch.nonce, Some(7));
/// assert_eq!(AccountPatch::default().balance, None);
/// ```
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct AccountPatch {
    /// New balance, if set.
    pub balance: Option<U256>,
    /// New nonce, if set.
    pub nonce: Option<u64>,
    /// New runtime code, if set. Setting it recomputes the code hash; empty
    /// bytes clear the code to the empty-code hash.
    pub code: Option<Bytes>,
}

impl AccountPatch {
    /// Set the balance to overwrite (builder style).
    pub fn balance(mut self, balance: U256) -> Self {
        self.balance = Some(balance);
        self
    }

    /// Set the nonce to overwrite (builder style).
    pub fn nonce(mut self, nonce: u64) -> Self {
        self.nonce = Some(nonce);
        self
    }

    /// Set the runtime code to overwrite (builder style). The code hash is
    /// recomputed from these bytes when the patch is applied.
    pub fn code(mut self, code: Bytes) -> Self {
        self.code = Some(code);
        self
    }
}

/// What part of an address's cached state a purge removes.
///
/// The enum is `#[non_exhaustive]`: new scopes may be added pre-1.0 without a
/// breaking change.
///
/// # `StorageCleared`/`NotExisting` accounts
/// Purging *storage* ([`AllStorage`](Self::AllStorage) / [`Slots`](Self::Slots))
/// removes the slot from the backend so a normal forked account re-fetches it on
/// the next read. For an account revm marks `StorageCleared`/`NotExisting` (a
/// locally-created/cleared account, e.g. after a `CREATE`/selfdestruct), the EVM
/// reads a missing slot as **zero without re-fetching** — its storage is locally
/// complete — so a purged slot reads `0`, not a fresh RPC value. This is correct
/// (such an account has no on-chain storage to refetch), but it means
/// [`Slots`](Self::Slots) does not force a refetch for those accounts. Use
/// [`Account`](Self::Account) (or `purge_account`) to fully drop the account.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum PurgeScope {
    /// Full account: `AccountInfo` (balance/nonce/code) **and** all storage.
    /// Equivalent to
    /// [`EvmCache::purge_account`](crate::cache::EvmCache::purge_account).
    Account,
    /// All storage slots; account info preserved. Equivalent to
    /// [`EvmCache::purge_contract_storage`](crate::cache::EvmCache::purge_contract_storage).
    AllStorage,
    /// Only the listed storage slots. Equivalent to
    /// [`EvmCache::purge_contract_slots`](crate::cache::EvmCache::purge_contract_slots).
    Slots(Vec<U256>),
}

/// What an `apply_*` call actually changed.
///
/// Returned by [`EvmCache::apply_update`](crate::cache::EvmCache::apply_update)
/// and [`apply_updates`](crate::cache::EvmCache::apply_updates). Only real
/// changes are recorded, so a no-op write yields a [`Default`] (empty) diff.
///
/// The struct is `#[non_exhaustive]`: it has grown fields pre-1.0
/// ([`skipped`](Self::skipped), [`skipped_balances`](Self::skipped_balances),
/// [`skipped_masks`](Self::skipped_masks), and
/// [`skipped_accounts`](Self::skipped_accounts)) and may grow more. Construct it
/// via [`Default`] + field assignment, never an exhaustive struct literal.
///
/// # Checking for skips
///
/// [`is_empty`](Self::is_empty) / [`len`](Self::len) are **changes-only**, so a
/// cold-skipped update ([`SlotDelta`](StateUpdate::SlotDelta) /
/// [`BalanceDelta`](StateUpdate::BalanceDelta) /
/// [`SlotMasked`](StateUpdate::SlotMasked) / [`Account`](StateUpdate::Account))
/// is invisible to them. After applying cold-aware updates, check
/// [`has_skipped`](Self::has_skipped) (or inspect the `skipped_*` fields) — a cold
/// target was dropped, not applied.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct StateDiff {
    /// Storage slots whose value changed (`old != new`).
    pub slots: Vec<SlotChange>,
    /// Accounts whose balance/nonce/code-hash changed.
    pub accounts: Vec<AccountChange>,
    /// Purges performed, with what they removed.
    pub purged: Vec<PurgeRecord>,
    /// Relative slot updates ([`StateUpdate::SlotDelta`]) that were **not** applied
    /// because the target slot's current value was unknown (cold). This is
    /// informational metadata, not a change: it does **not** affect
    /// [`is_empty`](Self::is_empty) / [`len`](Self::len).
    pub skipped: Vec<SkippedDelta>,
    /// Relative balance updates ([`StateUpdate::BalanceDelta`]) that were **not**
    /// applied because the target account was absent from both layers (its balance
    /// was unknown). Like [`skipped`](Self::skipped) this is informational
    /// metadata, not a change.
    pub skipped_balances: Vec<SkippedBalanceDelta>,
    /// Masked slot updates ([`StateUpdate::SlotMasked`]) that were **not** applied
    /// because the target slot's current value was unknown (cold) — the un-masked
    /// bits could not be preserved. Like [`skipped`](Self::skipped) this is
    /// informational metadata, not a change.
    pub skipped_masks: Vec<SkippedMask>,
    /// Account patches ([`StateUpdate::Account`]) that were **not** applied
    /// because the account was absent from both layers. Like
    /// [`skipped`](Self::skipped) this is informational metadata, not a change.
    pub skipped_accounts: Vec<SkippedAccountPatch>,
}

impl StateDiff {
    /// Whether the diff recorded no change at all.
    ///
    /// Changes-only: counts `slots` + `accounts` + `purged`. Any skipped update
    /// (any of the `skipped_*` fields) is informational metadata, not a change, so
    /// it does not affect this.
    pub fn is_empty(&self) -> bool {
        self.slots.is_empty() && self.accounts.is_empty() && self.purged.is_empty()
    }

    /// Total number of changed entries (slots + accounts + purges).
    ///
    /// Changes-only: skipped updates (any `skipped_*` field) are not counted (a
    /// skip is not a change). See [`skipped_len`](Self::skipped_len) for the skip
    /// count.
    pub fn len(&self) -> usize {
        self.slots.len() + self.accounts.len() + self.purged.len()
    }

    /// Whether any cold-aware update was skipped (relative slot, balance, masked
    /// slot, **or** cold account patch).
    ///
    /// `true` iff any of [`skipped`](Self::skipped),
    /// [`skipped_balances`](Self::skipped_balances),
    /// [`skipped_masks`](Self::skipped_masks), or
    /// [`skipped_accounts`](Self::skipped_accounts) is non-empty. A cold-skipped
    /// update produces no change, so it is invisible to
    /// [`is_empty`](Self::is_empty) — callers applying cold-aware updates should
    /// check this to avoid silently dropping an update.
    pub fn has_skipped(&self) -> bool {
        !self.skipped.is_empty()
            || !self.skipped_balances.is_empty()
            || !self.skipped_masks.is_empty()
            || !self.skipped_accounts.is_empty()
    }

    /// Total number of skipped relative/masked updates (`skipped` +
    /// `skipped_balances` + `skipped_masks` + `skipped_accounts`).
    pub fn skipped_len(&self) -> usize {
        self.skipped.len()
            + self.skipped_balances.len()
            + self.skipped_masks.len()
            + self.skipped_accounts.len()
    }

    /// Whether every relative update in the apply was applied (none skipped).
    ///
    /// The inverse of [`has_skipped`](Self::has_skipped).
    pub fn is_fully_applied(&self) -> bool {
        !self.has_skipped()
    }

    /// Fold `other` into `self`, concatenating each category.
    ///
    /// Used by [`apply_updates`](crate::cache::EvmCache::apply_updates) to merge
    /// per-update diffs; the concatenation preserves order, so two writes to the
    /// same slot record their `old → new` history in sequence. The `skipped`,
    /// `skipped_balances`, `skipped_masks`, and `skipped_accounts` metadata are
    /// concatenated too.
    pub fn merge(&mut self, other: StateDiff) {
        self.slots.extend(other.slots);
        self.accounts.extend(other.accounts);
        self.purged.extend(other.purged);
        self.skipped.extend(other.skipped);
        self.skipped_balances.extend(other.skipped_balances);
        self.skipped_masks.extend(other.skipped_masks);
        self.skipped_accounts.extend(other.skipped_accounts);
    }
}

/// An account field delta. Each field is `Some((old, new))` only when it changed.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AccountChange {
    /// Account whose fields changed.
    pub address: Address,
    /// Balance delta `(old, new)`, present only if the balance changed.
    pub balance: Option<(U256, U256)>,
    /// Nonce delta `(old, new)`, present only if the nonce changed.
    pub nonce: Option<(u64, u64)>,
    /// Code-hash delta `(old, new)`, present only if the code changed.
    pub code_hash: Option<(B256, B256)>,
}

/// Record of a purge: how much of each layer it removed.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PurgeRecord {
    /// Account that was purged.
    pub address: Address,
    /// The scope that was applied.
    pub scope: PurgeScope,
    /// Storage slots removed from the BlockchainDb backend (layer 2).
    pub slots_removed: usize,
    /// Whether an `AccountInfo` was removed (only the [`PurgeScope::Account`] scope).
    pub account_removed: bool,
}

/// A relative update ([`StateUpdate::SlotDelta`]) that could not be applied
/// because the slot's current value is unknown (not cached in either layer).
///
/// A delta against a cold slot is skipped rather than applied (applying `0 ±
/// amount` would corrupt an unknown value and, write-through, make it
/// authoritative). It is surfaced here so the caller can fetch+seed the true
/// value and retry; otherwise the next read lazily fetches it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SkippedDelta {
    /// Contract whose storage slot the delta targeted.
    pub address: Address,
    /// Storage slot key that was cold.
    pub slot: U256,
    /// The delta that was not applied.
    pub delta: SlotDelta,
}

/// A relative balance update ([`StateUpdate::BalanceDelta`]) that could not be
/// applied because the account's info is unknown to the EVM — absent from
/// **both** cache layers, or present in the overlay as revm `NotExisting` (so its
/// native balance is unknown).
///
/// A delta against a cold account is skipped rather than applied (applying it
/// against an assumed-zero balance would corrupt an unknown value, and
/// materializing a default account would mask the real on-chain one). It is
/// surfaced here so the caller can fetch+seed the account and retry.
///
/// Deliberately **not** `#[non_exhaustive]`: it is a stable, fully-determined leaf
/// record routinely constructed as a struct literal in equality assertions by the
/// test suite and downstream users testing against a returned diff.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SkippedBalanceDelta {
    /// Account whose native balance the delta targeted.
    pub address: Address,
    /// The delta that was not applied.
    pub delta: SlotDelta,
}

/// A masked write ([`StateUpdate::SlotMasked`]) that could not be applied because
/// the target slot's current value is unknown (not cached in either layer).
///
/// A masked write needs the slot's current value to preserve the un-masked bits
/// (`new = (old & !mask) | (value & mask)`); without it the result cannot be
/// computed, so the write is skipped rather than applied against an assumed value.
/// It is surfaced here so the caller can fetch+seed the slot and retry; otherwise
/// the next read lazily fetches the true value.
///
/// Deliberately **not** `#[non_exhaustive]`: it is a stable, fully-determined leaf
/// record routinely constructed as a struct literal in equality assertions by the
/// test suite and downstream users testing against a returned diff.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SkippedMask {
    /// Contract whose storage slot the masked write targeted.
    pub address: Address,
    /// Storage slot key that was cold.
    pub slot: U256,
    /// The mask that was not applied.
    pub mask: U256,
    /// The value bits that were not applied.
    pub value: U256,
}

/// An account patch ([`StateUpdate::Account`]) that could not be applied because
/// the account's info is unknown to the EVM — absent from **both** cache layers,
/// or present in the overlay as revm `NotExisting`.
///
/// A partial patch against a cold account is skipped rather than applied against
/// [`AccountInfo::default`](revm::state::AccountInfo::default), because default
/// nonce/code would become authoritative and mask a later RPC fetch. It is
/// surfaced here so the caller can fetch+seed the account and retry, or opt in to
/// materialization with [`StateUpdate::AccountUpsert`].
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SkippedAccountPatch {
    /// Account whose patch was skipped.
    pub address: Address,
    /// The patch that was not applied.
    pub patch: AccountPatch,
}

#[cfg(test)]
mod tests {
    use super::*;

    fn addr(n: u8) -> Address {
        Address::repeat_byte(n)
    }

    #[test]
    fn account_patch_default_is_all_none() {
        let p = AccountPatch::default();
        assert_eq!(p.balance, None);
        assert_eq!(p.nonce, None);
        assert_eq!(p.code, None);
    }

    #[test]
    fn account_patch_builders_compose() {
        let p = AccountPatch::default()
            .balance(U256::from(42))
            .nonce(7)
            .code(Bytes::from_static(&[0x60, 0x00]));
        assert_eq!(p.balance, Some(U256::from(42)));
        assert_eq!(p.nonce, Some(7));
        assert_eq!(p.code, Some(Bytes::from_static(&[0x60, 0x00])));
    }

    #[test]
    fn state_update_constructors_produce_expected_variants() {
        let a = addr(0xaa);

        assert_eq!(
            StateUpdate::slot(a, U256::from(1), U256::from(2)),
            StateUpdate::Slot {
                address: a,
                slot: U256::from(1),
                value: U256::from(2),
            }
        );
        assert_eq!(
            StateUpdate::balance(a, U256::from(9)),
            StateUpdate::Account {
                address: a,
                patch: AccountPatch::default().balance(U256::from(9)),
            }
        );
        assert_eq!(
            StateUpdate::account_upsert(a, AccountPatch::default().balance(U256::from(9))),
            StateUpdate::AccountUpsert {
                address: a,
                patch: AccountPatch::default().balance(U256::from(9)),
            }
        );
        assert_eq!(
            StateUpdate::purge(a, PurgeScope::Account),
            StateUpdate::Purge {
                address: a,
                scope: PurgeScope::Account,
            }
        );
    }

    #[test]
    fn state_diff_default_is_empty() {
        let d = StateDiff::default();
        assert!(d.is_empty());
        assert_eq!(d.len(), 0);
    }

    #[test]
    fn state_diff_merge_concatenates_and_counts() {
        let a = addr(0xbb);
        let mut left = StateDiff::default();
        left.slots.push(SlotChange {
            address: a,
            slot: U256::from(1),
            old: U256::ZERO,
            new: U256::from(5),
        });

        let mut right = StateDiff::default();
        right.accounts.push(AccountChange {
            address: a,
            balance: Some((U256::ZERO, U256::from(3))),
            nonce: None,
            code_hash: None,
        });
        right.purged.push(PurgeRecord {
            address: a,
            scope: PurgeScope::AllStorage,
            slots_removed: 2,
            account_removed: false,
        });

        left.merge(right);
        assert!(!left.is_empty());
        assert_eq!(left.len(), 3);
        assert_eq!(left.slots.len(), 1);
        assert_eq!(left.accounts.len(), 1);
        assert_eq!(left.purged.len(), 1);
        // Concatenation preserves the merged-in slot value.
        assert_eq!(left.slots[0].new, U256::from(5));
    }

    #[test]
    fn slot_delta_add_applies_saturating() {
        assert_eq!(
            SlotDelta::Add(U256::from(50)).apply(U256::from(100)),
            U256::from(150)
        );
        // Saturates at U256::MAX rather than wrapping.
        assert_eq!(
            SlotDelta::Add(U256::from(10)).apply(U256::MAX - U256::from(1)),
            U256::MAX
        );
        assert_eq!(SlotDelta::Add(U256::from(5)).apply(U256::MAX), U256::MAX);
    }

    #[test]
    fn slot_delta_sub_applies_saturating() {
        assert_eq!(
            SlotDelta::Sub(U256::from(30)).apply(U256::from(100)),
            U256::from(70)
        );
        // Saturates at zero rather than underflowing.
        assert_eq!(
            SlotDelta::Sub(U256::from(50)).apply(U256::from(30)),
            U256::ZERO
        );
        assert_eq!(SlotDelta::Sub(U256::from(1)).apply(U256::ZERO), U256::ZERO);
    }

    #[test]
    fn state_update_slot_delta_constructor() {
        let a = addr(0xcc);
        assert_eq!(
            StateUpdate::slot_delta(a, U256::from(1), SlotDelta::Add(U256::from(2))),
            StateUpdate::SlotDelta {
                address: a,
                slot: U256::from(1),
                delta: SlotDelta::Add(U256::from(2)),
            }
        );
    }

    #[test]
    fn state_diff_merge_extends_skipped_without_counting_it() {
        let a = addr(0xdd);
        let mut left = StateDiff::default();
        let mut right = StateDiff::default();
        right.skipped.push(SkippedDelta {
            address: a,
            slot: U256::from(1),
            delta: SlotDelta::Sub(U256::from(3)),
        });

        left.merge(right);
        assert_eq!(left.skipped.len(), 1);
        // A skip is metadata, not a change.
        assert!(left.is_empty());
        assert_eq!(left.len(), 0);
    }

    #[test]
    fn slot_masked_constructor_produces_variant() {
        let a = addr(0xee);
        assert_eq!(
            StateUpdate::slot_masked(a, U256::from(1), U256::from(0xFF), U256::from(0x42)),
            StateUpdate::SlotMasked {
                address: a,
                slot: U256::from(1),
                mask: U256::from(0xFF),
                value: U256::from(0x42),
            }
        );
    }

    #[test]
    fn state_diff_merge_extends_skipped_masks_without_counting_it() {
        let a = addr(0xef);
        let mut left = StateDiff::default();
        let mut right = StateDiff::default();
        right.skipped_masks.push(SkippedMask {
            address: a,
            slot: U256::from(1),
            mask: U256::from(0xFF),
            value: U256::from(0x42),
        });

        left.merge(right);
        assert_eq!(left.skipped_masks.len(), 1);
        // A masked skip is metadata, not a change.
        assert!(left.is_empty());
        assert_eq!(left.len(), 0);
        // But it is discoverable through the skip accessors.
        assert!(left.has_skipped());
        assert_eq!(left.skipped_len(), 1);
        assert!(!left.is_fully_applied());
    }

    #[test]
    fn slot_masked_serde_round_trips() {
        let a = addr(0xf0);
        let update = StateUpdate::slot_masked(a, U256::from(5), U256::from(0xFF), U256::from(3));
        let json = serde_json::to_string(&update).expect("serialize");
        let back: StateUpdate = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(update, back);

        let mask = SkippedMask {
            address: a,
            slot: U256::from(1),
            mask: U256::from(0xFF),
            value: U256::from(2),
        };
        let json = serde_json::to_string(&mask).expect("serialize");
        let back: SkippedMask = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(mask, back);
    }
}