Skip to content

fix: cache every leaf-root epoch slot, not just the main one - #610

Open
blacks1ne wants to merge 1 commit into
QuilibriumNetwork:v2.1.0.25from
blacks1ne:blacks1ne/fix-leafroot-cache-epoch-window
Open

fix: cache every leaf-root epoch slot, not just the main one#610
blacks1ne wants to merge 1 commit into
QuilibriumNetwork:v2.1.0.25from
blacks1ne:blacks1ne/fix-leafroot-cache-epoch-window

Conversation

@blacks1ne

@blacks1ne blacks1ne commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

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: 932045a3

The bug. A leafroot:LeafRootRegistration vertex carries a rolling three-epoch window — Prev*, the main slot, Next* — which upsert_leaf_root_registration maintains 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:23 records 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_epoch matches an opening against whichever slot holds the epoch being proved, so it sees all three. The prover-registry cache — read through get_leaf_root, which is what the app-shard storage-attestation check calls (frame_validator.rs:902, keyed on epoch_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_frame dropped an honest, committee-certified frame.

The fix. decode_leaf_root returns one entry per populated slot instead of an Option of the main one, and refresh inserts each.

Whether a slot is populated is decided by a new read_u64_be_opt rather than the existing read_u64_be. That is not incidental: read_u64_be returns 0 both for an absent field and for a stored zero, and Prev*/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 at leaf_root_vertex_count for the old meaning.

Tests. leaf_root_cache_covers_every_epoch_slot_like_the_audit_reader builds a real three-slot vertex through upsert_leaf_root_registration (epochs 5/6/7), writes it under the real GLOBAL_INTRINSIC_ADDRESS ++ leaf_root_address(...) key, and drives the real refresh over a real store. It then asserts the cache against leaf_root_registration_for_epoch across 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:

one cache entry per populated epoch slot: left: 1, right: 3

Post-fix it passes. The two existing decode_leaf_root tests 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_verified path, 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.

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
blacks1ne force-pushed the blacks1ne/fix-leafroot-cache-epoch-window branch from d747069 to b2a7383 Compare August 18, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant