apply_confirmed_block is public and unguarded: make certificate verification a precondition in the type

Author: ma2bdCreated Aug 11, 2026Updated Aug 11, 2026
Labelsenhancement

Surfaced while auditing the safety proof (#6685, layer 0).

The gap

manager::proof::commit::TipAdvancesOnlyOnValidCertificate states that a correct validator's tip advances only for a block carried by a ConfirmedBlockCertificate that has passed check against the committee of the block's epoch. That is true today, but nothing enforces it:

rust
pub async fn apply_confirmed_block(
    &mut self,
    block: &ConfirmedBlock,     // the certified *value* — no signatures
    local_time: Timestamp,
    tracked: Option<&ChainIdSet>,
) -> Result<BTreeSet<StreamId>, ChainError>

It is pub, on ChainStateView, which linera-chain re-exports — and linera-chain is published. It takes the certified value and no Committee, so it cannot verify anything: the entire guarantee rests on callers doing the check first. The lemma's enumeration ("reached only through process_confirmed_block, whose every non-early-return path first evaluates certificate.check(&committee)?") holds by current usage, not by visibility.

pub(crate) is not available — linera-core calls it from ChainWorkerState::execute_contiguous_block.

Proposed fix: a minted token

Make check return unforgeable evidence, and require it:

rust
/// Evidence that a certificate's signatures were checked against a committee.
pub struct Checked(CryptoHash);          // private field: only `check` can mint one

impl ConfirmedBlockCertificate {
    pub fn check(&self, committee: &Committee) -> Result<Checked, ChainError>;
}

impl ChainStateView<C> {
    pub async fn apply_confirmed_block(&mut self, block: &ConfirmedBlock, checked: Checked, …) {
        ensure!(checked.0 == block.inner().hash(), …);
        …
    }
}

Properties:

  • Unforgeable outside the crate — the field is private, so only check produces one.
  • Bound to the block, not merely to "some certificate was checked somewhere": the hash comparison rules out checking certificate A and applying block B.
  • Cheap — one hash comparison. Re-verifying the certificate inside apply_confirmed_block would be the obvious alternative but pays N secp256k1 verifications twice per block on the hot path.

It threads cleanly: mint in process_confirmed_block right after the existing certificate.check(&committee)?, pass down through execute_contiguous_block and execute_block_with_checkpoint_restore, both of which already hold the certificate.

Cost

  • ConfirmedBlockCertificate::check changes return type from Result<(), _> to Result<Checked, _> — a small break for in-workspace callers.
  • Three apply_confirmed_block call sites in linera-chain's unit tests need to mint a token; a #[cfg(with_testing)] constructor covers that.

Alternatives considered

  • Take the certificate plus the committee and verify inside — correct, but duplicates signature verification on the hot path.
  • Rename to apply_verified_confirmed_block — zero cost, but documentation by another name; it does not stop anything.
  • Leave it, keep the documented fragility note — where things stand now. Defensible: only linera-core calls it, and the note is in the spec. The argument for fixing it is that linera-chain is published, so the footgun is reachable from outside the workspace, and the proof would then rest on the type rather than on a grep.

Links

  • #6685 — the audit that surfaced it; the fragility note lives in TipAdvancesOnlyOnValidCertificate.
  • #6674 — the specification.

Source: linera-io/linera-protocol