linera_chain/justification/mod.rs
1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! # Fault attributability
5//!
6//! When two `ConfirmedBlock` certificates exist for different blocks at the same height, the
7//! protocol must be able to *attribute* the fault: name validators that provably misbehaved,
8//! using only data the certificates carry. This module provides that data — the
9//! [`JustificationChain`] — and the algorithm that extracts a proof from it.
10//!
11//! Each `ValidatedBlock` vote signs an unlocking round (see [`VoteValue`]), asserting that the
12//! voter has not voted to confirm a *different* block in any round at or above it. A vote only
13//! counts if its unlocking round is `0` (no justification needed) or it is justified by a quorum
14//! of `ValidatedBlock` votes for the same block in a round at or above the unlocking round and
15//! below `r`. That quorum is itself justified, so the justifications form a chain of quorums with
16//! strictly increasing rounds, rising from the round where the block was first validated (where
17//! the unlocking round is `0`, represented as `None`) up to the certifying round.
18//!
19//! Votes also sign a *justification commitment*: the hash of the quorum they cite, as a
20//! [`CommittedQuorum`] — which itself contains the hash of the quorum below it, and so on down to
21//! the fresh proposal. The chain is thus hash-linked, and by signing its head a voter attests
22//! that they verified the cited quorum, whose voters in turn attested the quorum below. A
23//! certificate is therefore verified with a *single* signature check over its top quorum plus a
24//! hash walk down the carried chain: any quorum contains honest voters, so an invalid link can
25//! only sit beneath a quorum whose signers all lied — and signing over an invalid quorum is
26//! itself an attributable fault.
27//!
28//! A confirmation in a chain's *first* round needs no chain: its `ConfirmedBlock` votes instead
29//! carry a first-round attestation (see [`VoteValue`]), asserting that no lower round exists at
30//! this height. A vote in a lower round together with an attestation in a higher one is therefore
31//! itself an attributable fault.
32//!
33//! Because every certificate carries this chain (or attestation), two conflicting certificates
34//! are self-contained evidence: walking one block's chain against the other block's confirmation
35//! quorum reaches validators whose unlocking-round claims are contradicted by their own
36//! confirmation votes, or the two confirmation quorums intersect in validators that contradicted
37//! themselves. See [`extract_equivocations`].
38//!
39//! # Correctness specification
40//!
41//! The accountability guarantees this module provides are stated and proved in [`proof`]:
42//! [`ProofSoundness`](proof::ProofSoundness) (no correct validator is convictable),
43//! [`ConflictCompleteness`](proof::ConflictCompleteness) (a conflict convicts a validity
44//! threshold), and [`AccountableSafety`](proof::AccountableSafety) combining them with the safety
45//! theorem. The `linera-spec` crate gives the reading order.
46//!
47//! [`VoteValue`]: crate::data_types::VoteValue
48
49pub mod proof;
50
51use std::collections::BTreeMap;
52
53use allocative::Allocative;
54use linera_base::{
55 crypto::{BcsHashable, CryptoHash, ValidatorPublicKey, ValidatorSignature},
56 data_types::Round,
57 ensure,
58};
59use linera_execution::committee::Committee;
60use serde::{Deserialize, Serialize};
61
62use crate::{
63 block::BlockHeader,
64 data_types::{check_signatures, VoteValue},
65 types::CertificateKind,
66 ChainError,
67};
68
69#[cfg(with_metrics)]
70pub(crate) mod metrics {
71 use linera_base::prometheus_util::{exponential_bucket_interval, register_histogram_vec};
72 use prometheus::HistogramVec;
73
74 linera_base::declare_metrics! {
75 /// The number of links in a justification chain a node verified. A chain grows by one link
76 /// per round a block had to fight through, so a rising tail here signals contention on some
77 /// height before it becomes a certificate-size or finalization problem.
78 pub static JUSTIFICATION_CHAIN_LENGTH: HistogramVec =
79 register_histogram_vec(
80 "justification_chain_length",
81 "Number of links in a verified justification chain",
82 &[],
83 exponential_bucket_interval(1.0, 1024.0),
84 );
85 }
86}
87
88/// One link in a justification chain: a quorum of validators that all voted to validate the
89/// same block in `round`.
90#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
91#[cfg_attr(with_testing, derive(Eq, PartialEq))]
92pub struct JustificationLink {
93 /// The round in which these `ValidatedBlock` votes were cast.
94 pub round: Round,
95 /// The validators' signatures over the corresponding [`VoteValue`].
96 pub signatures: Vec<(ValidatorPublicKey, ValidatorSignature)>,
97}
98
99/// A quorum of `ValidatedBlock` votes as the votes built on top of it commit to it: the block
100/// and round it validated, the payload fields its own voters signed — their unlocking round and
101/// their commitment to the quorum below — and its signatures.
102///
103/// Hashing this struct yields the *justification commitment* (see [`VoteValue`]) that votes
104/// citing this quorum sign. Because the struct contains the previous quorum's commitment, the
105/// hash transitively commits to the entire chain below: verifying one quorum's signatures over
106/// it attests, link by link, that every quorum underneath was checked by the honest voters who
107/// signed above it.
108#[derive(Clone, Debug, Serialize, Deserialize)]
109#[cfg_attr(with_testing, derive(Eq, PartialEq))]
110pub struct CommittedQuorum {
111 /// The hash of the block this quorum validated.
112 pub value_hash: CryptoHash,
113 /// The round in which the quorum's votes were cast.
114 pub round: Round,
115 /// The unlocking round the quorum's voters signed: the round of the quorum below, or `None`
116 /// for a fresh proposal.
117 pub unlocking_round: Option<Round>,
118 /// The justification commitment the quorum's voters signed: the hash of the quorum below as
119 /// a [`CommittedQuorum`], or `None` for a fresh proposal.
120 pub previous: Option<CryptoHash>,
121 /// The `ValidatedBlock` signatures forming the quorum.
122 pub signatures: Vec<(ValidatorPublicKey, ValidatorSignature)>,
123}
124
125impl BcsHashable<'_> for CommittedQuorum {}
126
127impl CommittedQuorum {
128 /// Returns the justification commitment for this quorum: the value a vote citing it signs.
129 pub fn commitment(&self) -> CryptoHash {
130 CryptoHash::new(self)
131 }
132}
133
134/// The chain of `ValidatedBlock` quorums that justifies a validated or confirmed block, from
135/// the round where the block was first validated up to the certifying round.
136///
137/// Links are ordered by **strictly increasing** round. The quorum in link `i` was cast with
138/// unlocking round `links[i - 1].round` — i.e. it is justified by the previous, lower link — and
139/// the first link (index `0`) was cast with unlocking round `0` (`None`), the fresh proposal that
140/// grounds the chain. An empty chain means the block was confirmed in the fast round, which needs
141/// no validation.
142#[derive(Clone, Debug, Default, Serialize, Deserialize, Allocative)]
143#[cfg_attr(with_testing, derive(Eq, PartialEq))]
144pub struct JustificationChain {
145 links: Vec<JustificationLink>,
146}
147
148impl JustificationChain {
149 /// Creates a justification chain from its links, ordered by increasing round.
150 pub fn new(links: Vec<JustificationLink>) -> Self {
151 Self { links }
152 }
153
154 /// Returns the links, ordered by increasing round.
155 pub fn links(&self) -> &[JustificationLink] {
156 &self.links
157 }
158
159 /// Returns a new chain with the given quorum appended as a new top link in `round`,
160 /// i.e. the highest, certifying round. The existing links must all be in lower rounds.
161 pub fn append(
162 &self,
163 round: Round,
164 signatures: Vec<(ValidatorPublicKey, ValidatorSignature)>,
165 ) -> Self {
166 let mut links = self.links.clone();
167 links.push(JustificationLink { round, signatures });
168 Self { links }
169 }
170
171 /// Returns the unlocking round that a certificate sitting on top of this chain signed: the
172 /// round of the highest link, or `None` if the chain is empty.
173 pub fn top_unlocking_round(&self) -> Option<Round> {
174 self.links.last().map(|link| link.round)
175 }
176
177 /// Returns the justification commitment that a vote sitting on top of this chain signs (see
178 /// [`VoteValue`]): the hash of the top link as a [`CommittedQuorum`], which transitively
179 /// commits to every link below. `None` iff the chain is empty.
180 pub fn commitment(&self, value_hash: CryptoHash) -> Option<CryptoHash> {
181 let mut previous = None;
182 let mut unlocking_round = None;
183 for link in &self.links {
184 previous = Some(
185 CommittedQuorum {
186 value_hash,
187 round: link.round,
188 unlocking_round,
189 previous,
190 signatures: link.signatures.clone(),
191 }
192 .commitment(),
193 );
194 unlocking_round = Some(link.round);
195 }
196 previous
197 }
198
199 /// Verifies the chain structurally — rounds strictly increase, so each link sits in a higher
200 /// round than the one justifying it — and returns its justification commitment.
201 ///
202 /// Link signatures are deliberately *not* verified here. The quorum built on top of this
203 /// chain signs the chain's commitment, so a single signature check over that quorum attests
204 /// every link below it: each link's voters verified the quorum beneath them before signing
205 /// over its hash. Signing over an invalid quorum is itself an attributable fault, so link
206 /// signatures are only re-checked when auditing a chain to blame the validators that attested
207 /// an invalid link.
208 ///
209 /// The chain length needs no explicit cap: strictly increasing rounds mean a chain of `n`
210 /// links spans `n` distinct rounds, and each link must carry a genuine quorum, so a longer
211 /// chain necessarily reaches a higher round and cannot be inflated cheaply. The observed
212 /// length is recorded as a metric so real contention shows up in monitoring.
213 pub fn verify(&self, value_hash: CryptoHash) -> Result<Option<CryptoHash>, ChainError> {
214 #[cfg(with_metrics)]
215 metrics::JUSTIFICATION_CHAIN_LENGTH
216 .with_label_values(&[])
217 .observe(self.links.len() as f64);
218 for window in self.links.windows(2) {
219 ensure!(
220 window[0].round < window[1].round,
221 ChainError::JustificationRoundsNotIncreasing
222 );
223 }
224 Ok(self.commitment(value_hash))
225 }
226}
227
228/// A confirmed block's *header* together with the justification that makes it self-contained
229/// evidence: the round and quorum of `ConfirmedBlock` votes that finalized it, and the chain of
230/// `ValidatedBlock` quorums for the same block, with its top link in the round the block was
231/// confirmed. Only the header travels, never the block body. The header hashes to the value the
232/// votes sign (`CryptoHash::new(&header)`) and carries the chain ID and height that scope the
233/// fault. This is the shape a `ConfirmedBlockCertificate` reduces to for fault attribution.
234#[derive(Clone, Debug)]
235pub struct JustifiedConfirmation {
236 /// The header of the confirmed block. Its hash is what the chain's `ValidatedBlock` and
237 /// `ConfirmedBlock` votes sign (`ValidatedBlock` and `ConfirmedBlock` wrap the same block).
238 pub header: BlockHeader,
239 /// The round in which the block was confirmed.
240 pub round: Round,
241 /// The first-round attestation the `ConfirmedBlock` votes signed (see [`VoteValue`]).
242 pub first_round: bool,
243 /// The quorum of `ConfirmedBlock` votes finalizing the block.
244 pub confirmed_signatures: Vec<(ValidatorPublicKey, ValidatorSignature)>,
245 /// The `ValidatedBlock` justification chain (empty iff confirmed in the chain's first round).
246 pub justification: JustificationChain,
247}
248
249impl JustifiedConfirmation {
250 /// The hash of the confirmed block, which is what its votes sign.
251 pub fn block_hash(&self) -> CryptoHash {
252 CryptoHash::new(&self.header)
253 }
254
255 /// The justification commitment the confirmed votes signed: the hash of the justification
256 /// chain's top link, or `None` for a first-round confirmation (empty chain).
257 pub fn confirmed_commitment(&self) -> Option<CryptoHash> {
258 self.justification.commitment(self.block_hash())
259 }
260}
261
262/// A quorum of `ValidatedBlock` votes for one block, cast in one round under one unlocking round.
263/// This is the top of a `ValidatedBlockCertificate`; comparing two of them in the same round
264/// attributes a double-validation fault.
265#[derive(Clone, Debug)]
266pub struct ValidatedQuorum {
267 /// The header of the validated block. Its hash is what the votes sign, and it carries the
268 /// chain ID and height that scope the fault.
269 pub header: BlockHeader,
270 /// The round in which the block was validated.
271 pub round: Round,
272 /// The unlocking round these `ValidatedBlock` votes signed.
273 pub unlocking_round: Option<Round>,
274 /// The justification commitment these `ValidatedBlock` votes signed.
275 pub justification_commitment: Option<CryptoHash>,
276 /// The quorum of `ValidatedBlock` votes.
277 pub signatures: Vec<(ValidatorPublicKey, ValidatorSignature)>,
278}
279
280/// A self-contained proof that a single validator misbehaved.
281#[derive(Clone, Debug)]
282pub enum EquivocationProof {
283 /// The validator voted to validate one block under an unlocking round while having voted to
284 /// confirm a *different* block in a round at or above it — contradicting its own
285 /// unlocking-round claim.
286 LockViolation {
287 /// The misbehaving validator.
288 validator: ValidatorPublicKey,
289 /// The header of the block the validator voted to confirm. Its hash, chain ID and height
290 /// are read from here; it must share the chain and height of `validated_header`.
291 confirmed_header: BlockHeader,
292 /// The round in which it voted to confirm.
293 confirmed_round: Round,
294 /// The first-round attestation the confirmation vote signed (see [`VoteValue`]).
295 ///
296 /// [`VoteValue`]: crate::data_types::VoteValue
297 confirmed_attested: bool,
298 /// The justification commitment the confirmation vote signed.
299 confirmed_commitment: Option<CryptoHash>,
300 /// Its `ConfirmedBlock` signature.
301 confirmed_signature: ValidatorSignature,
302 /// The header of the different block the validator voted to validate.
303 validated_header: BlockHeader,
304 /// The round in which it voted to validate.
305 validated_round: Round,
306 /// The unlocking round the validation vote signed, contradicted by the confirmation.
307 validated_unlocking_round: Option<Round>,
308 /// The justification commitment the validation vote signed.
309 validated_commitment: Option<CryptoHash>,
310 /// Its `ValidatedBlock` signature.
311 validated_signature: ValidatorSignature,
312 },
313 /// The validator cast two votes of the same kind for different blocks in the same round:
314 /// either two `ConfirmedBlock` votes, or two `ValidatedBlock` votes (illegal regardless of
315 /// the unlocking rounds, since a validator may vote for at most one block per round and kind).
316 DoubleVote {
317 /// The misbehaving validator.
318 validator: ValidatorPublicKey,
319 /// The round both votes share.
320 round: Round,
321 /// The certificate kind both votes share.
322 kind: CertificateKind,
323 /// The header of the first block voted for. The two headers must share a chain and height.
324 first_header: BlockHeader,
325 /// The unlocking round the first vote signed (`None` for `ConfirmedBlock` votes).
326 first_unlocking_round: Option<Round>,
327 /// The first-round attestation the first vote signed (`false` for `ValidatedBlock` votes).
328 first_attested: bool,
329 /// The justification commitment the first vote signed.
330 first_commitment: Option<CryptoHash>,
331 /// The signature on the first vote.
332 first_signature: ValidatorSignature,
333 /// The header of the second, different block voted for.
334 second_header: BlockHeader,
335 /// The unlocking round the second vote signed (`None` for `ConfirmedBlock` votes).
336 second_unlocking_round: Option<Round>,
337 /// The first-round attestation the second vote signed (`false` for `ValidatedBlock` votes).
338 second_attested: bool,
339 /// The justification commitment the second vote signed.
340 second_commitment: Option<CryptoHash>,
341 /// The signature on the second vote.
342 second_signature: ValidatorSignature,
343 },
344 /// The validator voted to confirm a block with the first-round attestation — asserting that
345 /// no lower round exists at this height — while having also voted to confirm a block in a
346 /// lower round. The two votes cannot both be honest, regardless of the blocks they are for.
347 FirstRoundViolation {
348 /// The misbehaving validator.
349 validator: ValidatorPublicKey,
350 /// The header of the block confirmed with the attestation. It must share the chain and
351 /// height of `earlier_header`.
352 attested_header: BlockHeader,
353 /// The round the attestation asserts to be the chain's first.
354 attested_round: Round,
355 /// The justification commitment the attested vote signed.
356 attested_commitment: Option<CryptoHash>,
357 /// The `ConfirmedBlock` signature carrying the attestation.
358 attested_signature: ValidatorSignature,
359 /// The header of the block the earlier vote confirmed.
360 earlier_header: BlockHeader,
361 /// The round of the earlier vote — strictly below `attested_round`.
362 earlier_round: Round,
363 /// The first-round attestation the earlier vote signed.
364 earlier_attested: bool,
365 /// The justification commitment the earlier vote signed.
366 earlier_commitment: Option<CryptoHash>,
367 /// Its `ConfirmedBlock` signature.
368 earlier_signature: ValidatorSignature,
369 },
370 /// The validator signed a justification commitment whose opening is not a quorum it could
371 /// honestly have cited: the opening's signatures are invalid or below the quorum threshold,
372 /// it validates a different block, or its round does not ground the vote that cited it. By
373 /// signing the commitment the validator attested that it had verified the cited quorum —
374 /// certificate verification relies on that attestation instead of re-checking the chain — so
375 /// an invalid opening is individually attributable, with no conflicting certificate needed.
376 InvalidJustification {
377 /// The misbehaving validator.
378 validator: ValidatorPublicKey,
379 /// The header of the block the attesting vote was for.
380 header: BlockHeader,
381 /// The round of the attesting vote.
382 round: Round,
383 /// The kind of the attesting vote.
384 kind: CertificateKind,
385 /// The unlocking round the attesting vote signed.
386 unlocking_round: Option<Round>,
387 /// The first-round attestation the attesting vote signed.
388 first_round: bool,
389 /// The attesting vote's signature. Its payload contains the opening's hash as the
390 /// justification commitment, binding the signature to the opening.
391 signature: ValidatorSignature,
392 /// The opening of the signed commitment: the cited quorum, which is not valid.
393 opening: CommittedQuorum,
394 },
395}
396
397impl EquivocationProof {
398 /// Returns the misbehaving validator.
399 pub fn validator(&self) -> ValidatorPublicKey {
400 match self {
401 EquivocationProof::LockViolation { validator, .. }
402 | EquivocationProof::DoubleVote { validator, .. }
403 | EquivocationProof::FirstRoundViolation { validator, .. }
404 | EquivocationProof::InvalidJustification { validator, .. } => *validator,
405 }
406 }
407
408 /// Verifies that this is a genuine proof of misbehavior: the referenced votes are actually
409 /// incompatible (or the opened justification actually invalid, judged against `committee`)
410 /// and signed by the named validator.
411 pub fn check(&self, committee: &Committee) -> Result<(), ChainError> {
412 match self {
413 EquivocationProof::LockViolation {
414 validator,
415 confirmed_header,
416 confirmed_round,
417 confirmed_attested,
418 confirmed_commitment,
419 confirmed_signature,
420 validated_header,
421 validated_round,
422 validated_unlocking_round,
423 validated_commitment,
424 validated_signature,
425 } => {
426 let confirmed_block_hash = CryptoHash::new(confirmed_header);
427 let validated_block_hash = CryptoHash::new(validated_header);
428 ensure!(
429 confirmed_block_hash != validated_block_hash,
430 ChainError::EquivocationProofSameBlock
431 );
432 // The two votes must concern the same height on the same chain; otherwise there
433 // is no lock relationship between them — a validator may freely confirm a block at
434 // one height and validate a different one at another height or on another chain.
435 ensure!(
436 confirmed_header.chain_id == validated_header.chain_id
437 && confirmed_header.height == validated_header.height,
438 ChainError::EquivocationProofDifferentChainOrHeight
439 );
440 // The unlocking-round claim — "no confirmation of a different block in any round
441 // at or above the unlocking round" — is made while validating in
442 // `validated_round`, so it covers only the rounds the voter had already acted in:
443 // the window `[unlocking_round, validated_round)` (an unlocking round of `None`
444 // means `0`). The confirmation contradicts it only if it falls in that window,
445 // i.e. `unlocking_round ≤ confirmed_round < validated_round`. A confirmation at or
446 // after `validated_round` is a legitimate later switch, not a violation.
447 ensure!(
448 *confirmed_round < *validated_round
449 && validated_unlocking_round
450 .is_none_or(|unlocking_round| *confirmed_round >= unlocking_round),
451 ChainError::EquivocationProofNoLockViolation
452 );
453 let confirmed = VoteValue(
454 confirmed_block_hash,
455 *confirmed_round,
456 CertificateKind::Confirmed,
457 None,
458 *confirmed_attested,
459 *confirmed_commitment,
460 );
461 confirmed_signature.check(&confirmed, *validator)?;
462 let validated = VoteValue(
463 validated_block_hash,
464 *validated_round,
465 CertificateKind::Validated,
466 *validated_unlocking_round,
467 false,
468 *validated_commitment,
469 );
470 validated_signature.check(&validated, *validator)?;
471 Ok(())
472 }
473 EquivocationProof::DoubleVote {
474 validator,
475 round,
476 kind,
477 first_header,
478 first_unlocking_round,
479 first_attested,
480 first_commitment,
481 first_signature,
482 second_header,
483 second_unlocking_round,
484 second_attested,
485 second_commitment,
486 second_signature,
487 } => {
488 let first_block_hash = CryptoHash::new(first_header);
489 let second_block_hash = CryptoHash::new(second_header);
490 ensure!(
491 first_block_hash != second_block_hash,
492 ChainError::EquivocationProofSameBlock
493 );
494 // Both votes must concern the same height on the same chain: a validator voting
495 // for different blocks at different heights or on different chains in the same
496 // round number is not double-voting.
497 ensure!(
498 first_header.chain_id == second_header.chain_id
499 && first_header.height == second_header.height,
500 ChainError::EquivocationProofDifferentChainOrHeight
501 );
502 let first = VoteValue(
503 first_block_hash,
504 *round,
505 *kind,
506 *first_unlocking_round,
507 *first_attested,
508 *first_commitment,
509 );
510 first_signature.check(&first, *validator)?;
511 let second = VoteValue(
512 second_block_hash,
513 *round,
514 *kind,
515 *second_unlocking_round,
516 *second_attested,
517 *second_commitment,
518 );
519 second_signature.check(&second, *validator)?;
520 Ok(())
521 }
522 EquivocationProof::FirstRoundViolation {
523 validator,
524 attested_header,
525 attested_round,
526 attested_commitment,
527 attested_signature,
528 earlier_header,
529 earlier_round,
530 earlier_attested,
531 earlier_commitment,
532 earlier_signature,
533 } => {
534 // Both votes must concern the same height on the same chain: the attestation only
535 // asserts that no lower round exists at *this* height, so a vote at a lower round
536 // number elsewhere does not contradict it. The blocks themselves may be equal —
537 // the contradiction is between the rounds, not the blocks.
538 ensure!(
539 attested_header.chain_id == earlier_header.chain_id
540 && attested_header.height == earlier_header.height,
541 ChainError::EquivocationProofDifferentChainOrHeight
542 );
543 ensure!(
544 *earlier_round < *attested_round,
545 ChainError::EquivocationProofNoFirstRoundViolation
546 );
547 let attested = VoteValue(
548 CryptoHash::new(attested_header),
549 *attested_round,
550 CertificateKind::Confirmed,
551 None,
552 true,
553 *attested_commitment,
554 );
555 attested_signature.check(&attested, *validator)?;
556 let earlier = VoteValue(
557 CryptoHash::new(earlier_header),
558 *earlier_round,
559 CertificateKind::Confirmed,
560 None,
561 *earlier_attested,
562 *earlier_commitment,
563 );
564 earlier_signature.check(&earlier, *validator)?;
565 Ok(())
566 }
567 EquivocationProof::InvalidJustification {
568 validator,
569 header,
570 round,
571 kind,
572 unlocking_round,
573 first_round,
574 signature,
575 opening,
576 } => {
577 // The signed payload contains the opening's hash, so verifying the signature
578 // binds the validator to exactly this opening.
579 let value = VoteValue(
580 CryptoHash::new(header),
581 *round,
582 *kind,
583 *unlocking_round,
584 *first_round,
585 Some(opening.commitment()),
586 );
587 signature.check(&value, *validator)?;
588 // The opening must be one that no honest voter could have cited.
589 ensure!(
590 check_cited_quorum(header, *round, *kind, *unlocking_round, opening, committee)
591 .is_err(),
592 ChainError::EquivocationProofValidJustification
593 );
594 Ok(())
595 }
596 }
597 }
598}
599
600/// Checks that `opening` is a quorum an honest voter could cite from a vote of the given kind,
601/// round and unlocking round for the block with the given header: it validates the same block, in
602/// the round the vote's payload grounds on, and its signatures form a genuine quorum of
603/// `committee` over the reconstructed `ValidatedBlock` payload. These are exactly the checks a
604/// voter performs before signing the opening's commitment, so their failure on a signed opening
605/// convicts the signer.
606fn check_cited_quorum(
607 header: &BlockHeader,
608 round: Round,
609 kind: CertificateKind,
610 unlocking_round: Option<Round>,
611 opening: &CommittedQuorum,
612 committee: &Committee,
613) -> Result<(), ChainError> {
614 ensure!(
615 opening.value_hash == CryptoHash::new(header),
616 ChainError::JustificationCommitmentMismatch
617 );
618 match kind {
619 // A validated vote cites the quorum grounding its unlocking round, in a lower round.
620 CertificateKind::Validated => ensure!(
621 unlocking_round == Some(opening.round) && opening.round < round,
622 ChainError::JustificationUnlockingRoundMismatch
623 ),
624 // A confirmed vote cites the quorum that validated the block in the same round.
625 CertificateKind::Confirmed => ensure!(
626 opening.round == round,
627 ChainError::JustificationUnlockingRoundMismatch
628 ),
629 // Timeout votes cite nothing; any commitment is dishonest.
630 CertificateKind::Timeout => ensure!(false, ChainError::JustificationCommitmentMismatch),
631 }
632 // A quorum with an unlocking round cites a quorum itself, and vice versa: its own commitment
633 // and unlocking round come from one chain, so they are both present or both absent.
634 ensure!(
635 opening.unlocking_round.is_some() == opening.previous.is_some(),
636 ChainError::JustificationUnlockingRoundMismatch
637 );
638 let value = VoteValue(
639 opening.value_hash,
640 opening.round,
641 CertificateKind::Validated,
642 opening.unlocking_round,
643 false,
644 opening.previous,
645 );
646 check_signatures(&value, &opening.signatures, committee)
647}
648
649/// Audits a justified confirmation by re-checking every link of its carried chain — the work
650/// certificate verification skips, because each link's validity is attested by the signatures
651/// above it. If a link is not a genuine quorum, returns one [`InvalidJustification`] proof per
652/// signer of the level above it (the next link, or the confirmation quorum for the top link):
653/// they all signed the invalid quorum's commitment. Returns an empty list if the chain is sound.
654///
655/// [`InvalidJustification`]: EquivocationProof::InvalidJustification
656pub fn audit_confirmation(
657 confirmation: &JustifiedConfirmation,
658 committee: &Committee,
659) -> Vec<EquivocationProof> {
660 let block_hash = confirmation.block_hash();
661 let mut previous = None;
662 let mut unlocking_round = None;
663 for (index, link) in confirmation.justification.links().iter().enumerate() {
664 let opening = CommittedQuorum {
665 value_hash: block_hash,
666 round: link.round,
667 unlocking_round,
668 previous,
669 signatures: link.signatures.clone(),
670 };
671 let value = VoteValue(
672 block_hash,
673 link.round,
674 CertificateKind::Validated,
675 unlocking_round,
676 false,
677 previous,
678 );
679 if check_signatures(&value, &link.signatures, committee).is_err() {
680 // This link is not a genuine quorum. Its own signatures prove nothing, but every
681 // signer of the level above committed to its hash, attesting they had verified it.
682 return match confirmation.justification.links().get(index + 1) {
683 Some(above) => above
684 .signatures
685 .iter()
686 .map(
687 |(validator, signature)| EquivocationProof::InvalidJustification {
688 validator: *validator,
689 header: confirmation.header.clone(),
690 round: above.round,
691 kind: CertificateKind::Validated,
692 unlocking_round: Some(link.round),
693 first_round: false,
694 signature: *signature,
695 opening: opening.clone(),
696 },
697 )
698 .collect(),
699 None => confirmation
700 .confirmed_signatures
701 .iter()
702 .map(
703 |(validator, signature)| EquivocationProof::InvalidJustification {
704 validator: *validator,
705 header: confirmation.header.clone(),
706 round: confirmation.round,
707 kind: CertificateKind::Confirmed,
708 unlocking_round: None,
709 first_round: confirmation.first_round,
710 signature: *signature,
711 opening: opening.clone(),
712 },
713 )
714 .collect(),
715 };
716 }
717 previous = Some(opening.commitment());
718 unlocking_round = Some(link.round);
719 }
720 Vec::new()
721}
722
723/// Extracts proofs of equivocation from two `ConfirmedBlock` certificates that finalize
724/// *different* blocks at the same height: one proof for every validator whose own signatures on
725/// the two certificates (including their justification chains) contradict each other.
726///
727/// Returns an empty list only if the inputs do not actually conflict (same block, or different
728/// chains or heights) or are malformed. For two genuinely conflicting, well-formed certificates
729/// the proven validators always hold at least a third of the total weight, because each of the
730/// following cases blames a full intersection of two quorums. With the lower confirmation in
731/// round `r` and the higher in round `s`:
732///
733/// - `r == s`: every validator in the intersection of the two confirmation quorums double-voted.
734/// - `r < s` and the higher certificate carries a justification chain: the chain's links tile
735/// all rounds from its grounding round up to `s` with unlocking-round windows, and the
736/// grounding link's window is unbounded below, so some link's window contains `r`. Every
737/// validator in that link's intersection with the lower confirmation quorum violated its lock.
738/// - `r < s` and the higher certificate instead carries the first-round attestation: every
739/// validator in the intersection of the two confirmation quorums confirmed in `r` while
740/// attesting that `s` is the chain's first round — a first-round violation.
741pub fn extract_equivocations(
742 a: &JustifiedConfirmation,
743 b: &JustifiedConfirmation,
744) -> Vec<EquivocationProof> {
745 // A conflict is two *different* blocks at the *same* height on the *same* chain.
746 if a.header.chain_id != b.header.chain_id || a.header.height != b.header.height {
747 return Vec::new();
748 }
749 if a.block_hash() == b.block_hash() {
750 return Vec::new(); // Not a conflict.
751 }
752 let mut proofs = BTreeMap::new();
753 // Walk each block's justification chain against the other's confirmation quorum.
754 walk_chain(a, b, &mut proofs);
755 walk_chain(b, a, &mut proofs);
756 // Both blocks were confirmed in the same round: two confirmation votes for different blocks
757 // in that round.
758 double_confirm(a, b, &mut proofs);
759 // One block was confirmed with a first-round attestation and the other in a lower round: a
760 // confirmation below the attested first round.
761 first_round_violation(a, b, &mut proofs);
762 first_round_violation(b, a, &mut proofs);
763 proofs.into_values().collect()
764}
765
766/// Records a proof for every validator that confirmed `confirmer`'s block and also appears in
767/// some link of `chained`'s justification chain with an unlocking round the confirmation
768/// contradicts.
769fn walk_chain(
770 confirmer: &JustifiedConfirmation,
771 chained: &JustifiedConfirmation,
772 proofs: &mut BTreeMap<ValidatorPublicKey, EquivocationProof>,
773) {
774 let chained_block_hash = chained.block_hash();
775 let chain = &chained.justification;
776 // The commitment each link's voters signed is the hash of the link below, folded up as we
777 // walk. It is needed to reconstruct the exact payload of the extracted signatures.
778 let mut commitment = None;
779 let mut unlocking_round = None;
780 for link in chain.links() {
781 // This link's votes (cast in `link.round`) claim no conflicting confirmation in any
782 // round at or above the unlocking round, covering the window `[unlocking_round,
783 // link.round)`. A confirmation in `confirmer.round` contradicts it only if it falls in
784 // that window: the link must have been cast strictly after the confirmation
785 // (`link.round > confirmer.round`) with an unlocking round reaching back over it
786 // (`unlocking_round ≤ confirmer.round`). Otherwise it's a legitimate later switch; another
787 // link may still straddle the confirmation, so keep scanning.
788 if link.round > confirmer.round
789 && unlocking_round.is_none_or(|unlocking_round| unlocking_round <= confirmer.round)
790 {
791 for (validator, validated_signature) in &link.signatures {
792 if let Some(confirmed_signature) =
793 signature_of(&confirmer.confirmed_signatures, validator)
794 {
795 proofs
796 .entry(*validator)
797 .or_insert_with(|| EquivocationProof::LockViolation {
798 validator: *validator,
799 confirmed_header: confirmer.header.clone(),
800 confirmed_round: confirmer.round,
801 confirmed_attested: confirmer.first_round,
802 confirmed_commitment: confirmer.confirmed_commitment(),
803 confirmed_signature,
804 validated_header: chained.header.clone(),
805 validated_round: link.round,
806 validated_unlocking_round: unlocking_round,
807 validated_commitment: commitment,
808 validated_signature: *validated_signature,
809 });
810 }
811 }
812 }
813 commitment = Some(
814 CommittedQuorum {
815 value_hash: chained_block_hash,
816 round: link.round,
817 unlocking_round,
818 previous: commitment,
819 signatures: link.signatures.clone(),
820 }
821 .commitment(),
822 );
823 unlocking_round = Some(link.round);
824 }
825}
826
827/// Extracts proofs that validators validated two *different* blocks in the same round, which
828/// is illegal regardless of the locks: a validator may vote to validate at most one block per
829/// round. One proof per validator that signed both quorums; empty if the quorums are for the
830/// same block or different rounds (validating conflicting blocks in different rounds is not
831/// itself a fault — only confirming is locked).
832pub fn extract_double_validations(
833 a: &ValidatedQuorum,
834 b: &ValidatedQuorum,
835) -> Vec<EquivocationProof> {
836 let mut proofs = BTreeMap::new();
837 if a.round == b.round {
838 double_vote(
839 a.round,
840 CertificateKind::Validated,
841 &a.header,
842 a.unlocking_round,
843 false,
844 a.justification_commitment,
845 &a.signatures,
846 &b.header,
847 b.unlocking_round,
848 false,
849 b.justification_commitment,
850 &b.signatures,
851 &mut proofs,
852 );
853 }
854 proofs.into_values().collect()
855}
856
857/// Records a proof for every validator that confirmed both blocks in the same round.
858fn double_confirm(
859 a: &JustifiedConfirmation,
860 b: &JustifiedConfirmation,
861 proofs: &mut BTreeMap<ValidatorPublicKey, EquivocationProof>,
862) {
863 if a.round != b.round {
864 return;
865 }
866 double_vote(
867 a.round,
868 CertificateKind::Confirmed,
869 &a.header,
870 None,
871 a.first_round,
872 a.confirmed_commitment(),
873 &a.confirmed_signatures,
874 &b.header,
875 None,
876 b.first_round,
877 b.confirmed_commitment(),
878 &b.confirmed_signatures,
879 proofs,
880 );
881}
882
883/// Records a proof for every validator that confirmed `attested`'s block with the first-round
884/// attestation and also confirmed `earlier`'s block in a lower round — contradicting the
885/// attestation's claim that no lower round exists at this height.
886fn first_round_violation(
887 attested: &JustifiedConfirmation,
888 earlier: &JustifiedConfirmation,
889 proofs: &mut BTreeMap<ValidatorPublicKey, EquivocationProof>,
890) {
891 if !attested.first_round || earlier.round >= attested.round {
892 return;
893 }
894 for (validator, attested_signature) in &attested.confirmed_signatures {
895 if let Some(earlier_signature) = signature_of(&earlier.confirmed_signatures, validator) {
896 proofs
897 .entry(*validator)
898 .or_insert_with(|| EquivocationProof::FirstRoundViolation {
899 validator: *validator,
900 attested_header: attested.header.clone(),
901 attested_round: attested.round,
902 attested_commitment: attested.confirmed_commitment(),
903 attested_signature: *attested_signature,
904 earlier_header: earlier.header.clone(),
905 earlier_round: earlier.round,
906 earlier_attested: earlier.first_round,
907 earlier_commitment: earlier.confirmed_commitment(),
908 earlier_signature,
909 });
910 }
911 }
912}
913
914/// Records an [`EquivocationProof::DoubleVote`] for every validator that signed both quorums.
915#[allow(clippy::too_many_arguments)]
916fn double_vote(
917 round: Round,
918 kind: CertificateKind,
919 first_header: &BlockHeader,
920 first_unlocking_round: Option<Round>,
921 first_attested: bool,
922 first_commitment: Option<CryptoHash>,
923 first_signatures: &[(ValidatorPublicKey, ValidatorSignature)],
924 second_header: &BlockHeader,
925 second_unlocking_round: Option<Round>,
926 second_attested: bool,
927 second_commitment: Option<CryptoHash>,
928 second_signatures: &[(ValidatorPublicKey, ValidatorSignature)],
929 proofs: &mut BTreeMap<ValidatorPublicKey, EquivocationProof>,
930) {
931 // Only a conflict if the two votes are for different blocks at the same height on the same
932 // chain.
933 if first_header.chain_id != second_header.chain_id
934 || first_header.height != second_header.height
935 || CryptoHash::new(first_header) == CryptoHash::new(second_header)
936 {
937 return;
938 }
939 for (validator, first_signature) in first_signatures {
940 if let Some(second_signature) = signature_of(second_signatures, validator) {
941 proofs
942 .entry(*validator)
943 .or_insert_with(|| EquivocationProof::DoubleVote {
944 validator: *validator,
945 round,
946 kind,
947 first_header: first_header.clone(),
948 first_unlocking_round,
949 first_attested,
950 first_commitment,
951 first_signature: *first_signature,
952 second_header: second_header.clone(),
953 second_unlocking_round,
954 second_attested,
955 second_commitment,
956 second_signature,
957 });
958 }
959 }
960}
961
962/// Returns the validator's signature in `signatures`, if present.
963fn signature_of(
964 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
965 validator: &ValidatorPublicKey,
966) -> Option<ValidatorSignature> {
967 signatures
968 .iter()
969 .find(|(key, _)| key == validator)
970 .map(|(_, signature)| *signature)
971}
972
973#[cfg(test)]
974#[path = "../unit_tests/justification_tests.rs"]
975mod justification_tests;