Re-certify revoked-epoch blocks via their descendants#6601
Open
afck wants to merge 9 commits into
Open
Conversation
A quorum signature from a revoked epoch is no longer a sufficient basis for trusting a confirmed block: the chain worker now rejects such a certificate with the new EpochRevoked error unless the block is vouched for by something already trusted — a checkpoint trust mark, an earlier acceptance of the same block, or an accepted child block that commits to it via its previous block hash. This lets a client convince a validator of a revoked-epoch block by uploading the block's descendants in decreasing height order: each accepted descendant re-certifies its parent. The client-side logic for doing so is left for a follow-up.
When a block's epoch has been revoked, its certificate alone no longer establishes trust; a trusted descendant must vouch for it via the previous-block-hash chain. This adds the client-side recovery logic in both directions: - Download: when the local worker rejects a certificate with EpochRevoked, the client collects the block's descendants — from local storage or from validators — up to the first block in a non-revoked epoch, and preprocesses them in decreasing height order before retrying. - Upload: when a validator rejects a pushed certificate with EpochRevoked, the updater reads the block's descendants from local storage up to the first trusted-epoch block and pushes them in decreasing height order before retrying. The explicit sync_validator API recovers by pushing the whole remaining suffix in decreasing height order, since the validator's revocation view may be ahead of the client's. Client-side certificate checks no longer treat a revoked epoch (CheckCertificateResult::OldEpoch) as a hard error: the local worker is the arbiter, accepting such blocks only when a trusted block vouches for them.
The client-side certificate gate no longer consults epoch revocation at all: it resolves the committee of the certificate's own epoch from the admin chain's event stream — which works even for revoked epochs, the same way the worker does — and verifies the signatures against it. Whether a valid certificate from a revoked epoch is still a sufficient basis for trust is decided in exactly one place, the worker, which only accepts such a block if an already-trusted block vouches for it. This removes the CheckCertificateResult enum: the only remaining recoverable condition, an epoch not known locally yet, is reported as CommitteeSynchronizationError, and callers that can recover match on that error and catch up on the admin chain. The now-unused CommitteeDeprecationError variant is removed, and revoked-epoch certificates get their signatures verified client-side again instead of being passed through unchecked.
Instead of tracking a committee-known flag for every downloaded sender certificate, partition the batch into the certificates that can be verified now and the heights of those whose epoch is not known locally yet. The latter are skipped without being re-downloaded, until a future sync cycle after the local view of the admin chain has caught up.
In the updater, stop consulting local revocation state when re-certifying a rejected block for a validator: what matters is the epoch the validator itself reported as revoked. Read descendants up to the first block from a later epoch and push those; if the validator revoked that epoch too, the recursive push extends the walk one epoch further. In sync_validator, fold the rejected certificate into the reverse push instead of re-sending it separately.
Recursing once per consecutively revoked epoch nests boxed futures and deepens the poll stack. Instead, keep a stack of certificates to send: when the validator rejects the top entry because its epoch is revoked, stack its descendants (up to the first later-epoch block) on top of it, so every rejected block is preceded by the child vouching for it. Each certificate is expanded at most once.
Once an epoch is revoked on the admin chain, its committee must not sign anything new. The chain worker now checks this in all four signature-creation paths: block proposals, validated blocks, leader-timeout votes, and fallback votes. A chain that failed to migrate to a newer epoch before the revocation is frozen.
`preprocess_descendants` tried validators one at a time in a `for node in nodes` loop, so a slow or faulty validator could stall the whole re-certification walk. Race them with `communicate_concurrently` instead; a validator that returns nothing counts as a failure so the others take over.
The doc comment recounted the past cold-sync hang; state the invariant the test verifies instead.
This was referenced Jul 13, 2026
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.
Motivation
Once an epoch is revoked, its committee's signatures are no longer trustworthy: a certificate from a revoked epoch can't be verified on its own anymore. But nodes still need to accept old blocks — a sender's block with an undelivered message, for example — long after the epoch that certified it is gone. That is why epoch revocation is currently not usable.
Proposal
Accept a confirmed block from a revoked epoch only if something the node already trusts vouches for it. A block certified in a live epoch commits to its parent's hash, so its descendants re-certify it: the worker records the hash of a block it trusts but hasn't seen yet, and accepts a certificate for that hash regardless of its epoch.
Clients re-certify accordingly: when a validator rejects a block as belonging to a revoked epoch, the client downloads that block's descendants (racing validators, so a single faulty one can't stall the sync) and submits them first. Validators also refuse to sign new blocks in a revoked epoch.
Test Plan
CI, plus new unit tests in
linera-corecovering worker rejection and acceptance of revoked-epoch blocks and the client's descendant-based recovery.Release Plan
Links