Phase 5: copy-on-write snapshots (Pillar A) + overlay reuse - #4
Merged
Conversation
docs/phase-5-spec.md is the build contract for the memoized-immutable-base snapshot design + overlay buffer/instance reuse, with locked decisions D1-D5. tests/cow_snapshot.rs is the red gate: a differential-equivalence property (create_snapshot must be read-indistinguishable from a retained create_snapshot_deep_clone reference after every mutation kind) plus the overlay reset()/buffer-reuse contract. Red until the implementation lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the O(total state) deep-clone create_snapshot with a two-tier copy-on-write snapshot: the cold layer-2 BlockchainDb index is flattened once into an immutable Arc<BaseState> (per-account storage shared by Arc), memoized across snapshots and rebuilt copy-on-write only for changed addresses; each snapshot folds just the hot layer-1 CacheDB delta over a cheap Arc::clone. Reads stay O(1) and lock-free (no persistent-map dep, D1); EvmSnapshot stays Send+Sync. create_snapshot is now &mut self (D5); create_snapshot_deep_clone is retained as the A/B baseline and the differential read-equivalence reference (D3). Every controlled layer-2 write marks the base dirty; an O(accounts) length-scan catches the append-only lazy-fetch growth. Overlay reuse (D4): EvmOverlay::reset() recycles an overlay across sims, and the 64KB shared-memory buffer is reused across calls via a Send-preserving take/reclaim (plain Vec field, method-local Rc) instead of re-allocating per build. Indicative: create_snapshot ~30-60x faster than the deep clone on the cold-index sweep; reset()-recycled fan-out beats fresh-overlay. Overseer review + adversarial-panel remediation: - mark_base_dirty in override_account_code_with_missing_target (D2 uniformity). - invalidate_snapshot_base() public re-honest hook + rustdoc warnings on blockchain_db()/backend() + a load-bearing-invariant note in refresh_base, for the one residual edge (an out-of-band same-length layer-2 overwrite through the public escape hatches); pinned by a guard test and recorded in KNOWN_ISSUES. Tests: 328 (default) / 275 (--no-default-features), incl. the cow_snapshot differential gate (COW == deep-clone after every mutation kind) and overlay-reuse contract. fmt + clippy (both configs) + doc + bench --no-run clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-context EVM working-memory buffer was hardcoded to 64 KB in two places
(EvmCache + EvmOverlay), tuned for a state-heavy upstream workload. Make it a
first-class knob:
- `SharedMemoryCapacity { Fixed(usize), Auto }`, default `Fixed(64_000)`,
configured via `EvmCacheBuilder::shared_memory_capacity`. `Fixed` pins the size
(general users running wide fan-outs of small sims can lower it to cut
per-overlay memory); `Auto` sizes from the chain state loaded at build time
(e.g. a bincode state file) — `loaded_slots * 16`, clamped to a 64 KB floor /
4 MiB ceiling.
- Resolution happens in the new `with_cache_capacity` constructor (the builder's
worker; `with_cache`/`new`/`from_backend` keep their signatures, defaulting to
Fixed(64_000)). `Auto` reads the post-load layer-2 slot count, so it captures
the maintain-list filter and any source, not just the raw file.
- The resolved size is stored on EvmCache, exposed via
`EvmCache::shared_memory_capacity()`, raised by `reserve_shared_memory`, and
copied onto every EvmSnapshot so snapshot-backed EvmOverlays pre-allocate the
same amount (overlay gains a `buffer_capacity` field; the hardcoded overlay
constant is removed).
Tests: a `resolve` heuristic unit test (floor/linear/ceiling, both feature
configs) and `tests/shared_memory_capacity.rs` end-to-end over the builder
(default, Fixed, Auto-with-no-state floor, and Auto sizing 10k loaded slots →
160_000). Full suite 335 (default) / 282 (--no-default-features); fmt + clippy
(both configs) + doc + bench --no-run clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`dropping_speculative_sim_aborts_before_queueing_correction` assumed the SpeculativeSim's drop-abort would win a race against the spawned multi-thread validator's first poll, which fails intermittently under full-suite parallel load. Replace the racy "called" atomic flag with a `Gate` (Mutex + Condvar): the fetcher blocks until the test releases the gate, and the test releases it only *after* `drop(sim)` sets the cancel flag. So the validator's fetch — and thus its post-fetch, correction-queuing checkpoint — can only complete once cancellation is already observable, regardless of scheduler interleaving. Drops the over-strict "fetcher never reached" assertion (the product guarantees a cancel seen at a checkpoint suppresses side effects, not that an in-flight fetch is skipped) and keeps the real invariants: no correction queued, no re-run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses three PR-review findings on the copy-on-write snapshot: - P2 (correctness): refresh_base's Case-4 partial rebuild cloned the previous code_by_hash and only added refreshed dirty-account codes, so a purged or recoded account left a stale hash. A direct EvmOverlay::code_by_hash(old_hash) then returned removed bytecode while create_snapshot_deep_clone (which rebuilds the index from current accounts) returned none — a read-equivalence violation and a slow memory leak. Fix: rebuild the index from the refreshed accounts via a shared `code_index` helper used by both build_base_full and the Case-4 path, so the two stay in lockstep; handles shared hashes (a hash survives iff some present account still carries it) and prunes unreferenced ones. - P3 (coverage): the differential gate now also compares code_by_hash for each probed account's code hash, and a new regression test (cow_code_index_matches_deep_clone_after_base_account_recoded) warms the base with bytecode, recodes the account, dirties it via a controlled per-address write (Case-4 partial rebuild), and asserts the old hash no longer resolves. Verified red against the pre-fix code. - P3 (docs): create_snapshot rustdoc no longer claims it "merges both layers into a single flat HashMap"; it now describes the memoized layer-2 base + layer-1 overlay fold and the &mut self receiver. Tests 329 (default) / 276 (--no-default-features); fmt + clippy (both configs) + doc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deflake the drop-abort freshness test with a deterministic gate
Configurable EVM shared-memory pre-allocation (SharedMemoryCapacity)
Address known issues
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Pillar A of the roadmap: replace the O(total-state) deep-clone
create_snapshotwith a copy-on-write snapshot, and add overlay buffer/instance reuse. Stacked onphase-3-state-updates(which now carries the merged Phase 4 work, #3).Design (locked decisions D1–D5, see
docs/phase-5-spec.md)A two-tier snapshot: the cold layer-2
BlockchainDbindex is flattened once into an immutableArc<BaseState>(per-account storage shared byArc), memoized across snapshots and rebuilt copy-on-write only for changed addresses; each snapshot folds just the hot layer-1CacheDBdelta over a cheapArc::clone.Arc-shared maps, not a persistent/HAMT crate: reads stay O(1) and lock-free, no per-SLOADregression, no new dependency.create_snapshot_deep_clone()retained as the A/B baseline and the differential read-equivalence reference.EvmOverlay::reset()recycles an overlay across sims; the 64 KB shared-memory buffer is reused via aSend-preserving take/reclaim (plainVecfield + method-localRc).create_snapshotis now&mut self(it memoizes the base). All call sites updated.EvmSnapshotstaysSend + Sync,EvmOverlaystaysSend; read semantics (StorageCleared/NotExisting/two-layer precedence) are bit-for-bit identical to the legacy flatten.Benchmarks (
benches/simulation.rs, measured)create_snapshotCOW vs the legacy deep clone, cold index in layer 2:The deep clone scales with total slots; COW eliminates the per-slot copy and tracks
accounts + changed.overlay_fanoutreset-recycled is a modest win that grows with fan-out width.Tests
tests/cow_snapshot.rs— a differential-equivalence gate:create_snapshotis asserted read-identical tocreate_snapshot_deep_cloneafter 12 mutation kinds (layer-2-only write-through, same-length inject overwrite, simulated uncontrolled backend growth, purge,set_block,NotExisting, …), plus COW non-aliasing, overlayreset()/buffer-reuse, and an escape-hatch guard. 328 passed (default) / 275 (--no-default-features); existing snapshot/overlay/freshness contracts pass unchanged. fmt + clippy (both configs) + doc +bench --no-runclean.Review
Line-by-line overseer review + a 3-way adversarial panel (staleness / read-equivalence / concurrency) confirmed the core model sound (read-equivalence bit-for-bit; the COW rebuild never mutates a shared
Arc; the count-based growth scan is sufficient because the lazy fetch is append-only — validated against foundry-fork-db's source). Remediation applied:override_account_code…) now dirties the base.blockchain_db()/backend()handles (a contract boundary, not reachable internally) — now has a publicinvalidate_snapshot_base()re-honest hook, rustdoc warnings on both accessors, a load-bearing-invariant note inrefresh_base, a guard test, and aKNOWN_ISSUES.mdentry.Docs
CHANGELOG (
### AddedCOW +reset();### Changedcreate_snapshot/inject_storage_batch→&mut self), ROADMAP (Phase 5 → Done), KNOWN_ISSUES (limitation resolved + the boundary documented).🤖 Generated with Claude Code