{"record":{"id":"15c33f1ca623afe6","repo":"linera-io/linera-protocol","slug":"justification-chain-rounds-must-be-strictly-increa","errorCode":null,"errorMessage":"Justification chain rounds must be strictly increasing","messagePattern":"Justification chain rounds must be strictly increasing","errorType":"validation","errorClass":"ChainError","httpStatus":null,"severity":"error","filePath":"linera-chain/src/justification/mod.rs","lineNumber":219,"sourceCode":"    ///\n    /// Link signatures are deliberately *not* verified here. The quorum built on top of this\n    /// chain signs the chain's commitment, so a single signature check over that quorum attests\n    /// every link below it: each link's voters verified the quorum beneath them before signing\n    /// over its hash. Signing over an invalid quorum is itself an attributable fault, so link\n    /// signatures are only re-checked when auditing a chain to blame the validators that attested\n    /// an invalid link.\n    ///\n    /// The chain length needs no explicit cap: strictly increasing rounds mean a chain of `n`\n    /// links spans `n` distinct rounds, and each link must carry a genuine quorum, so a longer\n    /// chain necessarily reaches a higher round and cannot be inflated cheaply. The observed\n    /// length is recorded as a metric so real contention shows up in monitoring.\n    pub fn verify(&self, value_hash: CryptoHash) -> Result<Option<CryptoHash>, ChainError> {\n        #[cfg(with_metrics)]\n        metrics::JUSTIFICATION_CHAIN_LENGTH\n            .with_label_values(&[])\n            .observe(self.links.len() as f64);\n        for window in self.links.windows(2) {\n            ensure!(\n                window[0].round < window[1].round,\n                ChainError::JustificationRoundsNotIncreasing\n            );\n        }\n        Ok(self.commitment(value_hash))\n    }\n}\n\n/// A confirmed block's *header* together with the justification that makes it self-contained\n/// evidence: the round and quorum of `ConfirmedBlock` votes that finalized it, and the chain of\n/// `ValidatedBlock` quorums for the same block, with its top link in the round the block was\n/// confirmed. Only the header travels, never the block body. The header hashes to the value the\n/// votes sign (`CryptoHash::new(&header)`) and carries the chain ID and height that scope the\n/// fault. This is the shape a `ConfirmedBlockCertificate` reduces to for fault attribution.\n#[derive(Clone, Debug)]\npub struct JustifiedConfirmation {\n    /// The header of the confirmed block. Its hash is what the chain's `ValidatedBlock` and\n    /// `ConfirmedBlock` votes sign (`ValidatedBlock` and `ConfirmedBlock` wrap the same block).","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/justification/mod.rs#L201-L237","documentation":"Thrown by JustificationChain::verify when consecutive links of a justification chain do not have strictly increasing rounds (ChainError::JustificationRoundsNotIncreasing). A justification chain is an ordered sequence of quorums where each link must sit in a higher round than the link it justifies; equal or decreasing rounds break the chain's causal structure. verify scans every adjacent pair (windows(2)) and rejects the first non-increasing step.","triggerScenarios":"Calling JustificationChain::verify(value_hash) on a chain built by appending links out of round order, appending a link whose round is <= the current top link's round (the append API documents that new links must be in higher rounds), or deserializing a network-received chain with malformed ordering.","commonSituations":"Assembling chains from votes collected out of order without sorting by round; a Byzantine validator shipping a crafted chain with repeated rounds to probe clients; serialization bugs that reorder links; tests constructing chains with default Round::new(0) for every link.","solutions":["Append links in strictly increasing round order — sort collected quorums by round before building the chain.","Validate round ordering at deserialization/network ingress with verify() before storing or forwarding the chain.","If building from a map of round -> quorum, iterate rounds sorted ascending.","Treat chains failing this check as malicious input; do not attempt repair."],"exampleFix":"// before\nlet chain = JustificationChain::new(links_as_received); // unordered\nchain.verify(value_hash)?; // Err: rounds not increasing\n\n// after\nlet mut links = links_as_received;\nlinks.sort_by_key(|l| l.round);\nlinks.dedup_by_key(|l| l.round);\nlet chain = JustificationChain::new(links);\nchain.verify(value_hash)?;","handlingStrategy":"validation","validationCode":"// verify round ordering before accepting a chain\nfor pair in chain.links().windows(2) {\n    if pair[0].round >= pair[1].round {\n        return Err(anyhow::anyhow!(\"justification chain rounds not strictly increasing\"));\n    }\n}","typeGuard":"fn has_increasing_rounds(chain: &JustificationChain) -> bool {\n    chain.links().windows(2).all(|w| w[0].round < w[1].round)\n}","tryCatchPattern":"match chain.verify(value_hash) {\n    Err(ChainError::JustificationRoundsNotIncreasing) => {\n        // malformed or malicious chain: reject and penalize the sender\n        Err(anyhow::anyhow!(\"rejecting malformed justification chain\"))\n    }\n    other => other,\n}","preventionTips":["Append links in ascending round order only (the append API requires it).","Run verify() at network ingress before storing a received chain.","Never attempt to repair a non-increasing chain by reordering — reject it."],"tags":["consensus","justification-chain","rounds","validation"],"backgroundTag":"invalid-justification-chain","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}