{"record":{"id":"45bf5ab95ea04933","repo":"linera-io/linera-protocol","slug":"equivocationproofnolockviolation","errorCode":"EquivocationProofNoLockViolation","errorMessage":"Equivocation proof does not violate the lock claim","messagePattern":"Equivocation proof does not violate the lock claim","errorType":"validation","errorClass":"ChainError","httpStatus":null,"severity":"error","filePath":"linera-chain/src/justification/mod.rs","lineNumber":447,"sourceCode":"                    confirmed_block_hash != validated_block_hash,\n                    ChainError::EquivocationProofSameBlock\n                );\n                // The two votes must concern the same height on the same chain; otherwise there\n                // is no lock relationship between them — a validator may freely confirm a block at\n                // one height and validate a different one at another height or on another chain.\n                ensure!(\n                    confirmed_header.chain_id == validated_header.chain_id\n                        && confirmed_header.height == validated_header.height,\n                    ChainError::EquivocationProofDifferentChainOrHeight\n                );\n                // The unlocking-round claim — \"no confirmation of a different block in any round\n                // at or above the unlocking round\" — is made while validating in\n                // `validated_round`, so it covers only the rounds the voter had already acted in:\n                // the window `[unlocking_round, validated_round)` (an unlocking round of `None`\n                // means `0`). The confirmation contradicts it only if it falls in that window,\n                // i.e. `unlocking_round ≤ confirmed_round < validated_round`. A confirmation at or\n                // after `validated_round` is a legitimate later switch, not a violation.\n                ensure!(\n                    *confirmed_round < *validated_round\n                        && validated_unlocking_round\n                            .is_none_or(|unlocking_round| *confirmed_round >= unlocking_round),\n                    ChainError::EquivocationProofNoLockViolation\n                );\n                let confirmed = VoteValue(\n                    confirmed_block_hash,\n                    *confirmed_round,\n                    CertificateKind::Confirmed,\n                    None,\n                    *confirmed_attested,\n                    *confirmed_commitment,\n                );\n                confirmed_signature.check(&confirmed, *validator)?;\n                let validated = VoteValue(\n                    validated_block_hash,\n                    *validated_round,\n                    CertificateKind::Validated,","sourceCodeStart":429,"sourceCodeEnd":465,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/justification/mod.rs#L429-L465","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Before submitting, assert confirmed_round < validated_round and (validated_unlocking_round is None or confirmed_round >= validated_unlocking_round).","If confirmed_round >= validated_round, discard the pair — it is honest behavior, not a violation.","If the unlocking round excludes the confirmation, select a different confirmation vote inside the window.","Log the three round values when the check fails to quickly classify the pairing bug."],"exampleFix":"// before\nlet proof = LockViolation { confirmed_round: r3, validated_round: r1, .. };\nproof.check(&committee)?; // Err: confirmation after validation is legal\n\n// after\nensure!(*confirmed_round < *validated_round, \"confirmation is a later switch, not a violation\");\nif let Some(u) = validated_unlocking_round {\n    ensure!(*confirmed_round >= *u, \"confirmation predates the locked window\");\n}\nlet proof = LockViolation { .. };","handlingStrategy":"validation","validationCode":"ensure!(confirmed_round < validated_round, \"confirmation after validation is legal\");\nif let Some(u) = validated_unlocking_round {\n    ensure!(confirmed_round >= u, \"confirmation predates the locked window\");\n}","typeGuard":null,"tryCatchPattern":"match proof.check(committee) {\n    Err(ChainError::EquivocationProofNoLockViolation) => {\n        // the vote pair is compatible: no fault, drop the proof\n        Ok(())\n    }\n    other => other,\n}","preventionTips":["Check the round window [unlocking_round, validated_round) contains the confirmation before reporting.","Order the pair so the validation vote is the later round.","Log confirmed_round, validated_round and unlocking_round on failure."],"tags":["consensus","equivocation","locking","rounds","proof-validation"],"backgroundTag":"invalid-equivocation-proof","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}