linera-io/linera-protocol · warning

Must contain a validation certificate if and only if it cont

Error message

Must contain a validation certificate if and only if it contains the execution outcome from a previous round

What it means

check_invariants (linera-chain/src/data_types/mod.rs:1003-1008) requires that a proposal carries the execution outcome from a previous round if and only if it carries an OriginalProposal::Regular validation certificate. The combinations (no original, some outcome), (Fast original, some outcome) and (Regular original, no outcome) are all rejected as structurally invalid — the outcome is exactly what lets validators skip re-execution when retrying a validated block, so it must be paired with the certificate that justifies it.

Source

Thrown at linera-chain/src/data_types/mod.rs:1010

        )
    }

    /// Checks that the original proposal, if present, matches the new one and has a higher round.
    pub fn check_invariants(&self) -> Result<(), &'static str> {
        match (&self.original_proposal, &self.content.outcome) {
            (None, None) => {}
            (Some(OriginalProposal::Fast(_)), None) => ensure!(
                self.content.round > Round::Fast,
                "The new proposal's round must be greater than the original's"
            ),
            (None, Some(_))
            | (Some(OriginalProposal::Fast(_)), Some(_))
            | (Some(OriginalProposal::Regular { .. }), None) => {
                return Err("Must contain a validation certificate if and only if \
                     it contains the execution outcome from a previous round");
            }
            (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
                ensure!(
                    self.content.round > certificate.round,
                    "The new proposal's round must be greater than the original's"
                );
                let block = outcome.clone().with(self.content.block.clone());
                let value = ValidatedBlock::new(block);
                ensure!(
                    certificate.check_value(&value),
                    "Lite certificate must match the given block and execution outcome"
                );
            }
        }
        Ok(())
    }
}

impl LiteVote {
    /// Uses the signing key to create a signed object.
    pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Always build proposals through the constructors — BlockProposal::new (neither field), new_retry_fast (Fast signature, no outcome), new_retry_regular (Regular certificate + outcome) — which keep the pair consistent
  2. If constructing manually, match the shape: Regular retry must set BOTH the lite certificate and the outcome; a fresh or fast-retry proposal must set NEITHER outcome nor Regular certificate
  3. Add a pre-send call to proposal.check_invariants() so the mismatch is caught locally

Example fix

// before: outcome carried without its certificate
let proposal = BlockProposal { content: ProposalContent { block, round, outcome: Some(outcome) },
    original_proposal: None, signature }; // -> invariant error

// after: retry properly via the constructor (certificate + outcome paired)
let proposal = BlockProposal::new_retry_regular(owner, new_round, validated_cert, &signer).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Mirror the pairing rule before sending (data_types/mod.rs:997-1008):
use linera_chain::data_types::OriginalProposal;
fn proposal_shape_valid(p: &BlockProposal) -> bool {
    matches!(
        (&p.original_proposal, &p.content.outcome),
        (None, None)
            | (Some(OriginalProposal::Fast(_)), None)
            | (Some(OriginalProposal::Regular { .. }), Some(_))
    )
}
anyhow::ensure!(proposal_shape_valid(&proposal), "outcome iff Regular certificate");

Type guard

fn is_well_shaped_proposal(p: &BlockProposal) -> bool {
    matches!(
        (&p.original_proposal, &p.content.outcome),
        (None, None)
            | (Some(OriginalProposal::Fast(_)), None)
            | (Some(OriginalProposal::Regular { .. }), Some(_))
    )
}

Try / catch

match result {
    Err(WorkerError::InvalidBlockProposal(msg)) if msg.contains("if and only if") => {
        // structural fix: pair the Regular certificate WITH its outcome (or remove
        // both); then re-sign and resend
    }
    other => other?,
}

Prevention

When it happens

Trigger: Hand-constructing BlockProposal with mismatched original_proposal/outcome fields (e.g., setting outcome but not the certificate, or attaching a Regular certificate without its outcome); deserializing a truncated or foreign proposal; bugs in proposal copy/merge logic that drop one of the pair.

Common situations: Protocol tests assembling proposals field-by-field; serialization round-trips that lose optional fields; forked client code that builds retries differently than the SDK constructors.

Understand the failure class

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/81bc071abc4b349c. Report an issue: GitHub.