feat(sentinel): track unpublished and orphaned checkpoint proposals - #362
Open
spalladino wants to merge 3 commits into
Open
spalladino wants to merge 3 commits into
spalladino wants to merge 3 commits into
Conversation
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.
spalladino
commented
Oct 2, 2026
Comment on lines
+663
to
+665
| if (this.reexecutionTracker.hasEquivocation(slot)) { | ||
| return { status: 'checkpoint-valid', reason: 'equivocation' }; | ||
| } |
Collaborator
Author
There was a problem hiding this comment.
Equivocation is tracked on a separate slasher
spalladino
marked this pull request as ready for review
October 2, 2026 20:53
This branch has not been deployed
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.
What
The sentinel records
checkpoint-validfor 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-validstays as the fallback when neither can be decided.Slashing is unchanged. Neither new status counts toward
missedProposals, epoch performance or inactivity. Likecheckpoint-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:checkpointNumberorlastArchiveRoot, the status stayscheckpoint-valid.checkpointNumber - 1) witharchiver.getCheckpointData({ number }). If it is missing, the status ischeckpoint-orphaned(parent-not-on-l1). If its archive root differs from the proposal'slastArchiveRoot, the status ischeckpoint-orphaned(parent-hash-mismatch). The first checkpoint builds on genesis, which always counts as landed.unexpected-parent-appeared, and the sentinel uses the same reason. With a quorum of attestations the status ischeckpoint-orphaned. Below quorum it stayscheckpoint-valid.computeQuorum(committee.length), the status stayscheckpoint-valid: the attestors are at fault, and they are already taggedattestation-missed. The proposer's own attestation counts, as it does in the sequencer.collectOwnAttestationsadds the proposer's attestations to the local pool, andcollectAttestationscounts the whole pool againstcomputeQuorum.checkpoint-unpublished. The sentinel logs it atinfo, 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. Forunexpected-parent-appeared, the log also carries the slot that took the number, the attestation count and the quorum.Non-signers are tagged
attestation-missedfor both new statuses, as they are forcheckpoint-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 aninvalidoutcome and for equivocation still apply.Plumbing
CheckpointReexecutionTracker.recordOutcometakes an optional trailinglastArchiveRoot. The newgetRecordForSlot(slot)returns the full record for a slot.getOutcomeForSlotis unchanged.checkpointHeader.lastArchiveRoot: the proposer's own proposal, throughrecordOwnCheckpointProposalAsValid, andProposalHandler.handleCheckpointProposal. The existing rules that stop a record from being overwritten are unchanged.getCheckpointData, which reads the header and archive without loading the checkpoint's blocks.ValidatorStatusInSlotand its zod schema gain both values. The sentinel store encodes them as 9 and 10.SCHEMA_VERSIONis not bumped, because the new codes are compatible with stored history.computeStatsForValidatornow counts both statuses when it pickslastProposal.e2e:
sentinel_status_slash.parallel.test.tsThat test asserts that a node broadcasting an invalid proposal records
checkpoint-validfor 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 stayscheckpoint-valid. CI will run it.Follow-ups
checkpoint-unpublished.blocks-missed.Fixes A-2240