Skip to content

Re-certify revoked-epoch blocks via their descendants#6601

Open
afck wants to merge 9 commits into
mainfrom
afck/recertify-descendants
Open

Re-certify revoked-epoch blocks via their descendants#6601
afck wants to merge 9 commits into
mainfrom
afck/recertify-descendants

Conversation

@afck

@afck afck commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

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-core covering worker rejection and acceptance of revoked-epoch blocks and the client's descendant-based recovery.

Release Plan

  • Nothing to do / These changes follow the usual release cycle.

Links

afck added 9 commits July 13, 2026 10:46
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.
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