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

  1. 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)
  2. 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

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


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