feat: coverage-replayer — minimal mainnet block set maximizing mega-evm branch coverage#153
Draft
flyq wants to merge 37 commits into
Draft
feat: coverage-replayer — minimal mainnet block set maximizing mega-evm branch coverage#153flyq wants to merge 37 commits into
flyq wants to merge 37 commits into
Conversation
New workspace member bin/coverage-replayer: replays mainnet blocks against mega-evm with branch-granular coverage instrumentation and maintains the minimal block set that maximizes coverage. - backfill: RPC fetch -> spool -> resident worker pool (per-block counter reset/capture via LLVM profiler runtime FFI) -> non-zero-counter bitmap -> pattern dedup + representative archiving (redb store) - set-cover: greedy minimal block set with incumbent-biased tie-breaking - report: llvm-cov summary for the selected set (only place llvm-cov runs) - executes via stateless-core replay_block over LightWitness; skips witness verification/state-root update; free gas/receipts/bloom sanity checks - [profile.coverage] added for instrumented builds; zero source changes to existing crates
…evm sources - write_atomic: concurrent fetch tasks resolving the same contract hash collided on the shared .tmp name; embed pid+seq so both writers succeed - report: llvm-cov segfaults reporting over the full covmap (LLVM instantiation-group bug on some dependency files); restrict SOURCES to the mega-evm checkout auto-detected from Cargo.lock (--source-dir to override) - set-cover/backfill: emit per-block worker timing stats (E3 measurement) - add replay_fixtures integration test: replays all test_data/mainnet blocks through the worker execution path and asserts the gas/receipts/bloom sanity triple - document that -C link-dead-code must NOT be used (breaks revm const-eval) Local mainnet E2E (200 blocks): 200/200 replayed, zero sanity failures, worker avg 136ms/block, 76 patterns / 1522 branch counters, set-cover picks 8 blocks, llvm-cov branch coverage 29.87% for tip-only traffic.
Execution-only consumers (debug-trace-server, coverage-replayer) read state
from a witness but never verify its proof — yet decoding a SaltWitness pays
one Element::from_bytes (modular sqrt + subgroup check) per parent commitment
plus the IPA proof parse: ~110ms wall / ~1 core-second per large mainnet
witness even with salt's parallel validation, all for output that is
discarded.
stateless-core: add LightWitnessFromSalt, a serde adapter that decodes
LightWitness DIRECTLY from full SaltWitness bytes. Layout-mirror types keep
the deserializer byte-synchronized while commitments (NodeId -> [u8;32]) and
the IPA proof (Vec<u8>) are consumed as raw bytes and dropped — no curve
point is ever constructed. ~1.4ms single-threaded for the same witness
(~80x wall, ~700x CPU). Mirror congruence is locked by tests against all
mainnet fixture witnesses (byte-exact stream consumption, config-agnostic,
and a corrupt-point stream that must fail the full decode but pass light).
stateless-common: decode_witness_payload_light / decode_witness_response_light
(the latter also returns the raw compressed payload for archival), plus
RpcClient::get_witness_light{,_with_deadline}. Includes an end-to-end test
light-decoding the committed real-mainnet payload
test_data/mainnet/bench/6906405.zst.
coverage-replayer: the backfill fetcher now uses get_witness_light — no EC
work per block, and the spool archives the exact wire payload instead of
re-encoding one.
Also refreshes the stale "~240ms" performance notes in light_witness.rs
(pre-salt#137 numbers).
…on demand Per review: the only consumer of a stored raw payload would have been PR payload assembly, and E4 established the RPC serves witnesses for the full history — archiving raw bytes that are re-fetchable on demand is waste. - get_witness_light / decode_witness_response_light no longer return the raw compressed payload; the light fetch is now strictly (LightWitness, MptWitness) - SpoolEntry loses witness_payload: spool and archive hold only what replay needs (block JSON + light witness + code hashes); full witnesses are never written to disk - PR payload assembly (M3) will fetch full witnesses for the few selected blocks via get_witness at build time
Opens the store without the binary-id namespace check (read-only) so data produced on another machine/build can be analyzed locally: block status and worker-timing stats, pattern/hit-count distribution, counter-rarity, and the current manifest summary.
…es archival Server data showed the archive problem was profraw, not entries (1.8G vs 8.2M for 391 patterns): a raw profraw carries the whole binary's counter array plus an incompressible (already zlib'd) name table, so plain zstd only managed 2.3x. Promotion now converts to single-block sparse profdata (llvm-profdata merge -sparse drops every zero-count function and its name entry) + zstd: ~106KB per pattern, ~45x smaller, produced off the judge's critical path (JoinSet of blocking tasks, drained before exit). report inflates the profdata files and merges them as before — identical output. Entries archival is removed entirely per review: representatives' block/witness data is re-fetched from the RPC by block number (recorded in the store) when a resweep or PR payload needs it. Nothing block-sized is retained anywhere.
…mate, greedy preview
…ain prune 101k-block run data: patterns grow ~650 per 10k blocks with no saturation, but 93% are strict subsets of an existing pattern — mathematically unable to improve the minimal cover. Promotion now records dominated patterns' bitmaps (dedup/stats) but skips the profile archive; set-cover excludes dominated patterns from candidates (a dominated pattern could otherwise win a gain tie-break and select a block with no archived profile) and deletes profiles of patterns dominated after the fact. Archive steady state drops from linear growth (~880MB per 100k blocks) to the antichain (~60MB, bounded).
…megaeth-labs/stateless-validator into liquan/coverage-replayer-design
…ives, profile-by-pattern Prerequisites for the full-history backfill so it need not be redone when the resident run mode lands: - binary_id is now a fingerprint of the instrumented mega-evm build (locked rev from Cargo.lock + rustc version, captured by build.rs) instead of a whole-exe hash. Editing the dispatcher / adding resident mode no longer invalidates an existing store; only a real mega-evm or toolchain change does — exactly when counter ids actually shift. - A pattern's representative is now the lightest (min replay time) block seen for it, re-homed whenever a lighter one appears — the best fixture candidate, and correct for size-based exclusion. - Archived sparse profiles are keyed by pattern, not block, so re-homing the representative never moves or orphans a profile. report looks them up via the manifest's pattern field. Note: the schema change (PatternRecord.representative_elapsed_ms) and new binary_id mean pre-existing stores are not readable — the full-history scan starts from a fresh data-dir anyway.
Full history (~20M blocks) is ~6 days single-machine for the real segments plus ~2 weeks for the 6M-7M stress range (~19s/block). The merge subcommand combines per-shard stores (same instrumented binary, disjoint ranges) so 4 machines finish in a few days. Correctness: a pattern bitmap uses each shard's own dense indexing (counter first-seen order differs per shard), so bitmaps cannot be OR'd directly. merge remaps every bitmap via source-dense -> content-addressed counter id -> unified-dense, then folds patterns by key (FxHash of sorted counter ids, machine-stable). Unit tests lock the reversed-dense-order case and shard-order independence; an E2E check confirms merge(2 shards) yields the same universe as one sequential run over the union. Also determined and documented: per-block coverage is deterministic, but distinct-pattern count wobbles +-1-2 under multi-worker/sharding because of mega-evm per-process caches whose hit/miss branch depends on worker warmth. Universe (coverage) and minimal-cover size are INVARIANT across 1-worker/4-worker/sharded (1454 in 5 independent runs), so multi-worker production is correct. Prereqs also landed: store.write_bulk, BitSet::iter_ones, StoreSnapshot: Clone
Port the R2 witness primitives (keys / sigv4 / endpoint / client) from mega-reth c290c3e into a dedicated leaf crate, replacing the git-rev dependency; mega-reth will delete its copy and consume this one via git tag, making the cross-repo dependency one-way. - add keys::block_object_key as the single primary-key template (object_keys delegates, the validator's R2 source calls it directly) - add golden wire-vector tests: a real migrated mainnet object key and a full SigV4 header set computed with an independent implementation - unify the workspace on reqwest 0.12 (0.13 was only used by debug-trace-server dev-deps); stateless-r2 keeps an explicit 0.12 pin since &reqwest::Client is part of its public API - r2_witness: carry Bytes end-to-end (no multi-MB copy per block), dedicated DecodePanicked variant, module-level retry consts, drop the unused deadline parameter and the per-fetch debug header log - docs: neutral witness-source wording; R2 is a first-class source Co-Authored-By: Claude Fable 5 <[email protected]>
… worker timeout, fail-stop judge Full-scan policy per review: every block must eventually execute; a stuck or failing block must be loudly visible, never silently skipped. - fetch: infinite retry with 5s backoff and per-attempt WARN (was: warn+skip) - worker: --block-timeout-secs replaced by --slow-block-warn-secs; a long block only logs 'still executing' periodically and is never killed; a crashed worker respawns and retries the SAME block indefinitely - judge: replay error or sanity divergence records the block (spool kept for forensics) and ABORTS the whole run — with every block independently verified to replay cleanly, any failure is an infrastructure/chain-spec bug and scanning past it would leave a silent coverage gap. Replaces the 20-consecutive-divergent streak heuristic. - work list now skips only status=Ok blocks: Error/Divergent records are retried automatically on re-run (previously permanently excluded)
- Record R2 witness fetch metrics: fetch+decode duration histogram, GET retry counter, surfaced-error counter labelled by kind, and the same witness-size breakdown the RPC path reports (pre-registered at startup like the RPC method counters). - Add an end-to-end happy-path test: a fixture witness encoded with encode_witness_payload served over the mock R2 server must decode back to the original tuple in exactly one GET (mock now serves byte bodies). - Warn at startup when --witness-endpoint is set but ignored because --witness-source r2 is active. - Fix rustdoc intra-doc links introduced by this branch. Co-Authored-By: Claude Fable 5 <[email protected]>
The trace server never verifies witness proofs (it executes from LightWitness and trusts its data sources), yet both RPC paths did the full SaltWitness decode — one Element::from_bytes (modular sqrt + subgroup check) per parent commitment, ~100ms wall / ~1 core-second on large witnesses — and then threw the proof away via LightWitness::from. - DataProvider::fetch_witness (stateless mode, on-demand): now get_witness_light_with_deadline; the Step-3 conversion stage is gone, so uncached trace requests lose ~100ms of latency on large witnesses - TraceFetcher::fetch (local cache mode, chain sync): now get_witness_light; prefetch stops burning ~1 core-second of EC work per block - WitnessSizeBreakdown::new_light for the size metric on light paths: a documented lower bound (parent commitments are never materialized, so their contribution is unknowable without the full decode) The DB-cache read path already stored/loaded LightWitness directly and is unchanged. All 94 debug-trace-server + stateless-common tests pass (the integration suites exercise the full mock-RPC fetch -> light decode -> trace pipeline).
…an/coverage-replayer-design
Run outputs (store.redb, codes cache, manifests) are transferred as tarballs over jumpserver from now on — far too large for git. The upcoming full-history scan starts fresh anyway.
--witness-source r2 fetches each block's witness object straight from the R2 bucket (SigV4-signed GET via stateless-r2 primitives, same key layout as the uploaders) and decodes it with LightWitnessFromSalt — zero elliptic-curve work, unlike the validator's R2 path which needs the full decode. block / bytecode / tip queries stay on --rpc-endpoint; --witness-endpoint is only required for (and only used by) the rpc source. The client deliberately has no internal retry: the backfill fetcher's retry-forever loop (WARN + 5s, blocks are never skipped) is the retry policy, and its flat 5s cadence is already gentler than a backoff ladder's early rounds. Errors carry status/key/body context for the log. Mirrors the validator's flag names with the COVERAGE_REPLAYER_ env prefix. Tests: mock-R2 happy path (fixture payload light-decodes to the same light parts as the full decode), status surfacing, corrupt-body decode error. Also drops committed run artifacts (backfill.log, eras.log, bin.pid) and gitignores *.log / *.pid.
Correctness / operational (the four-machine full-history scan depends on these): - judge: a new pattern's sparse profile is now archived BEFORE the pattern + Ok record are committed (and synchronously, replacing the fire-and-forget JoinSet). A crash between the two previously orphaned the pattern forever: the block was Ok so never retried, later same-bitmap profraws were deleted, and report hard-failed on the missing profile. Archive failure is now fail-stop; the spool entry is deleted only after the commit. - binary_id: fingerprint now uses `rustc -vV` (release + host + LLVM version) instead of `rustc --version`, which carried neither the target triple nor the LLVM version — macOS and Linux builds previously shared an id, so merge would happily combine cross-target stores whose counter ids may differ. A missing mega-evm git rev in Cargo.lock (path-dep override) is now a hard build error instead of a silent "unknown" namespace collapse. - store namespace now also stamps and enforces --symbol-filter (backfill and merge; the filter defines the counter universe, so mixing filters silently blended incompatible universes). merge requires all shards to agree. - pattern keying extracted to store::pattern_base_key, shared by the judge and merge — the byte-for-byte mirror contract is now structural, not copy-paste. - fetch retry rounds reuse the already-downloaded block (a witness-side retry loop no longer re-downloads the full block every 5s). - --workers 0 now fails validation instead of hitting tokio's channel assert. - R2 secret access key wrapped in a Debug-redacting type (same hardening as the validator's RedactedSecret). - merge: deterministic dense assignment (sorted per shard), the per-pattern reverse-map rebuild removed (was O(patterns x total_counters)), collision re-key warning now covers the wrong-content rsync-union case, and the module docs no longer claim byte-identical output. - archive_sparse_profile cleans up its tmp profdata on failure paths. Tests (previously untested algorithm cores): - set-cover core extracted as a pure function (fs side effects stay in run) with 5 tests: full coverage, dominated pruning, equal-bits survival, incumbent tie-break, redundancy elimination of a covered first pick. - judge tests on a real temp store: known-pattern dedup + lightest-block re-homing, dominated-new-pattern (bitmap recorded, no archive), fail-stop on replay error / sanity divergence, and the shared keying contract. - WitnessSizeBreakdown::new_light lower-bound test (gap == commitments term). Docs / conventions: - README + AGENTS.md: three binaries, coverage-replayer rows added. - debug-trace-server: unused salt dependency removed. - .gitignore: stray unanchored archive/codes/tmp entries removed. - stale doc claims fixed (rpc_client light-metrics rationale, inspect 'never writes', crate doc pointer to a gitignored file, R2 'full history' comment now warns about bucket lifecycle retention). Note: the binary_id change invalidates pre-existing stores (none in production — the full scan starts fresh).
…ilds Under --no-default-features `std` is the `alloc` alias and the prelude has no `vec!`; cargo test -p stateless-core --no-default-features --lib failed to compile the corrupt-point test. Explicit `use std::vec;`, same as chain_spec.rs.
Codecov Report❌ Patch coverage is
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…-loop wins Reuse: - pattern probing walk extracted to store::resolve_pattern_slot (with PROBE_STEP moved beside pattern_base_key): the judge and merge now share the byte-identical keying + probing mechanically instead of by comment; merge tests key via pattern_base_key instead of hand-rolled FxHasher - strict-domination predicate unified as PatternRecord::dominates (was triplicated; the load-bearing 'bits >, never >=' now has one definition) - inspect's antichain estimate + greedy preview replaced by one setcover::select_cover call — the preview can no longer drift from a real set-cover run (it previously omitted pruning and redundancy elimination) - sorted-dedup code-hash collection hoisted to stateless_core::collect_code_hashes (spool + trace-server copies deleted) - elapsed p50/p95 summary triplication -> store::elapsed_stats - report locates the mega-evm checkout from the compile-time rev (COVERAGE_MEGA_EVM_REV) instead of re-parsing ./Cargo.lock at runtime — no cwd dependence, cannot disagree with the instrumented build - rpc_client: fetch_witness_raw / fetch_witness_light collapse into one generic fetch_witness_with Simplification: - judge ingest: one block_record constructor for the three status paths and a single commit/cleanup tail (archive-before-commit invariant unchanged) - select_cover applies redundancy elimination internally; callers no longer filter selected by redundant_removed - Store::load / write_bulk table-loop triplication -> generic read_table/write_table - dead code removed: SpoolEntry.block_hash (never read), owned From<SaltWitness> for LightWitness (no callers), BitSet::words (callers now use iter_ones); from_salt_witness module made private (newtype is the only consumer); current_binary_id no longer returns Result - DataDir::new is pure path arithmetic; writers call ensure_layout() — inspect / merge shard inputs no longer scaffold empty dirs in foreign paths Efficiency: - BitSet::is_subset_of short-circuits on the first disproving word — the inner kernel of every dominance scan (~40x on the common non-subset case) - merge remap uses flat per-shard dense->id/unified tables (one array index per set bit instead of two hash lookups; billions of bits at full scale) - inspect counter-rarity uses a dense Vec instead of a HashMap 267 workspace tests green (incl. no_std lib tests); instrumented two-shard E2E (backfill x2 -> merge -> set-cover -> report -> inspect) verified on mainnet blocks.
…fore exit Addresses the two open Codex review findings on this PR: - R2WitnessClient now enforces --witness-max-concurrent-requests with its own semaphore (permit scoped per GET attempt, like the RPC path's), so the documented knob for bounding witness I/O works in R2 mode instead of being silently ignored. Queue wait on the cap is excluded from the fetch-duration histogram so it cannot masquerade as R2 latency. Concurrency-probe test asserts the in-flight high-water mark. - run_with_signals sends one final validation report after the pipeline (and any signal drain) has fully stopped. The periodic reporter is cancelled the instant the pipeline completes, so an --end-block slice run could previously finish without ever reporting its tail upstream (no later restart re-reports it). The reporting round is extracted into report_range_once, shared by the tick loop and the final flush. New end-to-end regression test drives run_with_signals to a sync target and asserts the mock upstream saw the final tip; both new tests were mutation-tested against the reverted fixes. Co-Authored-By: Claude Fable 5 <[email protected]>
The final shutdown report inherited the periodic reporter's best-effort semantics (log + skip), but it has no next tick behind it: a transient endpoint blip at exactly shutdown would permanently lose an --end-block slice run's tail report while the process exits 0. report_range_once now returns whether the round settled, and the final flush retries up to 3 attempts (1s apart) before giving up with an ERROR log; a detected validation gap stays non-retried. Regression test fails the first report call via the mock and asserts the retry lands the tail (mutation-tested against FINAL_REPORT_ATTEMPTS = 1). Also fixes both pre-existing rustdoc warnings: re-export ValidationTask (it is ValidatorFetcher's public Output type) and backtick the <metrics-port> placeholder that rustdoc read as an unclosed HTML tag. Co-Authored-By: Claude Fable 5 <[email protected]>
Comment-only pass over PR #152: compress multi-sentence narratives to the minimal statement of the constraint, keep every load-bearing "why" (throttle rationale, redirect policy, wire-format contracts, golden-vector docs), and give each rationale a single home instead of restating it at every call site. No code changes. Co-Authored-By: Claude Fable 5 <[email protected]>
Round 1 (review findings): - write_atomic: fsync before rename (+ best-effort dir fsync), tmp cleanup on failure — the archive-before-commit invariant now survives power loss, not just process crashes; stale *.tmp swept at backfill startup - fetch_block: decode-validate existing spool entries (delete + refetch on corruption) and re-resolve missing codes, so no on-disk artifact can wedge the run across restarts - worker stdout: stray library prints no longer kill the worker (deterministic prints wedged the block forever); repeated crashes on one block escalate to error-level logs every 10 attempts - light_witness: metadata mapped to None is a decode error, not unreachable! (witness bytes are unvalidated network input on the light path) - judge: first_block min-folded on the occupied path (matches merge); elapsed samples bounded by a deterministic decimating reservoir; BLOCKS table loaded only for the scanned range (full-history stores hold tens of millions of rows) - store: schema_version enforced on open/open_readonly; tests for the probe collision walk and namespace rejections - set-cover: refuses a missing store (no more silent 0-block manifests); redundancy-eliminated picks actually logged; completeness contract documented (full-universe coverage always; minimality never at its expense) - inspect: last growth bucket no longer drops patterns on exact-multiple spans - merge: BlockRecord.pattern_key rewritten through re-keyed patterns - misc: unused deps dropped, rebuild hint includes --target, R2 secret via CLI warns about process-list visibility, AGENTS.md test section corrected Round 2 (adversarial verification of round 1): - sweep runs only after Store::open's exclusive lock and skips files younger than 1h (a doomed double-start could delete a live writer's tmp files) - response frames are salvage-parsed out of dirty stdout lines (an unterminated print! glued to the frame previously hung the round-trip) - code files are content-verified (keccak) and refetched on mismatch — a truncated code file could previously fail-stop the run on every restart - spool validation mirrors the worker's checks (block_json parses, number matches) and spool files carry a zstd content checksum, so any byte-level corruption routes into delete-and-refetch - slow-block warning stays factual when non-protocol output was seen (no destructive restart advice for merely slow blocks) Verified: 282 workspace tests, clippy clean, instrumented E2E (two-shard backfill/merge/set-cover/report/inspect; planted corrupt spool and corrupt code files heal in-run; full-universe cover 772/772).
flyq
added a commit
that referenced
this pull request
Jul 17, 2026
…erf docs Review follow-ups on this PR: - estimate_witness_size removed: its only caller was the data-provider path this PR migrates to WitnessSizeBreakdown::new_light; no other in-workspace or mega-reth usage exists. - Doc comments no longer pre-reference the coverage-replayer (it lands with PR #153); consumers are named generically. - Perf numbers in docs recalibrated to a reproducible measurement on the committed 6.3MiB mainnet payload (14-core M4 Pro, median of 20): full decode ~112ms wall / ~1.4 core.s CPU, light ~3.7ms single-threaded (bincode stage); payload-level incl. zstd: 124.8ms vs 6.0ms. Co-Authored-By: Claude Fable 5 <[email protected]>
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.
Summary
Adds
bin/coverage-replayer, an offline tool that replays mainnet blocks under LLVM branch instrumentation and derives the minimal block set covering every branch counter ever observed in mega-evm — the source of truth for keepingtest_data/mainnetmaximally representative. Also adds the zero-validation "light" witness decode to stateless-core/common and switches debug-trace-server onto it. Carries PR #152 (stateless-r2) via a merge; those files are reviewed there.coverage-replayer
backfillfetches blocks + witnesses (RPC or straight from R2 with--witness-source r2), resident worker subprocesses replay each block with per-block counter isolation (reset →replay_block→ capture; process isolation keeps the dispatcher out of the bitmaps), and a judge dedups per-block coverage bitmaps into patterns in a redb store, keeping one ~106KB sparse profdata per non-dominated pattern. Never-skip semantics: fetch retries forever, slow blocks are warned about but never killed, replay errors / header-sanity divergence fail-stop the run and are retried on re-run.set-covercomputes the greedy minimal cover (antichain pruning, incumbent-biased tie-breaks, redundancy elimination);reportrenders llvm-cov for the selected set;mergecombines per-machine shard stores (dense remap via content-addressed counter ids);inspectprints store statistics. Stores are namespaced by a build fingerprint (mega-evm rev + toolchain/host) and the symbol filter.Light witness decode
LightWitnessFromSalt(stateless-core) decodes kvs+levels directly from fullSaltWitnessbytes via a layout-mirroring deserializer — no curve points constructed: ~1.4ms vs ~110ms wall (~700x CPU) on a 6.3MiB mainnet witness. Locked by byte-exact tests against every mainnet fixture and the committedtest_data/mainnet/bench/6906405.zst.get_witness_light/decode_witness_payload_lightin stateless-common.debug-trace-server
Both RPC witness paths switch to the light decode (the server never verifies proofs): uncached trace requests lose ~100ms on large witnesses; chain-sync prefetch stops burning ~1 core·s/block. Note: the
witness_generatorsize metric now records a documented lower bound (excludes parent commitments), shifting the histogram down ~40-60%. Unusedsaltdep dropped.Verification
267 workspace tests green (incl. new set-cover/judge/merge/R2/light-decode units and a fixture integration test replaying all
test_data/mainnetblocks with gas/receipts/bloom equality). Validated on mainnet: 241k blocks replayed with zero sanity failures; branches coverage 47.90% for the selected 45-block set vs 32.63% from tip-only traffic.