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

  1. Attach only a timeout generated for the same (epoch, round) as the vote (e.g. from generate_timeout at the same round)
  2. Discard and regenerate the vote+timeout pair after a round or epoch change
  3. 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

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

Related errors


AI-assisted analysis of diem/diem@fc4714a8ea (2026-09-04). Data as JSON: /api/errors/edf6787e650b26d5. Report an issue: GitHub.