{"record":{"id":"edf6787e650b26d5","repo":"diem/diem","slug":"2-chain-timeout-has-different-epoch-round-than","errorCode":null,"errorMessage":"2-chain timeout has different (epoch, round) than Vote","messagePattern":"2-chain timeout has different \\(epoch, round\\) than Vote","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"consensus/consensus-types/src/vote.rs","lineNumber":191,"sourceCode":"    pub fn verify(&self, validator: &ValidatorVerifier) -> anyhow::Result<()> {\n        ensure!(\n            self.ledger_info.consensus_data_hash() == self.vote_data.hash(),\n            \"Vote's hash mismatch with LedgerInfo\"\n        );\n        ensure!(\n            self.timeout_signature.is_none() || self.two_chain_timeout.is_none(),\n            \"Only one timeout should exist\"\n        );\n        validator\n            .verify(self.author(), &self.ledger_info, &self.signature)\n            .context(\"Failed to verify Vote\")?;\n        if let Some(timeout_signature) = &self.timeout_signature {\n            validator\n                .verify(self.author(), &self.generate_timeout(), timeout_signature)\n                .context(\"Failed to verify Timeout Vote\")?;\n        }\n        if let Some((timeout, signature)) = &self.two_chain_timeout {\n            ensure!(\n                (timeout.epoch(), timeout.round())\n                    == (self.epoch(), self.vote_data.proposed().round()),\n                \"2-chain timeout has different (epoch, round) than Vote\"\n            );\n            timeout\n                .quorum_cert()\n                .verify(validator)\n                .context(\"Failed to verify QC from 2-chain timeout\")?;\n            validator\n                .verify(self.author(), &timeout.signing_format(), signature)\n                .context(\"Failed to verify 2-chain timeout signature\")?;\n        }\n        // Let us verify the vote data as well\n        self.vote_data().verify()?;\n        Ok(())\n    }\n}\n","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/diem/diem/blob/fc4714a8ea273b6efe8b13dbce72ea60aad9a16c/consensus/consensus-types/src/vote.rs#L173-L209","documentation":"Vote::verify checks that an attached two-chain timeout has the same epoch and round as the vote itself. The timeout proves the voter timed out at the vote's round; if the timeout's (epoch, round) differs from the vote's epoch and proposed block round, the attestation does not apply to this vote and the vote is rejected.","triggerScenarios":"Attaching a two_chain_timeout taken from a different round or epoch to a Vote before verify; constructing votes after an epoch change while reusing a timeout from the previous epoch; aggregating votes/timouts across rounds in custom code.","commonSituations":"Round rollover race where a timeout is captured then the vote is built for the next round; epoch-boundary bugs; hand-built votes in tests with stale timeouts.","solutions":["Attach only a timeout generated for the same (epoch, round) as the vote (e.g. from generate_timeout at the same round)","Discard and regenerate the vote+timeout pair after a round or epoch change","Check aggregation code so votes and timeouts are keyed by (epoch, round)"],"exampleFix":"// before\nvote.set_two_chain_timeout(timeout_from_previous_round);\n// after\nassert_eq!(timeout.epoch(), vote.epoch());\nassert_eq!(timeout.round(), vote.vote_data.proposed().round());\nvote.set_two_chain_timeout(current_round_timeout);","handlingStrategy":"validation","validationCode":"pub fn timeout_matches_vote(v: &Vote) -> bool {\n    v.two_chain_timeout.as_ref().map_or(true, |(t, _)| {\n        t.epoch() == v.epoch() && t.round() == v.vote_data.proposed().round()\n    })\n}","typeGuard":"fn matching_timeout(v: &Vote) -> bool {\n    match &v.two_chain_timeout {\n        Some((t, _)) => t.epoch() == v.epoch() && t.round() == v.vote_data.proposed().round(),\n        None => true,\n    }\n}","tryCatchPattern":"match vote.verify(validator) {\n    Ok(()) => accept_vote(vote),\n    Err(e) if e.to_string().contains(\"different (epoch, round)\") => discard_stale_vote_and_timeout(),\n    Err(e) => return Err(e),\n}","preventionTips":["Always attach the timeout generated in the same round as the vote","Re-generate vote + timeout together after any round/epoch change","Key votes and timeouts by (epoch, round) in aggregation structures"],"tags":["consensus","vote","timeout","validation"],"backgroundTag":"invalid-vote","analyzedSha":"fc4714a8ea273b6efe8b13dbce72ea60aad9a16c","analyzedAt":"2026-09-04T21:07:05.890Z","contentChangedAt":"2026-09-04T21:07:05.890Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}