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
# Changelog

All notable changes to `evm-fork-cache` are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

**Pre-1.0 policy:** until `1.0.0`, breaking changes may land in **minor**
versions (`0.x.0`); patch versions (`0.x.y`) are non-breaking. The roadmap in
[`docs/ROADMAP.md`](docs/ROADMAP.md) deliberately reshapes the API before the
surface freezes at 1.0.

## [Unreleased]

Remaining from the 0.2.0 plan, tracked as fast-follow: the docs/examples
polish remainder (a toy constant-product AMM example and the end-to-end
reactive integration tests enumerated in
[`docs/KNOWN_ISSUES.md`](docs/KNOWN_ISSUES.md)).

## [0.2.1] - 2026-07-06

Trace-based discovery of hash-derived storage slots: derive a mapping's base
slot and byte-order **layout** from a single simulated call, instead of
brute-force probing. Works across Solidity (`keccak(key‖slot)`), Vyper
(`keccak(slot‖key)`), Solady/assembly packed layouts, and nested mappings
(e.g. allowances). All additions are backward compatible.

### Added

- New [`mapping_probe`]src/mapping_probe.rs module: `HashStorageProbe` (a
  composable `revm::Inspector` recording `KECCAK256` preimages and hashed
  `SLOAD`s), `HashSlotAccess` (keys, base slot, exact slot, layout, value,
  confidence), `SlotLayout`, `Confidence`, and `TrackedMapping` — a reusable
  descriptor whose `slot_for(key)` recomputes any entry's slot from the
  discovered layout. Re-exported at the crate root.
- `EvmCache::call_raw_with_inspector` — cache-level, lazily-fetching,
  non-committing inspector run (the counterpart to
  `EvmOverlay::call_raw_with_inspector`).
- `EvmCache::trace_hashed_slots` — general, ERC-20-agnostic slot discovery for
  any call; `EvmCache::discover_erc20_balance_slot` — balance-slot convenience;
  `EvmCache::write_mapping_entry` — layout-aware write (correct for Vyper/Solady,
  unlike `insert_mapping_storage_slot`).
- `EvmCache::track_erc20_balances` — "discover once, then track these
  addresses": returns a `TrackedMapping` plus each holder's exact slot to pin
  into a `FreshnessRegistry` / `PrefetchRegistry`.
- `EvmCache::seed_erc20_balance_layouts` and
  `Erc20TransferDecoder::with_tracked` — seed/configure a known non-Solidity
  layout up front.
- `EvmCache::set_erc20_allowance` — forge an ERC-20 allowance by discovering the
  nested `allowance` mapping entry for `(owner, spender)` and writing/verifying
  it. The approval counterpart to the balance setter, newly feasible via
  nested-mapping discovery (the old scan could not reach nested slots).
- **Overlay-scoped mocking**`EvmCache::mock_overlay` returns a throwaway
  [`EvmOverlay`] wired to the cache backend for lazy fetch carrying
  `mock_balance`, `mock_allowance`, `mock_call`, and `mock_view`. Each discovers
  the driving slot, writes it to the overlay's dirty layer, and verifies — so
  mocks live only for that overlay and **never persist to the cache** (no leak
  into later simulations). `mock_call`/`mock_view` generalize to any single-slot
  getter (balances, allowances, `totalSupply`, …) via
  `HashStorageProbe::slots_returning` (which now records plain SLOADs too).
  Zero-address balance/owner writes are refused.
- `EvmOverlay::call_sol` / `call_sol_from` — non-committing typed Solidity calls
  on the overlay (the counterpart to `EvmCache::call_sol`), so simulated reads
  decode natively: `overlay.call_sol(token, IErc20::balanceOfCall { account })?`
  returns the `U256` directly instead of decoding raw `call_raw` output. Adds
  `OverlayError::SolCallFailed` / `SolCallDecode`.
- `bulk_call_storage_fetcher_with_status` + `BulkFetcherStatus` — build a bulk
  storage fetcher together with a cheap, cloneable handle that reports whether it
  has latched to its point-read fallback (`fallback_latched()`) and the running
  provider-failure streak (`consecutive_override_failures()`). Makes silent
  bulk→point-read degradation — e.g. a provider that stops honoring `eth_call`
  state overrides — observable and alertable. Purely additive: the existing
  `bulk_call_storage_fetcher` / `bulk_call_storage_fetcher_with_fallback`
  constructors are unchanged.

[`EvmOverlay`]: https://docs.rs/evm-fork-cache/latest/evm_fork_cache/cache/struct.EvmOverlay.html

### Changed

- `set_erc20_balance_with_slot_scan` is now **discover-first**: a single
  layout-aware simulation replaces the `0..=max_slot` probe loop, with the
  brute-force scan kept as a fallback. This fixes silent failures on Vyper and
  Solady tokens, whose slots the Solidity-order scan could never match. The
  public signature is unchanged; every path still verifies the write.
  **Behavior note:** `max_slot` now bounds only the fallback scan — discovery is
  unbounded, so a token whose balance slot exceeds `max_slot` (or uses a
  namespaced/large slot) is now found where it previously returned `false`.
- `Erc20TransferDecoder` balance-slot computation is now layout-aware (via
  `with_tracked`); `new`/`with_token` remain Solidity-order and behave
  identically.

### Documentation

- Corrected the `ConfirmedFull` freshness story. The README safety checklist and
  `docs/KNOWN_ISSUES.md` previously implied that opting an account into
  `TrackingPolicy::WholeAccount`/`Scalars` upgrades a freshness verdict to
  `Validation::ConfirmedFull`. It does not: account-field tracking lives in the
  reactive runtime's root gate and keeps the **cache** fresh, while the
  speculative freshness validator still emits only `ConfirmedStorage`.
  `ConfirmedFull` (and `Corrected.changed_accounts`) remain defined-but-unemitted
  pending validator-side account verification — matching what `freshness`'s own
  rustdoc already stated.
- Corrected the crate-level "Requirements" rustdoc: fetch paths on a
  current-thread runtime return a typed `RuntimeError::CurrentThreadRuntime`, they
  do not panic (the `block_in_place` guard has done this since it was introduced;
  the doc was stale).
- Clarified the deep-reorg wording in the README safety checklist to match
  `docs/KNOWN_ISSUES.md`: reorgs beyond `ReactiveConfig::journal_depth` are not
  auto-purged; they escalate health to `Unhealthy` and rely on freshness
  validation as the backstop (resync before trusting sims).

### Internal

- The `test_save_load_round_trip` (and sibling `corrupt`/`legacy`/`write_error`)
  persistence tests in `binary_state`, `slot_observations`, and
  `prefetch_registry` now write to pid-keyed unique temp dirs (matching the idiom
  already used in `bytecode`/`code_seeds`), so overlapping `cargo test`
  invocations can't clobber each other's fixtures. Test-only; no effect on the
  shipped crate.

## [0.2.0] - 2026-07-05

Closes the top publication-review gaps (account-level *cache* freshness via the
reactive root gate, strict block-context, bounded `derived_slots`, deep-reorg
health escalation) and lands the full Phase-8 liveness plan (spec steps 1–6). Breaking changes are
grouped under **Changed**; they are permitted pre-1.0 (see the policy above).

### Performance

The round's RPC-economics wins (each documented with numbers and a reproduction
path; see the README "Performance & honest trade-offs" section for the full,
hedged picture):

- **Bulk `eth_call` storage extraction is now the default loader.** One
  state-override call reads thousands of slots at a flat ~26 CU instead of one
  point read each — e.g. 10,000 slots in 1 call vs 200,000 CU, and a full
  Uniswap V3 tick range (7,674 slots) in 2 calls / 52 CU (**~2,952× cheaper**),
  with automatic point-read fallback. Methodology + tables:
  [`docs/bulk-storage-extraction.md`]docs/bulk-storage-extraction.md;
  reproduce with `RPC_URL=… cargo run --release --example bulk_storage_bench`.
- **Verified code-seed cold starts.** Materializing a known contract set's code
  and balances is one bulk call settled against on-chain `EXTCODEHASH` rather
  than three reads per account: **~46× cheaper, ~25.6× faster** for 20 contracts
  (48 ms vs ~1.2 s, 26 CU vs 1,200). Same doc + bench (scenario 11).
- **Proof economics.** `eth_getProof` (no bulk substitute) is kept off the
  per-block path: root-gate probes fire on a cadence (default every 16 blocks →
  **16× fewer probes** by construction) and batch every gated account into one
  bounded fan-out (**≈4.7–7.3× over serial** across runs, live-measured for a
  50-account sweep — bounded by the concurrency cap, larger when per-proof
  latency is higher; reproduce with `E2E_RPC_URL=… cargo test --test
  liveness_root_gate -- --ignored`).

### Added

- **Mid-lifecycle adapter register/unregister (`ReactiveEngine`).** A new
  `ReactiveEngine` binds a `ReactiveRuntime` to an `EventSubscriber` and drives
  handler lifecycle as one operation, for consumers that add and drop adapters
  while the cache runs (e.g. register an AMM on a `PoolCreated` event, drop it
  later). `register_handler` updates runtime routing and subscriber interests
  together and, once ingestion has journaled a canonical block, backfills the
  new handler from that block automatically — closing the discovery→subscription
  gap with no caller bookkeeping (`register_handler_with_backfill` for deeper
  history, `register_handler_live_only` to opt out; `sync_handler_interests`
  bootstraps a pre-populated runtime). `unregister_handler` removes routing and
  transport for that handler only; the runtime adds `last_canonical_block()`,
  `pending_resyncs()`, `cancel_pending_resyncs(address)`, `handler_ids()`,
  `contains_handler`, `handler_interests`, and `unregister_handler` for the full
  teardown recipe (unregister + `untrack_account` + `cancel_pending_resyncs`;
  cache eviction stays explicit). Under the hood the new
  `InterestOwnerSubscriber` trait lets a subscriber add/remove interests keyed by
  a stable per-adapter `HandlerId` without disturbing unrelated owners; growing
  an owner's filter set carries its delivery anchor across the change and
  self-heals the gap, identical filters across owners share one subscription, and
  retired filters release their bookkeeping. `SubscriberBackfill` describes the
  anchor for owner-scoped `get_logs` backfill (`ReactiveEngineError` /
  `ReactiveEngineRegisterError` are the engine's typed errors). All existing
  runtime/subscriber APIs are unchanged; `register_interests` remains the
  full-replacement setup path.
- **Queryable cache health + metrics.** `ReactiveRuntime::health() -> CacheHealth`
  (`Healthy` / `Degraded` / `Unhealthy`) and `metrics() -> CacheMetricsSnapshot`
  (atomic counters: `deep_reorgs`, `reorgs_recovered`, `resync_requests`/`_failures`,
  `missed_ranges`, `coverage_gaps`, `pending_contamination`). `ReactiveReport::Health`
  transition reports and a caller-invoked `reset_health()`.
- **Conservative deep-reorg self-heal + missed-range detection.** A forward gap in
  the canonical block sequence (block *N**N+k*) is no longer silently accepted:
  it emits `ReactiveReport::MissedBlockRange` (+ `ResyncReason::MissedBlockRange`)
  and escalates health, while still applying the arriving block. Repeated trust-loss
  events (deep reorg beyond the journal, or a missed range) escalate
  `Healthy → Degraded → Unhealthy` (a "stop until rebuilt" signal).
- **Account/root fetcher seam.** `EvmCache::account_proof_fetcher` /
  `set_account_proof_fetcher` (`AccountProofFetchFn` over `eth_getProof`, returning
  `AccountProof`); `ResyncTarget::Account` resyncs now resolve through it
  (materializing so cold accounts are not silently skipped).
- **Bulk storage extraction (`bulk_storage` module) — now the default storage
  loader.** Every provider-backed cache ships a `StorageBatchFetchFn` that
  loads thousands of slots per `eth_call` by overriding target code with
  [Dedaub's 23-byte extractor]https://dedaub.com/blog/bulk-storage-extraction/
  (storage survives a code override), plus a Multicall3-dispatch path that
  spans many contracts in one call — with the classic point-read fetcher as
  automatic fallback (tiny requests, providers without override support —
  which also latch the fetcher to point reads after consecutive failures —
  and precompile targets). Public surface:
  `bulk_call_storage_fetcher[_with_fallback]`, the async core
  `fetch_slots_bulk`, `BulkCallConfig` (chunking, concurrency,
  `CallDispatch::CallMany` for Erigon-lineage endpoints where one 20-CU
  `eth_callMany` request carries the whole batch), `planned_call_count`,
  `StorageFetchStrategy` + `EvmCacheBuilder::{storage_fetch_strategy,
  bulk_call_config}` (opt-out / tuning), `point_read_storage_fetcher` (the
  extracted classic path), and both Shanghai (`PUSH0`) and pre-Shanghai
  extractor variants — all executed against revm in the offline test suite.
  Measured on Alchemy: 10,000 slots in one 26-CU call (vs 200,000 CU as point
  reads); a full Uniswap V3 pool tick range — 7,674 slots — in 2 calls /
  52 CU; 100 contracts × 30 slots in one call
  ([benchmarks + limitations]docs/bulk-storage-extraction.md).
  `StorageFetchError` and `RuntimeError` now derive `Clone` so one
  chunk-level failure can be reported per affected slot.
- **Custom storage programs + companion extractors + prewarm.**
  `StorageProgram` / `run_storage_program[s]` inject caller-supplied bytecode
  via the same override transport, enabling data-*dependent* extraction
  derived in-EVM (a worked one-shot Uniswap V3 observation-ring loader — zero
  calldata — ships in the benchmark example and revm tests);
  `fetch_account_fields_bulk` (`BALANCE` + `EXTCODEHASH` for many accounts in
  one call) and `fetch_block_context` (seven env words in one call) ride the
  same mechanism; `EvmCache::prewarm_slots` bulk-loads a declared working set
  into layer 2 through the installed fetcher, returning a `PrewarmReport`.
- **Verified code seeding & local etch.** Adapters can push runtime bytecode
  into the cache instead of paying an `eth_getCode` (plus the lazy backend's
  balance/nonce round trips) per address, with per-address trust marks
  (`CodeSeedState`; absence of a mark = RPC-origin) persisted to
  `code_seeds.bin` — saved *before* `bytecodes.bin` and pruned on load, so an
  unverified claim can never masquerade as chain-fetched across restarts.
  `seed_account_code[_with]` records a *canonical claim* (`Pending`);
  `verify_code_seeds()` settles the whole pending set against on-chain
  `EXTCODEHASH` in **one** `eth_call` through the new `AccountFieldsFetchFn`
  seam (default-wired to `fetch_account_fields_bulk`), marking matches
  `Verified` durably (real balance patched from the same response) and
  purging contradicted claims for refetch — `CodeVerifyReport` buckets
  `mismatched` / `not_deployed` / `codeless` / `unverifiable` (the last is
  fail-safe: transport failures keep seeds `Pending`). Chain-fetched code
  beats templates: an equal-hash seed over RPC-origin code verifies
  instantly with zero RPC; a conflicting one errors
  (`CacheError::CodeSeedConflict`) without touching the cache.
  `etch_account_code` is the raw-bytes *deliberate divergence* sibling
  (`Etched`, never verified); `override_account_code*` and `deploy_contract`
  targets now also record `Etched`, making `etched_accounts()` the single
  local-divergence health surface. The cold-start driver gained a
  `verify_code` phase that runs **first** — no discover sim executes over an
  unverified claim — recording `ColdStartResults.code_verifications` and
  guarded by `ColdStartError::NoAccountFieldsFetcher` for pending-bearing
  rounds. Spec: `docs/verified-code-seeding-spec.md`.
- **Strict block context + engine-driven env refresh.** `BlockContextRequirements`
  (`strict`/`lenient`/per-field) + `BlockContextError`;
  `EvmCacheBuilder::strict_block_context` / `block_context_requirements` and a
  fallible `try_build` (`EvmCache::new` stays infallible + lenient);
  `EvmCache::advance_block(header)` refreshes the full block env
  (number/basefee/coinbase/prevrandao/gas-limit/timestamp) with the same strict
  validation, driven from the reactive canonical-header path; new
  `coinbase()` / `prevrandao()` / `block_gas_limit()` getters.
- **`Validity` stamping (opt-in).** `ReactiveRuntime::enable_freshness_stamping()`
  stamps canonical event-derived writes `Validity::ValidThrough(N)` so
  event-maintained slots stop being needlessly re-verified.
- **storageHash root gate + `TrackingPolicy` + `RootGateCadence`.**
  `TrackingPolicy` (`Slots` / `WholeAccount` / `Scalars`) + `track_account`;
  the root gate emits `ReactiveReport::CoverageGap` (+
  `ResyncReason::RootMoved`) when a tracked account's storage root moves with
  no covering decoder, and `Scalars` tracks native balance/nonce/code (which
  do not move the storage root). The gate fires per `RootGateCadence`
  (`set_root_gate_cadence`), **default every 16 canonical blocks, never
  per-block**: `eth_getProof` is the slowest read the crate issues, and the
  gate diffs against its persisted baseline (never block-over-block), so
  skipping blocks trades bounded detection lag for a 16× probe-cost cut
  without losing detection. The decoder-touched set accumulates across
  skipped blocks (drained per firing) so covered writes never false-positive
  as gaps; `every_n_blocks(1)` restores per-block probing and `Disabled`
  turns the gate off. All root-gate and account-resync probes now go through
  **one seam invocation per firing** (grouped by resync block), and the
  default proof fetcher fans them out with bounded, order-preserving
  concurrency — `EvmCacheBuilder::max_concurrent_proofs` (default 8) — so a
  fleet probe costs ~`ceil(N / cap)` round trips instead of `N`.
- **Cold-start root baseline (`roots.bin`).** `RootBaseline` persists each
  tracked account's `storageHash` alongside the state file (versioned binary,
  magic `EFCROOT`); on restart, `RootBaselinePlanner` root-probes the baseline
  via `eth_getProof` and converts moved roots into targeted resyncs instead of
  trusting stale disk state (restart-drift detection, Phase-8 step 5).
- **Tier-3 trace-backed resync.** The reactive runtime resolves matching
  resync targets from one `debug_traceBlockByNumber` (`prestateTracer`,
  `diffMode`) block diff before falling back to storage/account point reads —
  `BlockStateDiffFetchFn` (+ `set_block_state_diff_fetcher`),
  `BlockStateDiff`/`BlockStateAccountDiff`/`BlockStateStorageDiff`, and
  `ReactiveRuntime::ingest_batch_with_resync` (Phase-8 step 6). Measured
  economics/latency in
  [`docs/trace-resync-benchmarks.md`]docs/trace-resync-benchmarks.md.
- **Bundle cost accounting.** `BundleResult::successful_tx_gas` and
  `reverted_tx_gas` — net searcher cost (`coinbase_payment + reverted_tx_gas`) is now
  direct under `AllowReverts`.
- **Snapshot-consistency generation guard.** `EvmCache::snapshot_generation()`
  — an opaque monotonic counter bumped by targeted state writes
  (`apply_update`/`apply_updates`/`modify_slot` and everything built on them)
  and block re-pins (`set_block`/`advance_block`), but not by cold prefetch.
  Read it around `snapshot()` to detect (and retry) a snapshot taken
  mid-block during continuous ingestion, closing the ROADMAP-listed
  consistency gap (G6).

### Changed (breaking)

- **Freshness verdict taxonomy is honest about scope.** `Validation::Confirmed` is
  renamed `ConfirmedStorage` (it only ever meant "no volatile storage slot changed",
  not account-level state); a new `ConfirmedFull` variant is *defined* for storage
  **and** verified account fields (but not yet emitted — validator-side account
  verification is a tracked follow-up, so a storage-only success stays
  `ConfirmedStorage`); `Validation::Corrected`'s `changed` field is renamed
  `changed_slots` and gains `changed_accounts: Vec<AccountChange>` (also empty
  until that follow-up lands). Account-level freshness that *is* shipped lives in
  the reactive runtime's root gate (below), not the speculative validator.
- **`ResyncFailureKind::UnsupportedAccountTarget` removed**, replaced by
  `MissingAccountFetcher` / `AccountFetchFailed` / `AccountFetchOmitted` now that
  account resync is supported.
- **Growth-bound enums are now `#[non_exhaustive]`.** `ReactiveReport`,
  `ResyncReason`, `ResyncFailureKind`, `TrackingPolicy`, `CacheHealth`, and the
  `CacheMetricsSnapshot` struct are marked `#[non_exhaustive]` (matching the
  `StateUpdate`/`PurgeScope` precedent): downstream `match`es need a wildcard arm,
  and future variants/counters will no longer be breaking changes.
- **Snapshot API names are now role-based.** The low-level in-place rollback copy
  is `EvmCache::checkpoint()` and the immutable fan-out view is
  `EvmCache::snapshot() -> Arc<EvmSnapshot>`. The pre-0.2.0 `snapshot()` /
  `create_snapshot()` public names were removed rather than aliased; the hidden
  benchmark/reference helper is now `snapshot_deep_clone()`.
- **Storage batch tuning is per instance.** `StorageBatchConfig` exposes
  `slots_per_batch` and `max_concurrent_batches` for the provider-backed
  storage fetcher. `EvmCacheBuilder::storage_batch_config(...)` accepts exact
  values, `CacheSpeedMode` remains as presets via `From<CacheSpeedMode>`, and
  `EvmCacheBuilder::speed_mode(...)` is shorthand. The process-global
  `set_cache_speed_mode` / `cache_speed_mode` functions and static are removed.
- **`SpeculativeSim::validate()` returns `Result<Validation>`.** Validator-owned
  uncertainty still returns `Validation::Unverified`, while a failed background
  task (for example a panic) now surfaces as an error instead of being folded into
  a verdict or panicking on the handle.
- **Crate-owned fallible APIs now return typed errors instead of `anyhow`.**
  Public helpers expose domain errors such as `CacheError`, `StorageFetchError`,
  `RpcError`, `FreshnessError`, `DeployError`, `MulticallError`, and
  `AccessListError`; `SimError::Other` now carries `SimHostError`. `anyhow` is no
  longer a normal library dependency, though examples/tests may still use it as
  harness glue.

### Fixed

- **`EventPipeline::derived_slots` no longer grows unbounded.** It is now a
  block-horizon ring bounded to `ReorgConfig::depth` (mirroring the `touched` ring),
  so steady-state ingestion does not leak memory.
- **`BLOCKHASH`-reading sims fail closed in freshness validation.** Validator
  overlays carry no block hashes, so an in-lookback-range `BLOCKHASH` read
  resolves to ZERO; such sims were previously eligible for a silent
  `ConfirmedStorage`. The read is now recorded
  (`EvmOverlay::blockhash_zero_fallback`) and the verdict is
  `Validation::Unverified` — on the optimistic pass and on corrected re-runs.
  Out-of-lookback reads return the spec-mandated ZERO and are not flagged.
- **Block-diff traces now surface full account deletions.** A SELFDESTRUCTed
  account (present in the trace's `pre`, absent from `post`) gets explicit
  zeroed balance/nonce/code in `BlockStateDiff`, so account-target resyncs
  resolve from the trace instead of falling back to point reads.

## [0.1.0] - 2026-06-30

This is the first release line. It captures the work done across the
pre-release development phases (see [`docs/ROADMAP.md`](docs/ROADMAP.md)).

### Added

- **Bundle simulation + coinbase accounting (Phase 6 Track A+B).** New
  `EvmOverlay::simulate_bundle` (and the cache-side convenience
  `EvmCache::simulate_bundle`, which snapshots internally and never mutates the
  cache) apply an ordered sequence of `Call`-kind transactions over **cumulative**
  block state on a single overlay — transaction `i` observes the committed writes
  of `0..i`. A `RevertPolicy` chooses between `Atomic` (any revert rolls the whole
  bundle back) and `AllowReverts(indices)` (whitelisted reverts roll back only
  their own transaction and execution continues). `BundleResult.coinbase_payment`
  reports the miner payment as the block beneficiary's balance delta — the honest
  priority fee plus any direct coinbase tips; revm already burns the base-fee
  portion in-EVM (EIP-1559), so no base-fee correction is applied. New public types
  live in the `bundle` module and are re-exported at the crate root: `BundleTx`,
  `BundleOptions`, `RevertPolicy`, `TxOutcome`, `BundleResult`.
- **`EvmCache::set_basefee(U256)`** installs a block base fee (the `BASEFEE`
  opcode) that propagates into the next `create_snapshot()`, so offline caches
  with no fetched header can exercise base-fee-aware (`Mainnet`) bundle accounting.
- Call-frame tracing (`tracing` module): `CallTracer`, a `revm::Inspector` that
  reconstructs the call-frame tree (`CallTrace`) of a simulation — top-level call
  plus nested `CALL`/`STATICCALL`/`DELEGATECALL`/`CALLCODE` and `CREATE`/`CREATE2`
  frames, each with from/to/value/input/gas/output/`CallStatus`/depth/subcalls.
  Implemented via the `call`/`call_end`/`create`/`create_end` hooks (no
  opcode/step or `SLOAD`/`SSTORE` tracing). Re-exported at the crate root as
  `CallTracer`, `CallTrace`, `CallKind`, `CallStatus`.
- `InspectorStack<A, B>`, a composing `revm::Inspector` that fans out every hook
  to two inner inspectors so, e.g., a `CallTracer` and a `TransferInspector`
  capture independently in one pass. Re-exported at the crate root.
- `EvmOverlay::call_raw_with_inspector`: a public, inspector-generic single-call
  seam that attaches any `revm::Inspector` (honoring `TxConfig` and `commit`) and
  returns the raw `ExecutionResult` (a revert/halt is `Ok`, not `Err`) alongside
  the inspector for the caller to read.

### Changed

- **Breaking:** the in-memory chain ID is no longer hard-defaulted to Arbitrum
  (`42161`). When no chain ID is set explicitly and no disk `CacheConfig` is
  supplied, the cache now infers it from the provider (`eth_chainId`) and falls
  back to `1` (Ethereum mainnet) only if that query fails. Set it explicitly with
  the new `EvmCacheBuilder::chain_id` / `EvmCache::set_chain_id`.
- **Breaking:** extracted the old in-crate AMM adapter surface before public
  release. Protocol-specific storage layouts, protocol metadata, injector
  helpers, tick snapshots, and protocol log decoders belong in `evm-amm-state`;
  this crate now exposes only the generic fork cache, simulation, freshness,
  state-update, ERC-20 decoding, and event-pipeline primitives.
- **Breaking:** `ImmutableDataCache` is now token-decimals-only and its on-disk
  format version is bumped to `2`, so older metadata files with protocol payloads
  are treated as stale cache misses.
- **Breaking:** renamed the remaining generic storage-purge helpers from
  pool-oriented names to contract-oriented names:
  `has_contract_storage`, `contract_storage_slot_count`,
  `purge_contract_storage`, and `purge_contract_slots`. The old pool-named
  aliases were removed rather than deprecated before the first public release.

### Added

- **Honest performance section & benchmarks.** The README "Performance" section
  is framed against a *competent* baseline (a shared `foundry-fork-db`
  `SharedBackend` + `checkpoint`/`revert` isolation), not a naive
  fork-per-candidate strawman. It states plainly that within-block fetch count,
  single-threaded CPU, and time-to-result are ~1× against that baseline, and
  leads with the genuinely-unique wins: cross-block freshness (0 RPC fetches/block,
  pinned in `tests/event_pipeline.rs``foundry-fork-db`'s cache is not
  block-keyed), parallel `Send` fan-out (`benches/fanout.rs`, a modest measured
  ~1.2× on micro-sims), point-in-time consistency, and the act-then-validate
  control plane (`benches/freshness.rs`). Adds `examples/fetch_minimization_counted.rs`
  and `tests/fetch_minimization.rs` (the fetch-once mechanic, with the
  shared-backend caveat stated). The internal copy-on-write snapshot cost model
  moved to [`docs/INTERNALS.md`]docs/INTERNALS.md.
- **Forked EVM cache** (`cache::EvmCache`) backed by `foundry-fork-db` with lazy
  RPC loading and on-disk persistence for accounts, storage, bytecode, and
  immutable metadata.
- **`EvmCacheBuilder`** — a fluent constructor (`EvmCache::builder(provider)`)
  subsuming the positional `with_cache` / `from_backend` constructors, with
  block pin, EVM spec, cache-config, chain-ID, and shared-memory-capacity
  configuration. `EvmCacheBuilder::chain_id` sets the `CHAINID` opcode value
  explicitly (recommended); `EvmCache::set_chain_id` sets it post-construction.
- **Snapshots and overlays**`create_snapshot()` produces an immutable,
  `Send + Sync` `EvmSnapshot`; `EvmOverlay` is a cheap per-simulation clone for
  isolated parallel evaluation.
- **Freshness control plane** (`freshness` module, Phase 2) — the four-layer
  model (`Validity`/`FreshnessRegistry`, `SlotObservationTracker`,
  `FreshnessPolicy`, `FreshnessController`), a configurable `FreshnessClock`
  (`BlockClock`/`WallClock`), and the optimistic verify-and-rerun execution loop
  with deferred validation (`SpeculativeSim`/`Validation`).
- **Freshness primitives on `EvmCache`**`verify_slots`, `purge_account`,
  `set_storage_batch_fetcher`; `EvmOverlay::call_raw_with_access_list` and
  `override_slot` for read-set capture and corrected re-runs.
- **State-update vocabulary & apply primitive** (`state_update` module, Phase 3,
  Pillar B.1) — a generic `StateUpdate` enum (`Slot` / partial-`AccountPatch`
  `Account` / `Purge` by `PurgeScope`) plus `EvmCache::apply_update` /
  `apply_updates`, the single dual-layer write-through primitive (backend always,
  overlay-if-present, no new overlay account materialized), returning a structured
  `StateDiff` (`SlotChange`s, `AccountChange`s, `PurgeRecord`s) that records only
  actual changes. The existing `inject_storage_batch_fresh` / `purge_account` /
  `purge_contract_storage` / `purge_contract_slots` writers and the freshness
  correction-drain are refolded onto it (signatures unchanged).
- **Relative / read-modify-write state updates** (`state_update`, Phase 3 §15) —
  a saturating `SlotDelta` (`Add`/`Sub`, clamping at `U256::MAX`/`U256::ZERO`), a
  `StateUpdate::SlotDelta { address, slot, delta }` variant (with the
  `StateUpdate::slot_delta` constructor) so deltas flow through `apply_updates`,
  and `EvmCache::modify_slot(address, slot, |Option<U256>| -> Option<U256>)` as
  the general closure escape hatch. Relative application is **cold-aware**: a
  delta against a slot absent from both layers is not applied (it would corrupt an
  unknown value) but surfaced in the new `StateDiff.skipped: Vec<SkippedDelta>`
  field for the caller to fetch+seed and retry. `skipped` is informational
  metadata and does not affect `StateDiff::is_empty` / `len` (changes-only).
  Adding the `StateDiff.skipped` field is a struct change permitted under the
  pre-1.0 break policy. Generic core.
- **Post-audit state-update remediation** (`state_update`, Phase 3 §16):
  - **`serde`**`Serialize`/`Deserialize` derived (unconditionally) on the whole
    vocabulary (`SlotDelta`, `StateUpdate`, `AccountPatch`, `PurgeScope`) and the
    diff (`StateDiff`, `AccountChange`, `PurgeRecord`, `SkippedDelta`,
    `SkippedBalanceDelta`) plus `freshness::SlotChange`, so updates can be shipped
    over the wire and diffs persisted.
  - **`#[non_exhaustive]`** on `StateDiff` and `AccountPatch` (both
    `Default`/builder-constructed), so future field additions are non-breaking. The
    leaf record types (`SlotChange`/`AccountChange`/`PurgeRecord`/`SkippedDelta`/
    `SkippedBalanceDelta`) are deliberately left exhaustive — they are routinely
    built as struct literals in equality assertions.
  - **Relative native-balance updates** — a `StateUpdate::BalanceDelta { address,
    delta: SlotDelta }` variant (with `StateUpdate::balance_delta`), the
    `EvmCache::modify_account_balance(addr, |Option<U256>| -> Option<U256>)`
    closure escape hatch, a new `StateDiff.skipped_balances:
    Vec<SkippedBalanceDelta>` field, and `SkippedBalanceDelta`. Cold-aware: a delta
    on an account absent from both layers is skipped and surfaced (never
    materialized). Adding `skipped_balances` is a struct change permitted under the
    pre-1.0 break policy.
  - **Discoverable skip accessors**`StateDiff::has_skipped()` / `skipped_len()`
    / `is_fully_applied()`, counting **both** `skipped` and `skipped_balances`, so a
    silently-dropped cold relative update is easy to detect (the changes-only
    `is_empty()`/`len()` do not reflect skips).
  - **Constructor symmetry**`StateUpdate::nonce(addr, u64)`,
    `StateUpdate::code(addr, Bytes)`, `StateUpdate::account(addr, AccountPatch)`.
- **Batched single-lock fast-path for `apply_updates`** (Phase 3 §16.9): a run of
  consecutive `Slot`/`SlotDelta` writes now holds the backend storage write-guard
  once for the run (the guard is dropped before any `Account`/`BalanceDelta`/
  `Purge` update to avoid deadlocking the non-reentrant `RwLock`, then re-acquired),
  and the `SlotDelta` double-read of the old value is eliminated. The result is
  byte-identical to folding `apply_update` over the batch (pinned by the
  batched==sequential equivalence test). Generic core.
- **Event → state pipeline** (`events` module, Phase 4, Pillar B.2 — the *reader
  half* of the event pipeline) — turn on-chain logs into the Phase 3 `StateUpdate`
  vocabulary and drive them through the cache for reactive freshness:
  - **`EventDecoder` / `StateView`** — a decoder is a pure function of
    `(log, pre-state)` returning `Vec<StateUpdate>`; the narrow read-only
    `StateView` (implemented by `EvmCache` via `cached_storage_value`) lets
    stateful adapters read current cached state without RPC. Generic core.
  - **`DecoderRegistry`** — dispatches a log to the decoders registered for its
    emitting address (plus globals) and concatenates their output. Generic core.
  - **`Erc20TransferDecoder`** — decodes ERC-20 `Transfer` logs into relative
    balance `SlotDelta`s (skipping the zero-address mint/burn leg), with per-token
    balance-slot config. The reactive-balance case from Phase 3 §15, now
    log-driven. Generic core.
  - **`EventPipeline`**`ingest_logs` decodes + applies a block's logs
    **log-by-log in order** (so a later log sees earlier applies) and returns a
    `BlockDigest`; `reorg_to` purges (purge-and-resync) the addresses touched
    after a new head; `reconcile` re-reads sampled event-derived slots against
    chain truth (correct **and** alarm) via the new `EvmCache::reconcile_slots`. A
    thin async `drive`/`LogSource` convenience layers the synchronous core over a
    stream. Generic core.
- **Reactive runtime** (`reactive` module, default-enabled) — a provider-neutral
  handler pipeline for logs, block notifications, and pending transaction
  signals. `ReactiveHandler`s are pure synchronous functions over
  `ReactiveInput` + `ReactiveContext` + `StateView`; they emit `StateUpdate`s,
  invalidations, resync requests, speculative requests, and hook signals. The
  runtime deduplicates inputs by `InputRef`, orders canonical logs by
  `(block_number, transaction_index, log_index)`, routes by `ReactiveInterest`
  with Alloy `Filter`s and local matchers, validates pending inputs so they
  cannot mutate canonical cache state, detects conflicting absolute writes for a
  single input, applies canonical mutations through `EvmCache::apply_updates`,
  and dispatches `ReactiveReport`s to hooks after mutation phases.
  `ReactiveRegistry` exposes consolidated Alloy log filters for provider
  subscription setup and exact local log routing with optional route keys.
  Includes a provider-agnostic `EventSubscriber` trait, an `AlloySubscriber`
  that drives default WebSocket/pubsub Alloy `subscribe_logs`,
  `subscribe_blocks`, and `subscribe_pending_transactions` streams for live log,
  block-header, and pending transaction hash inputs, with automatic
  source-specific reconnect that retries immediately first, attempts three
  times by default, and performs log gap backfill from the last seen block when
  a pubsub stream terminates, plus an adapter from legacy `EventDecoder`s to
  reactive handlers. HTTP polling `watch_logs` / `watch_pending_transactions`
  support is still exported behind the opt-in `reactive-polling` feature. Full
  block bodies and full pending transaction hydration remain follow-up transport
  work; owner-scoped log backfill can fetch from an explicit block anchor when
  adding new interests. Generic core.
- **Reactive storage resync execution**`ReactiveRuntime::ingest_batch_with_resync`
  preserves the direct-effect behavior of `ingest_batch`, then executes surfaced
  storage resync requests through `EvmCache`'s provider-neutral
  `StorageBatchFetchFn`, applies successful values as `StateUpdate::slot`
  updates, and reports requested targets, applied updates, the resulting
  `StateDiff`, and per-target failures in `ResyncReport`. `ResyncFailureKind`
  gives downstream retry policy and metrics a stable failure classification.
  Account targets resolve through the provider-neutral account proof fetcher;
  missing, failed, or omitted account fetches surface as typed failures. Generic
  core.
- **Reactive block journaling and reorg recovery** — canonical block inputs and
  applied handler reports are retained in a depth-bounded runtime journal.
  Removed logs, explicit `ChainStatus::Reorged` inputs, and parent-hash
  discontinuities now emit `ReactiveReport::Reorg` with dropped blocks, dropped
  inputs, rollback updates/diffs, purge updates/diffs, and canceled hash-pinned
  resync requests. Reversible storage-slot changes are rolled back in reverse
  apply order; account/code changes and prior purge effects conservatively fall
  back to targeted purge updates because `StateDiff` does not carry enough data
  to reconstruct those cache entries exactly. Recovery is bounded to the
  configured `ReactiveConfig::journal_depth` (default 64): a reorg deeper than
  that — or any reorg when `journal_depth = 0` — recovers only the blocks still
  resident in the journal and emits a `tracing::warn!` for the under-recovered
  span (the freshness loop is the backstop; see `docs/KNOWN_ISSUES.md`).
  Demonstrated offline in `examples/reactive_runtime.rs`. Generic core.
- **Cold-start** (`cold_start` module, default-enabled / reactive-gated) —
  declarative warming of a working set of accounts and storage slots into the
  cache in one batched pass via `EvmCache::run_cold_start` /
  `execute_cold_start_round` and a `ColdStartPlanner` (discover slots through a
  view-call, then verify them through the `StorageBatchFetchFn`), returning a
  structured `ColdStartRunReport` (`ColdStartConfig`/`Plan`/`Results`/
  `RoundSummary`/`Step`/`RoundOutcome`/`Error`, with per-slot `SlotOutcome`s). The
  account-warming phase is serial (one fetch per declared account) and aborts the
  round on the first account failure; storage verify/probe are batched. Generic core.
- **`StateUpdate::SlotMasked`** (`state_update`, Phase 4) — a cold-aware
  read-modify-write *masked* slot write (`new = (old & !mask) | (value & mask)`)
  with the `StateUpdate::slot_masked` constructor, so a pure decoder can update
  selected bits of a **packed** storage word without clobbering the rest. A masked
  write to a cold slot is skipped and surfaced in the new
  `StateDiff.skipped_masks: Vec<SkippedMask>` (counted by `has_skipped` /
  `skipped_len`, not by the changes-only `is_empty`/`len`); `serde` on
  `SkippedMask`. Adding the variant and the field is permitted under the pre-1.0
  break policy (`StateUpdate`/`StateDiff` are `#[non_exhaustive]`). Generic core.
- **Configurable transaction & block environment**`TxConfig` (value, gas
  limit, gas price, nonce, access list) threaded through `call_raw_with`; block
  context setters (`set_coinbase`, `set_prevrandao`, `set_block_gas_limit`).
- **Transfer-inspector simulation** (`inspector`) reporting per-token balance
  deltas from the `Transfer` event stream.
- **Access-list tooling** (`access_list`, `access_set`) — `StorageAccessList`
  touch-set capture, EIP-2930 list construction, and L2 profitability estimation.
- **Multicall3 batching** (`multicall`).
- **Deployment & etching** (`deploy`) — deploy from creation code, etch Foundry
  artifacts over forked contracts; **CREATE3** address derivation (`create3`).
- **Extensible revert decoder** (`errors`) — native `Error(string)` / `Panic(uint256)`
  decoding plus one-line custom-error registration; typed `SimError`
  (`Revert` / `Halt` / `Host`).
- **Fallible custom-error registration** (`errors`) —
  `RevertDecoder::try_register`, `try_register_raw`, and
  `DuplicateSelectorError` let callers reject duplicate custom-error selectors
  during decoder setup instead of relying on the warning-only ergonomic path.
- **Two-stage prefetch registry** (`prefetch_registry`) for cross-cycle
  storage-slot pre-warming.
- **Copy-on-write snapshots** (Phase 5, Pillar A) — `create_snapshot` is now a
  two-tier copy-on-write view instead of an O(total state) deep clone. The cold
  `BlockchainDb` index (layer 2) is flattened once into an internal, immutable,
  `Arc`-shared base (`Arc` per account storage map, structural sharing — no new
  dependency, Decision D1), memoized across snapshots and rebuilt copy-on-write
  only for the addresses that changed; each `create_snapshot` then folds just the
  hot CacheDB delta (layer 1) over a cheap `Arc::clone` of that base. Reads stay
  O(1) and lock-free and are bit-for-bit identical to the deep clone (pinned by
  the `tests/cow_snapshot.rs` differential-equivalence gate). The retained
  `EvmCache::create_snapshot_deep_clone()` (`#[doc(hidden)] pub`, Decision D3) is
  the equivalence reference and the A/B benchmark baseline. `EvmSnapshot` stays
  `Send + Sync` and `EvmOverlay` stays `Send`.
- **`EvmOverlay::reset()`** (Phase 5, Pillar A.2) — recycle one overlay across
  many simulations against the same snapshot without reallocating: it clears the
  per-simulation dirty layer (keeping the snapshot `Arc`, `ext_db`, and the
  reusable shared-memory buffer), reading the pristine snapshot again and behaving
  exactly like a freshly-built overlay. The 64 KiB shared-memory buffer is also
  recycled across the build→transact→revert call methods (stored as a plain
  `Vec<u8>`, so the overlay stays `Send`).
- **Configurable EVM shared-memory pre-allocation**`SharedMemoryCapacity`
  (`Fixed(usize)` / `Auto`, default `Fixed(64 * 1024)` / 65,536 bytes) set via
  `EvmCacheBuilder::shared_memory_capacity`. `Fixed` pins the per-context working-
  memory buffer (general users running wide fan-outs of small simulations can lower
  it to cut per-overlay memory; the previous behavior is the default); `Auto` sizes
  it from the chain state loaded at build time (e.g. a bincode state file), clamped
  to a 64 KiB floor / 4 MiB ceiling. The resolved size is readable via
  `EvmCache::shared_memory_capacity()` and is propagated to every snapshot so
  snapshot-backed overlays pre-allocate the same amount. `with_cache_capacity` is
  the lower-level constructor behind the builder setter.
- **Explicit cold-account materialization**`StateUpdate::AccountUpsert` and
  `StateUpdate::account_upsert(...)` intentionally materialize an account absent
  from both layers. Normal `StateUpdate::Account` patches are now cold-aware and
  surface skipped cold patches through `StateDiff.skipped_accounts:
  Vec<SkippedAccountPatch>`.
- **Invalidating layer-2 mutation wrapper**`EvmCache::with_blockchain_db_mut`
  runs a synchronous direct `BlockchainDb` mutation and invalidates the Phase 5
  memoized COW base automatically after the closure returns.
- **Exact access-list RLP data-gas helper**  `access_list::access_list_rlp_data_gas(&AccessList)` returns the EIP-2930 RLP
  calldata gas for an access list and backs the L2 profitability calculation.
- **Versioned on-disk cache envelope** — binary EVM state, bytecode, and
  `ImmutableDataCache` files now start with crate-specific magic bytes plus a
  `u32` version before the bincode payload.
- **Public-release CI gates** — the GitHub Actions workflow now enforces format,
  clippy on all targets, all-target tests, doctests, warning-free docs, bench
  compilation, package verification, and the MSRV library check.

### Changed

- **Duplicate `RevertDecoder` registrations keep the first selector owner.**
  `register`, `register_raw`, and builder-style `with_error` no longer replace an
  existing custom-error decoder for the same 4-byte selector. They retain the
  original registration and emit a `tracing::warn!`; callers that want hard
  failure can use `try_register` / `try_register_raw`.
- **`EvmCache::create_snapshot` is now `&mut self`** (Phase 5, Decision D5) —
  taking a snapshot memoizes/refreshes the cold copy-on-write base, which requires
  a mutable borrow. All callers (the freshness controller, tests, examples,
  benches) are updated; the return type (`Arc<EvmSnapshot>`) is unchanged.
  Permitted under the pre-1.0 break policy.
- **`EvmCache::inject_storage_batch` is now `&mut self`** (Phase 5) — the
  layer-2 bulk write now marks the touched addresses dirty for the memoized
  copy-on-write base. The write itself is still a direct backend (layer-2) write
  with the same semantics; only the receiver mutability changed.
- **Raw layer-2 handles were renamed to unchecked accessors** (Phase 5) —
  `EvmCache::blockchain_db()` is now `unchecked_blockchain_db()` and
  `EvmCache::backend()` is now `unchecked_backend()`. The rename makes the
  bypass explicit; use `with_blockchain_db_mut` for synchronous direct writes that
  should automatically invalidate the snapshot base.
- **Persistence APIs now return `Result<()>`**`cache::save_binary_state`,
  `PrefetchRegistry::save`, and `EvmCache::flush` report serialization,
  directory-creation, and write failures to explicit callers. `Drop` remains
  best-effort and logs `flush()` errors.
- **Block re-pins clear stale context**`set_block` sets `block_number` only
  for concrete numeric pins, clears it for tag/hash/`None` pins, and clears stale
  `basefee` on block changes and on non-concrete pin calls that can drift under
  the same tag. `repin_to_block` follows the same no-stale-basefee rule; callers
  refresh `NUMBER`/`BASEFEE` via `set_block_context` after fetching the new
  header.
- **Legacy raw-bincode cache files are treated as misses** — the versioned cache
  envelope intentionally rejects unversioned `evm_state.bin`, `bytecodes.bin`,
  and `immutable_data.bin` payloads rather than trying to deserialize ambiguous
  layouts.
- Simulation entry points that distinguish failure modes return
  `SimulationResult<T>` (`Result<T, SimError>`), separating decoded reverts,
  EVM halts, and host errors.

### Fixed

- **Duplicate custom-error selectors no longer shadow silently.** A second
  registration for the same selector is now observable through
  `DuplicateSelectorError` on the fallible APIs, or through a warning on the
  ergonomic `register` / `register_raw` / `with_error` path. Decoding keeps using
  the first registered selector owner.
- **Cold absolute account patches no longer mask on-chain accounts.**
  `StateUpdate::Account` on an account absent from both layers now skips instead
  of writing `AccountInfo::default()` fields through the shared backend. The
  skipped patch is visible in `StateDiff.skipped_accounts`; intentional cold
  creation uses `StateUpdate::AccountUpsert`.
- **Access-list profitability no longer conflates provider failures with
  unprofitable lists.** `SmartAccessList::into_access_list_if_profitable` and
  `access_list_if_profitable` now propagate provider/pricing failures as `Err`
  and reserve `Ok(None)` for empty, zero-priced, or genuinely unprofitable lists.
- **`simulate_call_with_balance_deltas` now isolates balance reads and reports a
  real access list.** Pre/post `balanceOf` calls run outside the target-call
  checkpoint, so they cannot warm the target call or commit side effects. The
  method commits only the target call when requested and returns the deduplicated
  EIP-2930 accounts/slots from balance reads plus the target call.
- **`cached_storage_value` silent-corruption bug** (Phase 3 §16.0, audit HIGH +
  MED). For a storage slot absent from an overlay account whose revm
  `account_state` is `StorageCleared` or `NotExisting`, the accessor now returns
  `Some(U256::ZERO)` — mirroring what the live EVM `SLOAD`s
  (`CacheDB::storage_ref`) — instead of falling through to the BlockchainDb
  backend and returning a *shadowed* backend value the EVM never sees. The old
  behavior let a `SlotDelta` / `modify_slot` compute a relative update against a
  base the EVM never reads (silent state corruption) and mis-recorded
  `apply_slot`'s `SlotChange.old` / change predicate. This also closes the
  same-root mismatch shared by `verify_slots` / `inject_storage_batch_fresh`.
- **No-op `Account` patch no longer materializes a backend account** (Phase 3
  §16.1, audit LOW). `apply_account_patch` now computes the field change first and
  **skips both layer writes** (returning an empty diff) when no field actually
  changes, instead of unconditionally inserting `AccountInfo::default()` into the
  shared backend for an all-`None` (or value-unchanged) patch on an absent address.
  Phase 5 later tightened this further: real field changes on cold accounts now
  skip through `StateDiff.skipped_accounts` unless the caller uses
  `StateUpdate::AccountUpsert`.
- **`account_state`-awareness extended to the snapshot + account-info paths**
  (Phase 3 fix-review, HIGH + MED). A follow-up adversarial review found the §16.0
  `cached_storage_value` fix had not been propagated to two sibling read paths:
  - `create_snapshot` now mirrors the live read: a `StorageCleared`/`NotExisting`
    account's storage is captured as **only** its overlay slots (shadowed backend
    slots dropped) and recorded in a new `EvmSnapshot.storage_cleared` set, so
    `EvmSnapshot::storage_value` and snapshot-backed `EvmOverlay`s read such a
    slot as ZERO instead of the shadowed backend value (which also kept the
    background freshness validator's `old` consistent with `verify_slots`). The
    `EvmOverlay` storage read honors the set and does **not** fall through to its
    `ext_db` for a cleared account.
  - `loaded_account_info` now mirrors revm `DbAccount::info()`: a `NotExisting`
    overlay account is treated as absent (returns `None`), so a `BalanceDelta` /
    partial `Account` patch skips rather than computing against a stale `info`.
  - `write_account_info_through` normalizes a `ZERO` `code_hash` to `KECCAK_EMPTY`
    so both cache layers store an identical hash (matching revm's `insert_contract`).
- **`account_state`-awareness completed on the account (`basic`) axis** (round-2
  review, HIGH). The snapshot path still leaked a `NotExisting` account's stale
  info: `create_snapshot` inserted it into `accounts`, so `EvmOverlay::basic`
  returned a phantom existing account where live revm / `loaded_account_info`
  return `None`. Now `create_snapshot` excludes `NotExisting` accounts from
  `accounts`/`code_by_hash` and records them in a new
  `EvmSnapshot.accounts_not_existing` set; `EvmOverlay::basic` returns `None` for
  them (no `ext_db` fall-through). `target_account_info` (deploy path) and
  `loaded_account_info` (code_hash normalized at load) were brought into line too.
- **Freshness validator trust contract hardened** (Phase 2 review). The
  background validator no longer returns a *trusted* verdict on incomplete or
  ambiguous verification:
  - **Fixed-point round cap → `Unverified`.** Exceeding `MAX_VALIDATION_ROUNDS`
    (corrections kept opening new volatile slots) now returns
    `Validation::Unverified` and queues no corrections, instead of a best-effort
    `Corrected` resting on un-verified state.
  - **Corrected re-run host error → `Unverified`.** A failed corrected re-run
    (a `transact` error, not a revert/halt) returns `Unverified` rather than
    silently keeping the stale optimistic result.
  - **Missing fetcher results → `Unverified`.** A new `collect_fetch_results`
    helper requires the batch fetcher to return *every* requested slot; an omitted
    slot yields `Unverified` instead of defaulting to zero (which could produce a
    false confirmation/correction with a custom fetcher).
- **`call_raw_with_access_list*` reverts its checkpoint on transact errors**
  (Phase 2 review; `EvmCache` + `EvmOverlay`). Previously the host-error path
  `?`-returned before `checkpoint_revert`, leaving the journal checkpoint
  un-reverted; both methods now revert on every path. This is recorded in the
  fixed-issues section of `docs/KNOWN_ISSUES.md`.

### Notes

- MSRV is Rust 1.88; edition 2024. Both are enforced in CI.
- `EvmCache` requires a multi-thread tokio runtime for any RPC-touching path.
- See [`docs/KNOWN_ISSUES.md`]docs/KNOWN_ISSUES.md for current limitations.

[Unreleased]: https://github.com/KaiCode2/evm-fork-cache/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/KaiCode2/evm-fork-cache/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/KaiCode2/evm-fork-cache/releases/tag/v0.1.0