{"record":{"id":"04d3182565ce5f8e","repo":"linera-io/linera-protocol","slug":"equivocationproofvalidjustification","errorCode":"EquivocationProofValidJustification","errorMessage":"Equivocation proof's opened justification is a valid quorum","messagePattern":"Equivocation proof's opened justification is a valid quorum","errorType":"validation","errorClass":"ChainError","httpStatus":null,"severity":"error","filePath":"linera-chain/src/justification/mod.rs","lineNumber":589,"sourceCode":"                kind,\n                unlocking_round,\n                first_round,\n                signature,\n                opening,\n            } => {\n                // The signed payload contains the opening's hash, so verifying the signature\n                // binds the validator to exactly this opening.\n                let value = VoteValue(\n                    CryptoHash::new(header),\n                    *round,\n                    *kind,\n                    *unlocking_round,\n                    *first_round,\n                    Some(opening.commitment()),\n                );\n                signature.check(&value, *validator)?;\n                // The opening must be one that no honest voter could have cited.\n                ensure!(\n                    check_cited_quorum(header, *round, *kind, *unlocking_round, opening, committee)\n                        .is_err(),\n                    ChainError::EquivocationProofValidJustification\n                );\n                Ok(())\n            }\n        }\n    }\n}\n\n/// Checks that `opening` is a quorum an honest voter could cite from a vote of the given kind,\n/// round and unlocking round for the block with the given header: it validates the same block, in\n/// the round the vote's payload grounds on, and its signatures form a genuine quorum of\n/// `committee` over the reconstructed `ValidatedBlock` payload. These are exactly the checks a\n/// voter performs before signing the opening's commitment, so their failure on a signed opening\n/// convicts the signer.\nfn check_cited_quorum(\n    header: &BlockHeader,","sourceCodeStart":571,"sourceCodeEnd":607,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/justification/mod.rs#L571-L607","documentation":"Thrown by EquivocationProof::check for an InvalidJustification proof when the cited opening (CommittedQuorum) actually passes check_cited_quorum — i.e. it is a justification an honest voter could legitimately have cited (ChainError::EquivocationProofValidJustification). The proof's thesis is that the validator signed a commitment to an invalid opening; if the opening is valid, no fault is attributable and the proof is malformed. The signature itself is verified first, binding the validator to exactly this opening, before validity is assessed.","triggerScenarios":"Submitting an EquivocationProof::InvalidJustification whose opening is a genuine quorum: right block hash, correctly grounded round relationship, valid quorum signatures for the committee passed to check. Common when the committee used for verification is the wrong epoch's (making an invalid opening look valid), or when the proof was built from an opening that was in fact correct.","commonSituations":"Epoch transitions: verifying a proof against a committee where the opening's signers happen to hold weight, or where threshold interpretation differs; misimplemented fault detectors flagging honest voters; openings whose round/kind relationship satisfies the rules by accident of construction.","solutions":["Verify the proof against the committee of the epoch in which the vote was cast (the same committee the opening's signatures are checked against).","Before reporting, re-run check_cited_quorum on the opening and require it to fail for the specific reason expected (hash mismatch, round grounding, or quorum failure).","If the opening is valid, withdraw the fault report — the validator did nothing wrong.","Add a regression test that builds a genuinely invalid opening (e.g. wrong value hash) to confirm the detector path."],"exampleFix":"// before\nlet proof = InvalidJustification { opening: valid_quorum.clone(), .. };\nproof.check(&committee)?; // Err: opening is valid, no fault\n\n// after\nif check_cited_quorum(header, round, kind, unlocking_round, &opening, &committee).is_ok() {\n    return Ok(()); // not a fault: opening is honest evidence\n}\nlet proof = InvalidJustification { opening, .. };","handlingStrategy":"validation","validationCode":"// only report a fault when the opening is genuinely invalid\nif check_cited_quorum(header, round, kind, unlocking_round, &opening, &epoch_committee).is_ok() {\n    return Ok(()); // opening is honest: no fault to report\n}","typeGuard":null,"tryCatchPattern":"match proof.check(committee) {\n    Err(ChainError::EquivocationProofValidJustification) => {\n        // withdraw the report; possibly wrong committee — retry with the vote's epoch committee\n        let epoch_committee = committee_of_epoch(vote_epoch)?;\n        proof.check(&epoch_committee)\n    }\n    other => other,\n}","preventionTips":["Verify proofs against the committee of the epoch the vote was cast in.","Require check_cited_quorum to fail for the expected reason before reporting a fault.","Test detectors with both valid and invalid openings."],"tags":["consensus","equivocation","justification","fault-attribution"],"backgroundTag":"invalid-equivocation-proof","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}