Skip to main content

linera_chain/manager/proof/
safety.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The safety proof: at most one block is ever committed per chain and height.
5//!
6//! The argument has one non-trivial step, [`LockPreservation`], an induction over rounds showing
7//! that once a block is committed no *later* round can validate anything else. Everything before
8//! it is local implementation properties and per-round uniqueness; everything after it is
9//! bookkeeping.
10//!
11//! Nothing in this module depends on synchrony, on message delivery, or on any validator being
12//! responsive. Safety holds in every execution permitted by
13//! [`MaxByzantineWeight`](crate::manager::proof::model::MaxByzantineWeight), including ones where
14//! the protocol makes no progress at all.
15
16use crate::{
17    data_types::proof::quorum::{
18        CertificateEmbedsQuorum, CorrectSignerCastItsVote, CorrectValidatorInIntersection,
19    },
20    manager::proof::{
21        commit::{CommitRestsOnValidation, TipAdvancesOnlyOnValidCertificate},
22        locking::{
23            CastValidationRoundFloor, ConfirmedVoteRoundMonotone, NoValidatedBlockInFastRound,
24            OneConfirmationVotePerRound, OneValidationVotePerRound, SafetyStateRecovery,
25            UniqueValidatedBlockPerRound,
26        },
27        model::{ConflictingBlocks, DeterministicExecution, EpochAgreement},
28        rounds::{CurrentRoundMonotone, VoteRoundBelowCurrentRound},
29        voting::{
30            ConfirmationNeedsValidatedCertificate, ConfirmationOnlyInCurrentRound,
31            FastConfirmationNeedsEmptyLock, UnlockingRequiresHigherCertificate,
32        },
33    },
34};
35
36/// **Lemma (A fast retry cannot change the block).** Let a block `A` be confirmed in
37/// [`Round::Fast`], and let a correct validator later cast a validation vote for a block `B` on a
38/// proposal whose [`OriginalProposal::Fast`] retries `A`'s proposal. Then `B = A`.
39///
40/// *Proof.* By [`UnlockingRequiresHigherCertificate`], the fast-retry arm of
41/// [`ChainManager::check_proposed_block`] accepts only if the validator's stored confirmation
42/// vote is in the fast round and its value satisfies `matches_proposed_block(new_block)`. That
43/// predicate compares the [`ProposedBlock`] components only — chain, epoch, transactions,
44/// height, timestamp, authenticated owner, parent hash — so it leaves open that `A` and `B`
45/// share a proposal but differ in [`BlockExecutionOutcome`], which by [`ConflictingBlocks`]
46/// would make them conflicting blocks.
47///
48/// That gap is closed by [`DeterministicExecution`]. The retry re-executes the proposal
49/// (`try_handle_block_proposal` takes the `else` branch of `if let Some(outcome) = outcome`,
50/// since a fast retry carries no outcome), at the same height with the same parent, so the only
51/// input that differs from the original fast execution is the round argument
52/// [`Round::multi_leader`]. A block accepted in the fast round recorded no oracle responses —
53/// `try_handle_block_proposal` rejects one that did with `WorkerError::FastBlockUsingOracles` —
54/// and the round is observable only as [`OracleResponse::Round`]. An execution that never
55/// queried the round therefore cannot branch on it, so by determinism the two executions agree
56/// and `B = A`. ∎
57///
58/// **Residual obligation.** The no-oracle check is applied when the *proposal's* round is fast,
59/// not when a fast block is retried, so this step relies on determinism of the execution engine
60/// rather than on a runtime check at the retry. An execution engine that made an outcome depend
61/// on the round without recording an [`OracleResponse::Round`] would break it. This is the one
62/// place in the safety argument that reaches outside consensus into execution.
63///
64/// [`Round::Fast`]: linera_base::data_types::Round::Fast
65/// [`Round::multi_leader`]: linera_base::data_types::Round::multi_leader
66/// [`OriginalProposal::Fast`]: crate::data_types::OriginalProposal::Fast
67/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
68/// [`ProposedBlock`]: crate::data_types::ProposedBlock
69/// [`BlockExecutionOutcome`]: crate::data_types::BlockExecutionOutcome
70/// [`OracleResponse::Round`]: linera_base::data_types::OracleResponse::Round
71pub trait FastRetryPreservesBlock:
72    UnlockingRequiresHigherCertificate + DeterministicExecution + ConflictingBlocks
73{
74}
75
76/// **Lemma (Unlocking justification).** Let a correct validator cast a validation vote for `B` in
77/// round `s`, and let `(A, p)` be its stored confirmation vote immediately before, with `A ≠ B`.
78/// Then a valid [`ValidatedBlockCertificate`] for `B` exists in some round `t` with
79/// `p < t < s`.
80///
81/// *Proof.* By [`UnlockingRequiresHigherCertificate`], with a stored confirmation vote present
82/// the proposal must carry an [`OriginalProposal`], and:
83///
84/// * a fresh proposal (`None`) is rejected;
85/// * a fast retry requires `A` to match `B`'s proposal, which by [`FastRetryPreservesBlock`]
86///   forces `A = B`, contradicting the hypothesis;
87/// * a regular retry carries a certificate `c` which — by the caller's `certificate.check(&
88///   committee)` and [`BlockProposal::check_invariants`] — is a valid
89///   [`ValidatedBlockCertificate`] for exactly `B`, with `c.round < s`; and since `A` does not
90///   match `B`, the accepted branch is `vote.round < certificate.round`, i.e. `p < c.round`.
91///
92/// Take `t = c.round`. ∎
93///
94/// This is the hinge of [`LockPreservation`]: a correct validator abandons a block it confirmed
95/// only in exchange for a quorum that validated the replacement *strictly above* its own
96/// confirmation — which lets the induction step down into a strictly smaller round.
97///
98/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
99/// [`OriginalProposal`]: crate::data_types::OriginalProposal
100/// [`BlockProposal::check_invariants`]: crate::data_types::BlockProposal::check_invariants
101pub trait UnlockingJustification:
102    UnlockingRequiresHigherCertificate + FastRetryPreservesBlock
103{
104}
105
106/// **Theorem (Lock preservation).** Suppose a valid [`ConfirmedBlockCertificate`] for a block `A`
107/// is certified in round `r`, at some height of some chain. Then for every round `s ≥ r`, every
108/// valid [`ValidatedBlockCertificate`] at that height and round `s` certifies `A`.
109///
110/// *Proof.* Strong induction on `s ≥ r`. Assume the claim for all `t` with `r ≤ t < s`, and let
111/// `C'` be a valid [`ValidatedBlockCertificate`] for `B` in round `s`. By [`EpochAgreement`] the
112/// confirmed certificate and `C'` are judged against the same committee; by
113/// [`CertificateEmbedsQuorum`] both signer sets are quorums of it, so by
114/// [`CorrectValidatorInIntersection`] they share a correct validator `v`, and by
115/// [`CorrectSignerCastItsVote`] `v` itself cast both votes:
116/// a confirmation vote for `A` in round `r` (call it **(a)**) and a validation vote for `B` in
117/// round `s` (call it **(b)**).
118///
119/// **Case `s = r`.** By [`NoValidatedBlockInFastRound`], `s` is not the fast round, so `r` is
120/// not either; by [`CommitRestsOnValidation`] a valid [`ValidatedBlockCertificate`] for `A` in
121/// round `r` exists. By [`UniqueValidatedBlockPerRound`] applied to it and `C'`, `B = A`.
122///
123/// **Case `s > r`.** Consider the order of **(a)** and **(b)** in `v`'s execution.
124///
125/// *Suppose **(b)** preceded **(a)**.* By [`VoteRoundBelowCurrentRound`] and
126/// [`CurrentRoundMonotone`], from **(b)** onwards `v`'s current round is `≥ s > r`. If `r` is not
127/// the fast round, **(a)** comes from [`ChainManager::create_final_vote`], which by
128/// [`ConfirmationNeedsValidatedCertificate`] and [`ConfirmationOnlyInCurrentRound`] casts a vote
129/// only when the current round *equals* `r` — impossible. If `r` is the fast round, **(a)** comes
130/// from the fast branch of [`ChainManager::create_vote`], which by
131/// [`FastConfirmationNeedsEmptyLock`] requires an empty lock and an absent validation vote — but
132/// [`CastValidationRoundFloor`], established by **(b)**, forces
133/// `max(validated_vote.round, lock round) ≥ s > Round::Fast`. Also impossible. So **(a)**
134/// preceded **(b)**.
135///
136/// *So **(a)** preceded **(b)**.* Let `(A', p)` be `v`'s stored confirmation vote immediately
137/// before **(b)**. It is present, since **(a)** stored one; and by
138/// [`ConfirmedVoteRoundMonotone`], `p ≥ r`.
139///
140/// * If `A' ≠ B`, then [`UnlockingJustification`] yields a valid
141///   [`ValidatedBlockCertificate`] for `B` in a round `t` with `p < t < s`. Then `r ≤ p < t < s`,
142///   so the induction hypothesis applies at `t` and gives `B = A`.
143/// * If `A' = B`, then `v` confirmed `B` in round `p ≥ r`.
144///   * If `p = r`: `v` also confirmed `A` in round `r` by **(a)**, so [`OneConfirmationVotePerRound`]
145///     gives `A = B`.
146///   * If `p > r`: then `p` is not the fast round (it exceeds `r ≥ Round::Fast`), so by
147///     [`ConfirmationNeedsValidatedCertificate`] a valid [`ValidatedBlockCertificate`] for `B` in
148///     round `p` existed. Moreover [`UnlockingRequiresHigherCertificate`] applied to **(b)** — in
149///     the branch where the stored vote's value matches the proposed block — gives
150///     `p ≤ c.round < s` for the certificate `c` the proposal carries, hence `p < s`. So
151///     `r < p < s`, the induction hypothesis applies at `p`, and `B = A`. ∎
152///
153/// The induction is well founded because rounds are totally ordered and every appeal to the
154/// hypothesis is at a round strictly between `r` and `s`.
155///
156/// [`ConfirmedBlockCertificate`]: crate::types::ConfirmedBlockCertificate
157/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
158/// [`ChainManager::create_final_vote`]: crate::manager::ChainManager::create_final_vote
159/// [`ChainManager::create_vote`]: crate::manager::ChainManager::create_vote
160/// [`ConfirmationOnlyInCurrentRound`]: crate::manager::proof::voting::ConfirmationOnlyInCurrentRound
161pub trait LockPreservation:
162    UnlockingJustification
163    + CommitRestsOnValidation
164    + UniqueValidatedBlockPerRound
165    + NoValidatedBlockInFastRound
166    + OneConfirmationVotePerRound
167    + ConfirmedVoteRoundMonotone
168    + CastValidationRoundFloor
169    + FastConfirmationNeedsEmptyLock
170    + ConfirmationNeedsValidatedCertificate
171    + ConfirmationOnlyInCurrentRound
172    + UnlockingRequiresHigherCertificate
173    + VoteRoundBelowCurrentRound
174    + CurrentRoundMonotone
175    + CorrectValidatorInIntersection
176    + CertificateEmbedsQuorum
177    + CorrectSignerCastItsVote
178    + EpochAgreement
179{
180}
181
182/// **Theorem (Commit agreement).** For a given chain and height, all valid
183/// [`ConfirmedBlockCertificate`]s certify the same block. Equivalently: no two conflicting blocks
184/// ([`ConflictingBlocks`]) are ever both committed.
185///
186/// *Proof.* Let valid confirmed certificates for `A` in round `r` and for `B` in round `s`
187/// exist, with `r ≤ s` after renaming.
188///
189/// * If `r = s`: by [`EpochAgreement`] both are judged against the same committee, and by
190///   [`CertificateEmbedsQuorum`] their signer sets are quorums of it, so by
191///   [`CorrectValidatorInIntersection`] a correct validator `v` signed both. By
192///   [`CorrectSignerCastItsVote`], `v` cast confirmation votes for `A` and for `B` in round `r`.
193///   By [`OneConfirmationVotePerRound`], `A = B`.
194/// * If `r < s`: then `s` is not [`Round::Fast`], so [`CommitRestsOnValidation`] gives a valid
195///   [`ValidatedBlockCertificate`] for `B` in round `s`. By [`LockPreservation`], applied to the
196///   commit of `A` in round `r` and to `s > r`, that certificate certifies `A`. Hence `B = A`. ∎
197///
198/// *In observable terms.* Combining with [`TipAdvancesOnlyOnValidCertificate`]: if any correct
199/// validator's [`ChainTipState`] records a block hash at height `h`, then no correct validator
200/// ever records a different hash at `h` — whatever the network does, and whatever the faulty
201/// validators sign.
202///
203/// [`ConfirmedBlockCertificate`]: crate::types::ConfirmedBlockCertificate
204/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
205/// [`Round::Fast`]: linera_base::data_types::Round::Fast
206/// [`ChainTipState`]: crate::ChainTipState
207pub trait CommitAgreement:
208    LockPreservation
209    + CommitRestsOnValidation
210    + OneConfirmationVotePerRound
211    + CorrectValidatorInIntersection
212    + CertificateEmbedsQuorum
213    + CorrectSignerCastItsVote
214    + ConflictingBlocks
215    + EpochAgreement
216    + TipAdvancesOnlyOnValidCertificate
217{
218}
219
220/// **Theorem (The committed chain is unique).** For each chain there is at most one sequence of
221/// committed blocks: the committed blocks at heights `0, 1, 2, …` form a single hash-linked list,
222/// and any two correct validators' [`block_hashes`](crate::ChainStateView) agree wherever both
223/// are defined. In particular the committed prefixes observed by correct validators are always
224/// compatible — one is a prefix of the other.
225///
226/// *Proof.* Induction on the height `h`.
227///
228/// At each height, [`CommitAgreement`] gives uniqueness of the committed block, *provided*
229/// [`EpochAgreement`] holds there. That proviso is what the induction supplies: the chain's epoch
230/// and committee at height `h` are functions of the execution state after height `h − 1`, which
231/// by the induction hypothesis (uniqueness below `h`) and [`DeterministicExecution`] is unique.
232/// The base case `h = 0` is the genesis configuration, which is agreed by construction. Applying
233/// [`CommitAgreement`] at `h` closes the step.
234///
235/// Linkage: by [`TipAdvancesOnlyOnValidCertificate`] a correct validator records a hash at `h`
236/// only for a certified block, and `ChainTipState::verify_block_chaining` requires a proposal's
237/// `previous_block_hash` to equal the tip's hash, so the unique committed block at `h` has the
238/// unique committed block at `h − 1` as its parent. ∎
239///
240/// This is the point where the specification's per-instance scoping
241/// ([`ConsensusInstance`](crate::manager::proof::model::ConsensusInstance)) is discharged: each
242/// consensus instance decides one height, and the heights compose into a chain.
243pub trait UniqueChain:
244    CommitAgreement + EpochAgreement + DeterministicExecution + TipAdvancesOnlyOnValidCertificate
245{
246}
247
248/// **Remark (Agreement failure is attributable).** The converse of [`CommitAgreement`]: when
249/// it fails, the failure is not silent. Two conflicting confirmed certificates are self-contained
250/// evidence convicting validators of at least
251/// [`validity_threshold`](linera_execution::committee::Committee::validity_threshold) weight, and
252/// no correct validator is ever convictable.
253///
254/// This is stated and proved in [`crate::justification::proof`] rather than here, because its
255/// assumption base is deliberately *weaker*: it must hold precisely when
256/// [`MaxByzantineWeight`](crate::manager::proof::model::MaxByzantineWeight) has failed, which is
257/// the one regime this module says nothing about. See
258/// [`AccountableSafety`](crate::justification::proof::AccountableSafety) for the theorem and
259/// [`AccountabilityScope`](crate::justification::proof::AccountabilityScope) for what it excludes
260/// — notably that incorrect block execution is *not* attributable.
261///
262/// It is worth stating here nonetheless, because it explains why certificates carry an unlocking
263/// round ([`UnlockingRound`]) and a justification chain at all, given that [`CommitAgreement`]
264/// uses neither: they buy accountability, not agreement.
265///
266/// [`UnlockingRound`]: crate::data_types::proof::objects::UnlockingRound
267pub trait Accountability {}
268
269/// **Remark (What safety does not claim).** Three exclusions are worth stating explicitly,
270/// because each is a property a reader may expect [`CommitAgreement`] to carry and it does not.
271///
272/// * **Nothing is claimed about faulty validators' state.** A faulty validator may record any
273///   block at any height. [`CommitAgreement`] constrains which *certificates* can exist; the
274///   observable consequence in [`TipAdvancesOnlyOnValidCertificate`] is about correct validators.
275/// * **Nothing is claimed about progress.** An execution in which no block is ever committed
276///   satisfies every result in this module. In particular a super owner that issues two
277///   conflicting fast proposals can split the vote and wedge the height permanently, and that is
278///   a liveness failure, not a safety one — see `linera_core::proof::liveness`.
279/// * **Nothing is claimed when [`MaxByzantineWeight`] fails.** Above the fault bound,
280///   [`CorrectValidatorInIntersection`] fails and conflicting commits become possible. What
281///   remains is [`Accountability`].
282///
283/// [`MaxByzantineWeight`]: crate::manager::proof::model::MaxByzantineWeight
284pub trait SafetyScope: CommitAgreement + SafetyStateRecovery + OneValidationVotePerRound {}