fix: cache every leaf-root epoch slot, not just the main one - #610
Open
blacks1ne wants to merge 1 commit into
Open
fix: cache every leaf-root epoch slot, not just the main one#610blacks1ne wants to merge 1 commit into
blacks1ne wants to merge 1 commit into
Conversation
A `leafroot:LeafRootRegistration` vertex carries a rolling three-epoch window — `Prev*`, the main slot and `Next*` — which `upsert_leaf_root_registration` maintains so that an opening produced just before an epoch boundary is still verifiable just after it. The vertex has two readers. `leaf_root_registration_for_epoch` matches an opening against whichever slot holds the epoch being proved, so it sees all three. The prover-registry cache — read via `get_leaf_root`, which is what the app-shard storage-attestation check in `frame_validator` calls — decoded only the main slot. So around an epoch boundary the two disagreed: the audit reader found the registration, the cache did not, the storage attestation was rejected, and `handle_cw_finalized_frame` dropped an honest, committee-certified frame. That is the cache half of QuilibriumNetwork#589. `decode_leaf_root` now returns one entry per populated slot instead of an `Option` of the main one. Populated is decided with a new `read_u64_be_opt`: `read_u64_be` cannot tell an absent field from a stored zero, and `Prev*`/`Next*` are simply missing until the window rolls, so the existing reader would have cached a phantom entry at epoch 0. The regression test drives the real `refresh` over a real store and asserts the cache against `leaf_root_registration_for_epoch` across the whole window and past both edges — the two readers must agree everywhere, not merely produce expected values. It uses only stable API so it compiles against pre-fix source, where it fails with `one cache entry per populated epoch slot: left: 1, right: 3`. The ρ_N anchor half of QuilibriumNetwork#589 is untouched and still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
blacks1ne
force-pushed
the
blacks1ne/fix-leafroot-cache-epoch-window
branch
from
August 18, 2026 10:35
d747069 to
b2a7383
Compare
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.
Fixes the prover-registry half of #589 — the bullet the issue names directly: "registration vertices retain PrevEpoch, Epoch, and NextEpoch, while the cache decoder currently loads only the main Epoch slot." #594 removed revalidation for locally verified bytes, which is the right fix for a replica that verified the proposal itself, but the decoder bug is in the code both paths share and still bites every certificate-only replica.
Base:
932045a3The bug. A
leafroot:LeafRootRegistrationvertex carries a rolling three-epoch window —Prev*, the main slot,Next*— whichupsert_leaf_root_registrationmaintains so an opening produced just before an epoch boundary is still verifiable just after it. The comment on the third slot says why it exists: it "covers the anchor-lagged storage audit window."That window is not hypothetical.
materialize.rs:23records what it was for: "A batch of honest, active app-shard provers was spuriously KICKED at epoch boundaries by an anchor-lagged storage audit (fixed upstream via 3-slot leaf-root registration), which set Status=4 + KickFrameNumber AND zeroed Seniority with no restore path" — the incident the flag-day amnesty exists to clean up. The writer and the audit reader were both updated for that fix; this decoder was left at one slot.So the vertex has two readers and they disagree.
leaf_root_registration_for_epochmatches an opening against whichever slot holds the epoch being proved, so it sees all three. The prover-registry cache — read throughget_leaf_root, which is what the app-shard storage-attestation check calls (frame_validator.rs:902, keyed onepoch_for_frame(header.global_frame_number)) — decoded only the main slot.So at exactly the boundaries the third slot exists to cover, the audit reader found the registration and the cache did not. The storage attestation was rejected, and
handle_cw_finalized_framedropped an honest, committee-certified frame.The fix.
decode_leaf_rootreturns one entry per populated slot instead of anOptionof the main one, andrefreshinserts each.Whether a slot is populated is decided by a new
read_u64_be_optrather than the existingread_u64_be. That is not incidental:read_u64_bereturns0both for an absent field and for a stored zero, andPrev*/Next*are simply missing until the window rolls. Reusing it would have cached two phantom entries at epoch 0 for every fresh registration — a registration claimed for an epoch it was never registered for, which is worse than the drop it replaces. The leaf-root bytes are checked non-empty as a second gate.leaf_root_count()now counts cache entries rather than registrations, so its doc comment says so and points atleaf_root_vertex_countfor the old meaning.Tests.
leaf_root_cache_covers_every_epoch_slot_like_the_audit_readerbuilds a real three-slot vertex throughupsert_leaf_root_registration(epochs 5/6/7), writes it under the realGLOBAL_INTRINSIC_ADDRESS ++ leaf_root_address(...)key, and drives the realrefreshover a real store. It then asserts the cache againstleaf_root_registration_for_epochacross 3..=9 — the two readers must agree everywhere, inside the window and past both edges, rather than merely matching hardcoded expectations.It touches only stable API, so it compiles and runs against pre-fix source, where it fails:
Post-fix it passes. The two existing
decode_leaf_roottests are updated for the new signature, and one of them now pins that a fresh registration yields exactly one entry — the phantom-epoch-0 guard.Full
-p quil-execution: 1091/1091.Still open in #589, deliberately not in this PR. The other bullet — ρ_N derivation failing because the anchored global frame is unavailable while the node is momentarily behind — remains live on the
!locally_verifiedpath, which still runs state-dependent validation and drops silently. That one is a design question, not a decoder bug: whether a certificate-only replica should defer and retry rather than drop. Worth deciding separately.