Skip to main content

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;