diem/diem · error
2-chain timeout has different (epoch, round) than Vote
Error message
2-chain timeout has different (epoch, round) than Vote
What it means
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.
Source
Thrown at consensus/consensus-types/src/vote.rs:191
pub fn verify(&self, validator: &ValidatorVerifier) -> anyhow::Result<()> {
ensure!(
self.ledger_info.consensus_data_hash() == self.vote_data.hash(),
"Vote's hash mismatch with LedgerInfo"
);
ensure!(
self.timeout_signature.is_none() || self.two_chain_timeout.is_none(),
"Only one timeout should exist"
);
validator
.verify(self.author(), &self.ledger_info, &self.signature)
.context("Failed to verify Vote")?;
if let Some(timeout_signature) = &self.timeout_signature {
validator
.verify(self.author(), &self.generate_timeout(), timeout_signature)
.context("Failed to verify Timeout Vote")?;
}
if let Some((timeout, signature)) = &self.two_chain_timeout {
ensure!(
(timeout.epoch(), timeout.round())
== (self.epoch(), self.vote_data.proposed().round()),
"2-chain timeout has different (epoch, round) than Vote"
);
timeout
.quorum_cert()
.verify(validator)
.context("Failed to verify QC from 2-chain timeout")?;
validator
.verify(self.author(), &timeout.signing_format(), signature)
.context("Failed to verify 2-chain timeout signature")?;
}
// Let us verify the vote data as well
self.vote_data().verify()?;
Ok(())
}
}
View on GitHub (pinned to fc4714a8ea)
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)
Example fix
// before vote.set_two_chain_timeout(timeout_from_previous_round); // after assert_eq!(timeout.epoch(), vote.epoch()); assert_eq!(timeout.round(), vote.vote_data.proposed().round()); vote.set_two_chain_timeout(current_round_timeout);
Defensive patterns
Strategy: validation
Validate before calling
pub fn timeout_matches_vote(v: &Vote) -> bool {
v.two_chain_timeout.as_ref().map_or(true, |(t, _)| {
t.epoch() == v.epoch() && t.round() == v.vote_data.proposed().round()
})
} Type guard
fn matching_timeout(v: &Vote) -> bool {
match &v.two_chain_timeout {
Some((t, _)) => t.epoch() == v.epoch() && t.round() == v.vote_data.proposed().round(),
None => true,
}
} Try / catch
match vote.verify(validator) {
Ok(()) => accept_vote(vote),
Err(e) if e.to_string().contains("different (epoch, round)") => discard_stale_vote_and_timeout(),
Err(e) => return Err(e),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Only one timeout should exist
- Timeout round should be larger than the QC round
- Inconsistent hqc round, qc has round {}, highest signed roun
- Vote's hash mismatch with LedgerInfo
- Parent and proposed epochs do not match
AI-assisted analysis of diem/diem@fc4714a8ea (2026-09-04).
Data as JSON: /api/errors/edf6787e650b26d5.
Report an issue: GitHub.