linera-io/linera-protocol · error · ChainError
HasIncompatibleConfirmedVote
HasIncompatibleConfirmedVote
Error message
Already voted to confirm a different block for height {0:?} at round number {1:?} What it means
Slashing-safety guard in check_proposed_block: once the manager has voted to confirm a block, it may only vote to validate a different block if the proposal carries an original validated-block certificate from a strictly later round (which unlocks the switch), or, when re-validating the very block it confirmed, a certificate at least as recent as the confirmation. Fast-variant original proposals only pass if the confirmation was in the fast round for the same block.
Source
Thrown at linera-chain/src/manager.rs:354
locking_block.round() < new_round,
ChainError::MustBeNewerThanLockingBlock(new_block.height, locking_block.round())
);
}
// If we have voted to confirm a block, we may only vote to validate a *different* block
// if a validated block certificate justifies it from a round strictly after our
// confirmation. The validation vote will then sign the unlocking round `certificate.round`,
// and since our confirmation is in an earlier round, the claim "I have not voted to confirm
// a different block in any round at or above the unlocking round" stays truthful.
//
// Re-validating the very block we confirmed is also allowed, but the certificate must
// still be at least as recent as our confirmation. The unlocking round only constrains
// switching blocks, yet the round we sign is a claim about *ourselves*: an earlier
// confirmation of a different block could fall at or above an older certificate's round
// and turn the claim into a lie we could be slashed for. Our confirmed vote sits in the
// highest round we ever confirmed in, so `vote.round <= certificate.round` guarantees no
// different-block confirmation lies in the unlocking window `[certificate.round, round)`.
if let Some(vote) = self.confirmed_vote() {
ensure!(
match proposal.original_proposal.as_ref() {
None => false,
Some(OriginalProposal::Regular { certificate }) =>
if vote.value().matches_proposed_block(new_block) {
vote.round <= certificate.round
} else {
vote.round < certificate.round
},
Some(OriginalProposal::Fast(_)) => {
vote.round.is_fast() && vote.value().matches_proposed_block(new_block)
}
},
ChainError::HasIncompatibleConfirmedVote(new_block.height, vote.round)
);
}
Ok(Outcome::Accept)
}
View on GitHub (pinned to 6c226ddcb3)
Solutions
- Attach an OriginalProposal::Regular whose validated certificate round is strictly greater than the confirmed vote's round when switching blocks (>= when re-proposing the same block)
- Do not submit conflicting proposals after a confirmation at that height — wait for finality and propose at the next height
Example fix
// before: conflicting re-proposal without justification
let proposal = BlockProposal::regular(Round::MultiLeader(1), block); // HasIncompatibleConfirmedVote
// after: carry the validated certificate that unlocks the switch
let proposal = BlockProposal::new_regular_with_certificate(
Round::MultiLeader(1),
block,
validated_cert, // OriginalProposal::Regular { certificate }: round must exceed the confirmed vote's
); Defensive patterns
Strategy: validation
Validate before calling
// Before submitting, check the confirmation constraint yourself.
let info = client.chain_info(chain_id).await?;
if let Some(vote) = info.manager.confirmed_vote() {
let same_block = vote.value().matches_proposed_block(&block);
let cert_round = match &proposal.original_proposal {
Some(OriginalProposal::Regular { certificate }) => Some(certificate.round),
_ => None,
};
let ok = cert_round.is_some_and(|r| if same_block { vote.round <= r } else { vote.round < r });
if !ok {
return Err(ClientError::WouldConflictWithConfirmedVote);
}
} Type guard
fn is_incompatible_confirmed_vote(e: &ChainError) -> bool {
matches!(e, ChainError::HasIncompatibleConfirmedVote(_, _))
} Prevention
- Never submit a conflicting block at a height where a confirmation already exists
- Always attach the unlocking validated certificate (OriginalProposal::Regular) when re-proposing after confirmations
- Remember fast-variant original proposals only pass for the exact fast-confirmed block
When it happens
Trigger: Submitting a conflicting block proposal without proposal.original_proposal (None), or with an OriginalProposal::Regular certificate whose round is <= the confirmed vote's round for a different block (or < for the same block), after manager.confirmed_vote() is set.
Common situations: Leader re-proposes a conflicting block after another block was already confirmed at that height; client forgot to attach the original proposal with the unlocking validated certificate when re-proposing; mixing fast and regular proposals across a confirmation.
Related errors
- MustBeNewerThanLockingBlock
- no signer found for owner ${owner}
- CannotRejectMessage
- Certificate justification commitment does not match its just
- Certificate unlocking round does not match the top of its ju
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/9b798320ef590d22.
Report an issue: GitHub.