Context
Consensus state can carry a pending_checkpoint that will later be used to finalize a checkpoint artifact. The intended invariant is that each checkpoint's digest is the hash of its embedded data, because trustless recovery compares finalized checkpoint hashes against the checkpoint bytes.
Claim
Decoded checkpoints carry data and digest as independent fields, and ConsensusState.pending_checkpoint decoding accepts the embedded digest without checking it matches the embedded data. The finalizer later uses that stored digest directly as the epoch-final block's checkpoint hash.
A malicious unchecked checkpoint artifact supplier or local decoded-state source can provide consensus state containing a pending_checkpoint whose stored digest differs from its data hash, and finalization later copies that unvalidated digest into the checkpoint hash so the exported checkpoint artifact fails trustless verification. A trustlessly verified checkpoint is not an arbitrary edit surface unless the signed checkpoint data itself already contains the malformed embedded checkpoint.
Flow
The path is reachable through consensus-state decoding when pending_checkpoint is present. It requires malformed serialized state, an explicitly trusted checkpoint loaded without verification headers, or canonical signed checkpoint data that already contains pending_checkpoint.data = X and pending_checkpoint.digest = Y where Y != sha256(X), followed by finalization that consumes the pending checkpoint. Startup verifies the outer checkpoint only when finalized headers are supplied.
Impact
Honest proposers that start from or otherwise decode such malformed state can finalize a header committing Y, while the stored/exported checkpoint artifact contains X. Future trustless importers reject the checkpoint because the signed header hash does not match the checkpoint data hash, making that epoch's checkpoint unusable for recovery.
Root Cause
The Checkpoint type stores a redundant digest but decode paths do not rederive and validate it before the finalizer trusts it as the canonical checkpoint hash.
Code
Related Issues/PRs
Related issues cover adjacent checkpoint startup, state-position binding, transition queue, genesis binding, and pending-checkpoint root omission risks.
Fix
Make Checkpoint::read_cfg or Checkpoint::from_ssz_bytes validate digest == sha256(data). Recompute the checkpoint hash from checkpoint.data when constructing aux data and when storing finalized checkpoints. Add tests for malformed embedded pending_checkpoint values in decoded ConsensusState.
Context
Consensus state can carry a
pending_checkpointthat will later be used to finalize a checkpoint artifact. The intended invariant is that each checkpoint's digest is the hash of its embedded data, because trustless recovery compares finalized checkpoint hashes against the checkpoint bytes.Claim
Decoded checkpoints carry
dataanddigestas independent fields, andConsensusState.pending_checkpointdecoding accepts the embedded digest without checking it matches the embedded data. The finalizer later uses that stored digest directly as the epoch-final block's checkpoint hash.A malicious unchecked checkpoint artifact supplier or local decoded-state source can provide consensus state containing a
pending_checkpointwhose stored digest differs from its data hash, and finalization later copies that unvalidated digest into the checkpoint hash so the exported checkpoint artifact fails trustless verification. A trustlessly verified checkpoint is not an arbitrary edit surface unless the signed checkpoint data itself already contains the malformed embedded checkpoint.Flow
The path is reachable through consensus-state decoding when
pending_checkpointis present. It requires malformed serialized state, an explicitly trusted checkpoint loaded without verification headers, or canonical signed checkpoint data that already containspending_checkpoint.data = Xandpending_checkpoint.digest = YwhereY != sha256(X), followed by finalization that consumes the pending checkpoint. Startup verifies the outer checkpoint only when finalized headers are supplied.Impact
Honest proposers that start from or otherwise decode such malformed state can finalize a header committing
Y, while the stored/exported checkpoint artifact containsX. Future trustless importers reject the checkpoint because the signed header hash does not match the checkpoint data hash, making that epoch's checkpoint unusable for recovery.Root Cause
The
Checkpointtype stores a redundant digest but decode paths do not rederive and validate it before the finalizer trusts it as the canonical checkpoint hash.Code
Checkpoint::newdefines the intended data/digest invariant: https://github.com/SeismicSystems/summit/blob/ed2c5c8/types/src/checkpoint.rs#L26.Checkpoint::from_ssz_bytesdecodesdataanddigestindependently without validating their relationship: https://github.com/SeismicSystems/summit/blob/ed2c5c8/types/src/checkpoint.rs#L75.ConsensusState::read_cfgaccepts an embeddedpending_checkpointthroughCheckpoint::read_cfg: https://github.com/SeismicSystems/summit/blob/ed2c5c8/types/src/consensus_state.rs#L870.Checkpoint::read_cfgonly decodes the SSZ payload and returns it: https://github.com/SeismicSystems/summit/blob/ed2c5c8/types/src/checkpoint.rs#L111.checkpoint.digestdirectly as the header checkpoint hash: https://github.com/SeismicSystems/summit/blob/ed2c5c8/finalizer/src/actor.rs#L932.sha256(checkpoint.data): https://github.com/SeismicSystems/summit/blob/ed2c5c8/types/src/checkpoint.rs#L263.Related Issues/PRs
Related issues cover adjacent checkpoint startup, state-position binding, transition queue, genesis binding, and pending-checkpoint root omission risks.
Fix
Make
Checkpoint::read_cfgorCheckpoint::from_ssz_bytesvalidatedigest == sha256(data). Recompute the checkpoint hash fromcheckpoint.datawhen constructing aux data and when storing finalized checkpoints. Add tests for malformed embeddedpending_checkpointvalues in decodedConsensusState.