linera-io/linera-protocol · error · ChainError

EquivocationProofNoLockViolation

EquivocationProofNoLockViolation

Error message

Equivocation proof does not violate the lock claim

What it means

Thrown by EquivocationProof::check for a LockViolation proof when the vote pair does not actually contradict the unlocking-round claim (ChainError::EquivocationProofNoLockViolation). The validation vote's claim covers only rounds in [unlocking_round, validated_round); the confirmation contradicts it only if confirmed_round < validated_round and confirmed_round >= unlocking_round (None means 0). A confirmation at or after validated_round is a legitimate later switch, so such a pair is not a fault.

Source

Thrown at linera-chain/src/justification/mod.rs:447

                    confirmed_block_hash != validated_block_hash,
                    ChainError::EquivocationProofSameBlock
                );
                // The two votes must concern the same height on the same chain; otherwise there
                // is no lock relationship between them — a validator may freely confirm a block at
                // one height and validate a different one at another height or on another chain.
                ensure!(
                    confirmed_header.chain_id == validated_header.chain_id
                        && confirmed_header.height == validated_header.height,
                    ChainError::EquivocationProofDifferentChainOrHeight
                );
                // The unlocking-round claim — "no confirmation of a different block in any round
                // at or above the unlocking round" — is made while validating in
                // `validated_round`, so it covers only the rounds the voter had already acted in:
                // the window `[unlocking_round, validated_round)` (an unlocking round of `None`
                // means `0`). The confirmation contradicts it only if it falls in that window,
                // i.e. `unlocking_round ≤ confirmed_round < validated_round`. A confirmation at or
                // after `validated_round` is a legitimate later switch, not a violation.
                ensure!(
                    *confirmed_round < *validated_round
                        && validated_unlocking_round
                            .is_none_or(|unlocking_round| *confirmed_round >= unlocking_round),
                    ChainError::EquivocationProofNoLockViolation
                );
                let confirmed = VoteValue(
                    confirmed_block_hash,
                    *confirmed_round,
                    CertificateKind::Confirmed,
                    None,
                    *confirmed_attested,
                    *confirmed_commitment,
                );
                confirmed_signature.check(&confirmed, *validator)?;
                let validated = VoteValue(
                    validated_block_hash,
                    *validated_round,
                    CertificateKind::Validated,

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Before submitting, assert confirmed_round < validated_round and (validated_unlocking_round is None or confirmed_round >= validated_unlocking_round).
  2. If confirmed_round >= validated_round, discard the pair — it is honest behavior, not a violation.
  3. If the unlocking round excludes the confirmation, select a different confirmation vote inside the window.
  4. Log the three round values when the check fails to quickly classify the pairing bug.

Example fix

// before
let proof = LockViolation { confirmed_round: r3, validated_round: r1, .. };
proof.check(&committee)?; // Err: confirmation after validation is legal

// after
ensure!(*confirmed_round < *validated_round, "confirmation is a later switch, not a violation");
if let Some(u) = validated_unlocking_round {
    ensure!(*confirmed_round >= *u, "confirmation predates the locked window");
}
let proof = LockViolation { .. };
Defensive patterns

Strategy: validation

Validate before calling

ensure!(confirmed_round < validated_round, "confirmation after validation is legal");
if let Some(u) = validated_unlocking_round {
    ensure!(confirmed_round >= u, "confirmation predates the locked window");
}

Try / catch

match proof.check(committee) {
    Err(ChainError::EquivocationProofNoLockViolation) => {
        // the vote pair is compatible: no fault, drop the proof
        Ok(())
    }
    other => other,
}

Prevention

When it happens

Trigger: Submitting a LockViolation proof where confirmed_round >= validated_round (the confirmation came after the validation — allowed), or where the validation vote signed an unlocking_round greater than confirmed_round (confirmation predates the locked window). Produced by pairing two honest votes in the wrong temporal order.

Common situations: Evidence collectors pairing a validator's early confirmation with a later validation without comparing rounds; misunderstanding of the locking window after protocol changes; tests constructing proofs from vote records without round-order assertions.

Related errors


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