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
- 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
- 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
- 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
- Never hand-assemble BlockProposal; use the constructors
- Round-trip proposals through serialization in tests to catch dropped optional fields
- Run check_invariants() before every send
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- The new proposal's round must be greater than the original's
- Block proposal has size {0} which is too large
- Protocol error within chain client: A quorum voted with an u
- Retry loop exited unexpectedly: {result:?}
- ProcessDeposit not committed: {other:?}
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/81bc071abc4b349c.
Report an issue: GitHub.