Skip to content

feat(sentinel): track unpublished and orphaned checkpoint proposals - #362

Open
spalladino wants to merge 3 commits into
mainfrom
spl/sentinel-unpublished-checkpoint-status
Open

spalladino wants to merge 3 commits into
mainfrom
spl/sentinel-unpublished-checkpoint-status

Conversation

@spalladino

@spalladino spalladino commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

What

The sentinel records checkpoint-valid for a slot when this node re-executed the slot's checkpoint proposal as valid but no checkpoint for the slot reached L1 (case 5). That one status covers two different situations, and this PR splits them apart:

  • checkpoint-unpublished (5a): the proposal was valid, its parent is on L1, and a quorum of the committee attested. The proposer had everything it needed and still did not land it. This is the proposer's fault.
  • checkpoint-orphaned (5b): the proposal was valid but could not land through no fault of its proposer. Either the parent it built on never landed on L1, or an earlier slot landed a checkpoint with the proposal's number after the proposer had built on the previous one. The first is the proposer pipelining cascade: the proposer for slot N+1 builds on slot N's gossiped proposal, so if N never lands, N+1 cannot land either. The second happens when slot N's proposal reaches the N+1 proposer too late to build on.

checkpoint-valid stays as the fallback when neither can be decided.

Slashing is unchanged. Neither new status counts toward missedProposals, epoch performance or inactivity. Like checkpoint-valid, they add only to the proposer's slot total. This PR records the signal in history, stats, RPC and logs, so that a later change can act on it.

Classification rule

This applies only when the slot has no L1 checkpoint and the tracker outcome is valid. The checks run in order, and the first match wins:

  1. If the slot had a proposal equivocation, or the tracker record has no checkpointNumber or lastArchiveRoot, the status stays checkpoint-valid.
  2. The sentinel looks up the parent checkpoint (checkpointNumber - 1) with archiver.getCheckpointData({ number }). If it is missing, the status is checkpoint-orphaned (parent-not-on-l1). If its archive root differs from the proposal's lastArchiveRoot, the status is checkpoint-orphaned (parent-hash-mismatch). The first checkpoint builds on genesis, which always counts as landed.
  3. The sentinel counts the distinct committee members among the slot's P2P attestations, and looks up the checkpoint that holds the proposal's own number. If that checkpoint came from an earlier slot, the proposal could not land once it did. This is the case the sequencer discards its own pipelined work for as unexpected-parent-appeared, and the sentinel uses the same reason. With a quorum of attestations the status is checkpoint-orphaned. Below quorum it stays checkpoint-valid.
  4. If the count is below computeQuorum(committee.length), the status stays checkpoint-valid: the attestors are at fault, and they are already tagged attestation-missed. The proposer's own attestation counts, as it does in the sequencer. collectOwnAttestations adds the proposer's attestations to the local pool, and collectAttestations counts the whole pool against computeQuorum.
  5. Otherwise the status is checkpoint-unpublished. The sentinel logs it at info, with the proposer, slot, checkpoint number, attestation count and quorum.

A checkpoint from a later slot that holds the proposal's number does not exempt the proposer: that slot only got the number because this proposer failed to land it.

Orphaned slots are logged at verbose, with the reason and what it means. For unexpected-parent-appeared, the log also carries the slot that took the number, the attestation count and the quorum.

Non-signers are tagged attestation-missed for both new statuses, as they are for checkpoint-valid, with one exception. When an earlier slot took the proposal's number, no one is tagged, with or without quorum. A validator that already held the earlier slot's blocks rejects the proposal's blocks as conflicting (block_number_already_exists), and is right to. The existing guards for an invalid outcome and for equivocation still apply.

Plumbing

  • CheckpointReexecutionTracker.recordOutcome takes an optional trailing lastArchiveRoot. The new getRecordForSlot(slot) returns the full record for a slot. getOutcomeForSlot is unchanged.
  • Both production call sites now pass checkpointHeader.lastArchiveRoot: the proposer's own proposal, through recordOwnCheckpointProposalAsValid, and ProposalHandler.handleCheckpointProposal. The existing rules that stop a record from being overwritten are unchanged.
  • Both lookups by checkpoint number use getCheckpointData, which reads the header and archive without loading the checkpoint's blocks.
  • ValidatorStatusInSlot and its zod schema gain both values. The sentinel store encodes them as 9 and 10. SCHEMA_VERSION is not bumped, because the new codes are compatible with stored history.
  • computeStatsForValidator now counts both statuses when it picks lastProposal.

e2e: sentinel_status_slash.parallel.test.ts

That test asserts that a node broadcasting an invalid proposal records checkpoint-valid for its own slot, because it records the archive it computed locally. It should still pass under the new rule. Honest validators reject the broadcast proposal and do not attest, so the malicious node sees only its own attestation: 1 of 6, against a quorum of 5. The quorum check fails, so the status stays checkpoint-valid. CI will run it.

Follow-ups

  • Add an inactivity check on the proposer side that counts checkpoint-unpublished.
  • Check what happens to proposer N+2 in a cascade: whether the sequencer's pipeline depth guard skips it, and whether it gets recorded as blocks-missed.

Fixes A-2240

Refine the sentinel's checkpoint-valid status (a valid proposal whose checkpoint never reached L1) into checkpoint-unpublished, when the parent is on L1 and the proposal reached quorum, and checkpoint-orphaned, when the parent never landed or an earlier slot took the checkpoint position. The re-execution tracker now records each proposal's parent archive root so the sentinel can check it against L1. Both statuses are neutral for the proposer: slashing is unchanged.
…ken by an earlier slot

A proposer whose node missed the previous slot's gossiped proposal and built on a stale parent is at fault. Drop the sibling lookup from the classifier, so checkpoint-orphaned now means only that the parent the proposal built on never landed on L1.
…ot took

When an earlier slot lands a checkpoint with the proposal's number after the proposer built on the previous one, the
proposal can no longer land. The proposer is not at fault: the earlier proposal reached it too late to build on. With a
quorum of attestations the slot is now `checkpoint-orphaned` with reason `unexpected-parent-appeared`, the reason the
sequencer gives when it discards its own pipelined checkpoint for this cause. Below quorum it stays `checkpoint-valid`.

In both cases non-signers are no longer tagged `attestation-missed`: a validator that already held the earlier slot's
blocks rejects the proposal's blocks as conflicting, and is right to.

A later slot landing the same number still leaves the proposer `checkpoint-unpublished`. Orphaned logs now name the
reason and explain it. The by-number lookups use `getCheckpointData`, which skips loading the checkpoint's blocks.
Comment on lines +663 to +665
if (this.reexecutionTracker.hasEquivocation(slot)) {
return { status: 'checkpoint-valid', reason: 'equivocation' };
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equivocation is tracked on a separate slasher

@spalladino
spalladino marked this pull request as ready for review October 2, 2026 20:53
@spalladino
spalladino requested a review from alexghr as a code owner October 2, 2026 20:53

This branch has not been deployed

No deployments
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