linera_chain/manager/proof/locking.rs
1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Locking invariants: what a correct validator's committed-to state can look like.
5//!
6//! These are *protocol invariants*: each is proved by induction over the transitions of one
7//! consensus instance ([`ConsensusInstance`]), and each proof discharges the induction by
8//! enumerating every method that writes the relevant field. The enumerations are the
9//! load-bearing part — a new writer invalidates them — so each invariant names its writers
10//! explicitly.
11//!
12//! The writer sets, for reference:
13//!
14//! | field | writers |
15//! |---|---|
16//! | [`locking_block`] | private `update_locking` (from [`create_vote`] ×3 and [`create_final_vote`]); [`ManagerSafetySnapshot::restore`]; [`reset`] |
17//! | [`confirmed_vote`] | [`create_vote`] (fast branch); [`create_final_vote`]; [`ManagerSafetySnapshot::restore`]; [`reset`] |
18//! | [`validated_vote`] | [`create_vote`] (both branches); [`create_final_vote`]; [`ManagerSafetySnapshot::restore`]; [`reset`] |
19//! | [`timeout_vote`] | [`create_timeout_vote`]; [`ManagerSafetySnapshot::restore`]; [`reset`] |
20//! | [`fallback_vote`] | [`vote_fallback`]; [`ManagerSafetySnapshot::restore`]; [`reset`] |
21//! | [`timeout`] | [`handle_timeout_certificate`]; [`reset`] |
22//! | [`proposed`] | private `update_proposed` (from [`create_vote`]); [`reset`] |
23//! | [`signed_proposal`] | [`update_signed_proposal`]; private `update_proposed` (clears it); [`reset`] |
24//!
25//! [`ConsensusInstance`]: crate::manager::proof::model::ConsensusInstance
26//! [`locking_block`]: crate::manager::ChainManager::locking_block
27//! [`confirmed_vote`]: field@crate::manager::ChainManager::confirmed_vote
28//! [`validated_vote`]: field@crate::manager::ChainManager::validated_vote
29//! [`timeout_vote`]: crate::manager::ChainManager::timeout_vote
30//! [`fallback_vote`]: crate::manager::ChainManager::fallback_vote
31//! [`timeout`]: crate::manager::ChainManager::timeout
32//! [`proposed`]: crate::manager::ChainManager::proposed
33//! [`signed_proposal`]: crate::manager::ChainManager::signed_proposal
34//! [`create_vote`]: crate::manager::ChainManager::create_vote
35//! [`create_final_vote`]: crate::manager::ChainManager::create_final_vote
36//! [`create_timeout_vote`]: crate::manager::ChainManager::create_timeout_vote
37//! [`vote_fallback`]: crate::manager::ChainManager::vote_fallback
38//! [`handle_timeout_certificate`]: crate::manager::ChainManager::handle_timeout_certificate
39//! [`update_signed_proposal`]: crate::manager::ChainManager::update_signed_proposal
40//! [`reset`]: crate::manager::ChainManager::reset
41//! [`ManagerSafetySnapshot::restore`]: crate::manager::ManagerSafetySnapshot::restore
42
43use crate::{
44 data_types::proof::quorum::{
45 CertificateCarriesCorrectVote, CertificateEmbedsQuorum, CorrectSignerCastItsVote,
46 CorrectValidatorInIntersection,
47 },
48 manager::proof::{
49 model::{ConsensusInstance, DurablePersistence, EpochAgreement, SequentialChainState},
50 rounds::{CurrentRoundMonotone, RoundFloor, VoteRoundBelowCurrentRound},
51 voting::{
52 ConfirmationNeedsValidatedCertificate, ConfirmationOnlyInCurrentRound,
53 FastConfirmationNeedsEmptyLock, NoValidationInFastRound, ProposalGate,
54 ValidationRoundStrictlyIncreases, VoteConstructionSites,
55 },
56 },
57};
58
59/// **Definition (Lock).** The *lock* of an instance is [`ChainManager::locking_block`], and its
60/// *lock round* is [`LockingBlock::round`] of that value, or `⊥` when the field is `None`, with
61/// `⊥` below every round.
62///
63/// A lock records the most recent block this validator may already have helped confirm: either a
64/// [`LockingBlock::Regular`] — a [`ValidatedBlockCertificate`], proof that a quorum validated
65/// that block in that round — or a [`LockingBlock::Fast`], the super owner's original fast-round
66/// proposal. Both are re-proposable: a client reads the lock out of
67/// [`ChainManagerInfo::requested_locking`] and re-proposes it in a higher round.
68///
69/// [`ChainManager::locking_block`]: crate::manager::ChainManager::locking_block
70/// [`LockingBlock::round`]: crate::manager::LockingBlock::round
71/// [`LockingBlock::Regular`]: crate::manager::LockingBlock::Regular
72/// [`LockingBlock::Fast`]: crate::manager::LockingBlock::Fast
73/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
74/// [`ChainManagerInfo::requested_locking`]: crate::manager::ChainManagerInfo::requested_locking
75pub trait Lock {}
76
77/// **Invariant (The lock round never decreases).** Within one consensus instance, the lock round
78/// is non-decreasing.
79///
80/// *Proof.* By the writer table in the [module documentation](self), [`locking_block`] is
81/// written only by the private `update_locking`, by [`ManagerSafetySnapshot::restore`], and by
82/// [`reset`] (which by [`ConsensusInstance`] ends the instance).
83///
84/// `update_locking` begins with
85///
86/// ```text
87/// if let Some(old_locked) = self.locking_block.get() {
88/// if old_locked.round() >= locking.round() { return Ok(()); }
89/// }
90/// ```
91///
92/// so it writes only a strictly higher round. All four call sites go through it: the two
93/// `update_locking` calls in the `Regular` and `Fast` arms of [`create_vote`]'s `match`, the one
94/// in the `None` arm, and the one in [`create_final_vote`].
95///
96/// [`ManagerSafetySnapshot::restore`] is handled by [`SafetyStateRecovery`]. ∎
97///
98/// [`locking_block`]: crate::manager::ChainManager::locking_block
99/// [`create_vote`]: crate::manager::ChainManager::create_vote
100/// [`create_final_vote`]: crate::manager::ChainManager::create_final_vote
101/// [`reset`]: crate::manager::ChainManager::reset
102/// [`ManagerSafetySnapshot::restore`]: crate::manager::ManagerSafetySnapshot::restore
103/// [`ConsensusInstance`]: crate::manager::proof::model::ConsensusInstance
104pub trait LockRoundMonotone: Lock + ConsensusInstance {}
105
106/// **Invariant (The confirmed-vote round never decreases).** Within one consensus instance, if
107/// [`confirmed_vote`] is `Some` with round `p`, it never later holds a vote with round `< p`.
108///
109/// *Proof.* By the writer table in the [module documentation](self) the field has three
110/// in-instance writers.
111///
112/// *[`create_final_vote`] at round `r`.* By [`ConfirmationOnlyInCurrentRound`] it writes only
113/// when `current_round == r`. If a previous confirmation vote had round `p`, then by
114/// [`VoteRoundBelowCurrentRound`] the current round was `≥ p` at that time, and by
115/// [`CurrentRoundMonotone`] it still is. Hence `r = current_round ≥ p`.
116///
117/// *[`create_vote`], fast branch.* Writes a vote in [`Round::Fast`], the minimum round — but by
118/// [`FastConfirmationNeedsEmptyLock`] this branch is reachable only when [`confirmed_vote`] was
119/// `None`, so there is no earlier round to undercut.
120///
121/// *[`ManagerSafetySnapshot::restore`].* See [`SafetyStateRecovery`]. ∎
122///
123/// [`confirmed_vote`]: field@crate::manager::ChainManager::confirmed_vote
124/// [`create_vote`]: crate::manager::ChainManager::create_vote
125/// [`create_final_vote`]: crate::manager::ChainManager::create_final_vote
126/// [`Round::Fast`]: linera_base::data_types::Round::Fast
127/// [`ManagerSafetySnapshot::restore`]: crate::manager::ManagerSafetySnapshot::restore
128pub trait ConfirmedVoteRoundMonotone:
129 ConfirmationOnlyInCurrentRound
130 + FastConfirmationNeedsEmptyLock
131 + VoteRoundBelowCurrentRound
132 + CurrentRoundMonotone
133{
134}
135
136/// **Invariant (Cast validation rounds leave a floor).** If a correct validator has ever cast a
137/// validation vote in round `s` during an instance, then from that moment on
138///
139/// ```text
140/// max(validated_vote.round, lock round) ≥ s
141/// ```
142///
143/// where an absent field contributes `⊥`.
144///
145/// This is the invariant that survives [`create_final_vote`] clearing [`validated_vote`], and it
146/// is what makes [`OneValidationVotePerRound`] hold over a whole instance rather than only
147/// between confirmations.
148///
149/// *Proof.* Induction over the transitions of the instance.
150///
151/// *Base.* Immediately after the vote, [`validated_vote`] is `Some(_, s)` — the non-fast branch
152/// of [`create_vote`] ends with `self.validated_vote.get_mut().insert(vote)`.
153///
154/// *Step.* Only two transitions can lower either side of the maximum. The lock round is
155/// non-decreasing by [`LockRoundMonotone`], so only [`validated_vote`] can fall, and by the
156/// writer table it is set to `None` in exactly two places:
157///
158/// * [`create_vote`], fast branch (`self.validated_vote.set(None)`). Unreachable here: that
159/// branch requires [`ChainManager::check_proposed_block`] to have accepted a proposal in
160/// [`Round::Fast`] ([`ProposalGate`]), whose [`validated_vote`] guard
161/// `ensure!(new_round > vote.round)` is unsatisfiable for the minimum round unless
162/// [`validated_vote`] is already `None`; and if it is `None`, the induction hypothesis is
163/// carried by the lock, which this branch does not lower.
164/// * [`create_final_vote`] at some round `r`. It clears the field only after
165/// `update_locking(LockingBlock::Regular(validated), blobs)` and only on the branch where a
166/// vote is cast, which by [`ConfirmationOnlyInCurrentRound`] requires `current_round == r`. By
167/// [`VoteRoundBelowCurrentRound`] and [`CurrentRoundMonotone`], `current_round ≥ s`, so
168/// `r ≥ s`. By [`RoundFloor`] and [`LockRoundMonotone`], after `update_locking` the lock round
169/// is `≥ r ≥ s`. So the maximum is preserved by the lock. ∎
170///
171/// [`validated_vote`]: field@crate::manager::ChainManager::validated_vote
172/// [`create_vote`]: crate::manager::ChainManager::create_vote
173/// [`create_final_vote`]: crate::manager::ChainManager::create_final_vote
174/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
175/// [`Round::Fast`]: linera_base::data_types::Round::Fast
176pub trait CastValidationRoundFloor:
177 LockRoundMonotone
178 + ProposalGate
179 + ConfirmationOnlyInCurrentRound
180 + VoteRoundBelowCurrentRound
181 + CurrentRoundMonotone
182 + RoundFloor
183{
184}
185
186/// **Lemma (One validation vote per round).** A correct validator casts at most one validation
187/// vote per round of an instance.
188///
189/// *Proof.* Suppose it casts validation votes for `B₁` and then `B₂` in the same round `s`. By
190/// [`ProposalGate`] the second was preceded by [`ChainManager::check_proposed_block`] returning
191/// `Accept` for a proposal in round `s`, so both of its round guards held at that moment:
192///
193/// * `ensure!(new_round > vote.round)` for [`validated_vote`], and
194/// * `ensure!(locking_block.round() < new_round)` for the lock.
195///
196/// Together these give `max(validated_vote.round, lock round) < s`, contradicting
197/// [`CastValidationRoundFloor`], which the first vote established. (Where "the same vote" is
198/// re-submitted rather than a different block, [`ChainManager::check_proposed_block`] returns
199/// [`Outcome::Skip`] on its first branch and nothing is signed.)
200///
201/// The argument needs the two votes to be observed by the *same* manager state, which is
202/// [`DurablePersistence`] — a validator that lost its state across a crash could sign twice —
203/// and [`SequentialChainState`], which rules out two concurrent handlers each seeing the
204/// pre-vote state. ∎
205///
206/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
207/// [`validated_vote`]: field@crate::manager::ChainManager::validated_vote
208/// [`Outcome::Skip`]: crate::manager::Outcome::Skip
209/// [`SequentialChainState`]: crate::manager::proof::model::SequentialChainState
210pub trait OneValidationVotePerRound:
211 ProposalGate
212 + CastValidationRoundFloor
213 + ValidationRoundStrictlyIncreases
214 + DurablePersistence
215 + SequentialChainState
216{
217}
218
219/// **Lemma (One confirmation vote per round).** A correct validator casts at most one
220/// confirmation vote per round of an instance.
221///
222/// *Proof.* Two cases on the round `r`.
223///
224/// *`r` is [`Round::Fast`].* By [`FastConfirmationNeedsEmptyLock`], such a vote requires the lock
225/// to be `None` beforehand and installs a [`LockingBlock::Fast`] — of round [`Round::Fast`] —
226/// afterwards. A second fast confirmation would again require the lock to be `None`,
227/// contradicting [`LockRoundMonotone`]. (Directly: the lock guard
228/// `ensure!(locking_block.round() < Round::Fast)` in [`ChainManager::check_proposed_block`] is
229/// unsatisfiable.)
230///
231/// *`r` is not [`Round::Fast`].* By [`ConfirmationNeedsValidatedCertificate`] the vote comes from
232/// [`create_final_vote`], which is guarded by [`ChainManager::check_validated_block`]
233/// ([`ProposalGate`]). That guard contains
234///
235/// ```text
236/// if let Some(locking) = self.locking_block.get() {
237/// ensure!(new_round > locking.round(), ChainError::InsufficientRoundStrict(locking.round()));
238/// }
239/// ```
240///
241/// The first confirmation at round `r` ran `update_locking(LockingBlock::Regular(validated), …)`
242/// with `validated.round == r`, so afterwards the lock round is `≥ r` by [`RoundFloor`] and
243/// [`LockRoundMonotone`]. A second certificate in round `r` therefore fails `new_round >
244/// locking.round()` and no second vote is cast. (If the second certificate is for the same block
245/// and round, [`ChainManager::check_validated_block`] returns [`Outcome::Skip`] on its first
246/// branch instead.)
247///
248/// As in [`OneValidationVotePerRound`], the argument consumes [`DurablePersistence`] and
249/// [`SequentialChainState`]. ∎
250///
251/// **Where this is fragile.** In the non-fast case the guard lives in
252/// [`ChainManager::check_validated_block`], i.e. at the *call site*, not inside
253/// [`create_final_vote`] — which re-checks only [`ChainManager::current_round`] and would sign
254/// again for a different block certified in the same round. See [`ProposalGate`].
255///
256/// [`Round::Fast`]: linera_base::data_types::Round::Fast
257/// [`LockingBlock::Fast`]: crate::manager::LockingBlock::Fast
258/// [`create_final_vote`]: crate::manager::ChainManager::create_final_vote
259/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
260/// [`ChainManager::check_validated_block`]: crate::manager::ChainManager::check_validated_block
261/// [`ChainManager::current_round`]: method@crate::manager::ChainManager::current_round
262/// [`Outcome::Skip`]: crate::manager::Outcome::Skip
263/// [`SequentialChainState`]: crate::manager::proof::model::SequentialChainState
264pub trait OneConfirmationVotePerRound:
265 ProposalGate
266 + FastConfirmationNeedsEmptyLock
267 + ConfirmationNeedsValidatedCertificate
268 + LockRoundMonotone
269 + RoundFloor
270 + DurablePersistence
271 + SequentialChainState
272{
273}
274
275/// **Lemma (At most one validated block per round).** For a given chain and height, all
276/// valid [`ValidatedBlockCertificate`]s certified in the same round certify the same block.
277///
278/// *Proof.* Let two such certificates certify `B₁` and `B₂` in round `s`. By
279/// [`EpochAgreement`] they are judged against the same committee, so by
280/// [`CertificateEmbedsQuorum`] their signer sets are two quorums of it, and by
281/// [`CorrectValidatorInIntersection`] some correct validator `v` signed both. By
282/// [`CorrectSignerCastItsVote`], `v` cast validation votes for `B₁` and for `B₂`, both in
283/// round `s`. By [`OneValidationVotePerRound`], `B₁ = B₂`. ∎
284///
285/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
286/// [`CertificateEmbedsQuorum`]: crate::data_types::proof::quorum::CertificateEmbedsQuorum
287pub trait UniqueValidatedBlockPerRound:
288 OneValidationVotePerRound
289 + CorrectValidatorInIntersection
290 + CertificateEmbedsQuorum
291 + CorrectSignerCastItsVote
292 + EpochAgreement
293{
294}
295
296/// **Lemma (No validated block certificate in the fast round).** No valid
297/// [`ValidatedBlockCertificate`] is certified in
298/// [`Round::Fast`](linera_base::data_types::Round::Fast).
299///
300/// *Proof.* By [`CertificateCarriesCorrectVote`] such a certificate would require a correct
301/// validator to have cast a validation vote in [`Round::Fast`](linera_base::data_types::Round::Fast),
302/// which [`NoValidationInFastRound`] forbids. ∎
303///
304/// This is why the fast round is not a "round" in the ordinary sense: it has a confirmation step
305/// but no validation step, and therefore no certificate a later round could be unlocked by.
306///
307/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
308pub trait NoValidatedBlockInFastRound:
309 NoValidationInFastRound + CertificateCarriesCorrectVote
310{
311}
312
313/// **Lemma (Safety state survives a local reset).** After
314/// `ChainWorkerState::reset_and_reexecute_chain`, the manager's lock and cast votes are at least
315/// what they were before the reset, and no invariant of this module is broken.
316///
317/// This is the one transition that writes [`locking_block`], [`confirmed_vote`] and
318/// [`validated_vote`] without going through the voting path, so [`LockRoundMonotone`],
319/// [`ConfirmedVoteRoundMonotone`] and [`CastValidationRoundFloor`] each owe it an argument.
320///
321/// *Proof.* The procedure captures [`ManagerSafetySnapshot::capture`] *before* wiping storage,
322/// replays the chain's confirmed blocks from storage, and then calls
323/// [`ManagerSafetySnapshot::restore`] — but only under the explicit guard
324/// `new_tip_height == tip_height`, so the restored fields belong to the same pending height and
325/// hence the same consensus instance in the sense of [`ConsensusInstance`].
326///
327/// Replay only feeds [`ConfirmedBlockCertificate`]s to `process_confirmed_block`, which casts no
328/// vote and calls [`reset`] once per replayed height; so at the moment of restore the five
329/// snapshot fields are exactly what the final `reset` left them — `None`. The restore therefore
330/// re-installs the pre-reset values rather than overwriting newer ones, and each field returns to
331/// a value it genuinely held. The three invariants above are stated over the values a correct
332/// validator has committed to, so restoring them re-establishes rather than violates them.
333///
334/// The subtle point is [`current_round`], which the snapshot does **not** capture or restore: it
335/// is left at [`ChainOwnership::first_round`] by the last [`reset`], potentially far below the
336/// restored lock. That cannot be exploited, because every path that could act on the lower round
337/// re-derives the round from the lock first:
338///
339/// * [`create_final_vote`] calls `update_locking` then `update_current_round` before comparing
340/// against `round`, so by [`RoundFloor`] the comparison sees a round `≥` the restored lock
341/// round — this is exactly [`ConfirmationOnlyInCurrentRound`];
342/// * [`ChainManager::check_proposed_block`] rejects any proposal not strictly above the lock
343/// round, regardless of [`current_round`].
344///
345/// So the restored lock, not the round register, is what binds. ∎
346///
347/// **Residual obligation.** [`ManagerSafetySnapshot`] records no height of its own; the
348/// correspondence between snapshot and instance rests entirely on the caller's
349/// `new_tip_height == tip_height` guard. A future caller of
350/// [`ManagerSafetySnapshot::restore`] must reproduce it.
351///
352/// [`locking_block`]: crate::manager::ChainManager::locking_block
353/// [`confirmed_vote`]: field@crate::manager::ChainManager::confirmed_vote
354/// [`validated_vote`]: field@crate::manager::ChainManager::validated_vote
355/// [`current_round`]: field@crate::manager::ChainManager::current_round
356/// [`create_final_vote`]: crate::manager::ChainManager::create_final_vote
357/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
358/// [`reset`]: crate::manager::ChainManager::reset
359/// [`ManagerSafetySnapshot`]: crate::manager::ManagerSafetySnapshot
360/// [`ManagerSafetySnapshot::capture`]: crate::manager::ManagerSafetySnapshot::capture
361/// [`ManagerSafetySnapshot::restore`]: crate::manager::ManagerSafetySnapshot::restore
362/// [`ConfirmedBlockCertificate`]: crate::types::ConfirmedBlockCertificate
363/// [`ConsensusInstance`]: crate::manager::proof::model::ConsensusInstance
364/// [`ChainOwnership::first_round`]: linera_base::ownership::ChainOwnership::first_round
365pub trait SafetyStateRecovery:
366 ConsensusInstance + LockRoundMonotone + RoundFloor + ConfirmationOnlyInCurrentRound
367{
368}
369
370/// **Remark (Votes a correct validator may hold simultaneously).** Nothing above forbids a
371/// correct validator from holding a [`confirmed_vote`] and a [`validated_vote`] at once — it
372/// does, whenever it validates in a round above its last confirmation. What the invariants
373/// forbid is *two of the same kind in the same round*. The reporting projection
374/// [`ChainManagerInfo::pending`] picks whichever is higher, which is why a client observing a
375/// validator sees a single "pending" vote even though two are stored.
376///
377/// Similarly, [`timeout_vote`] and [`fallback_vote`] may both be set: they are votes on the same
378/// [`Timeout`] value in different rounds ([`FallbackVote`]), so they are not conflicting votes in
379/// the sense of [`OneValidationVotePerRound`] — and if their rounds coincide, so do their
380/// payloads, and hence their signatures.
381///
382/// [`confirmed_vote`]: field@crate::manager::ChainManager::confirmed_vote
383/// [`validated_vote`]: field@crate::manager::ChainManager::validated_vote
384/// [`timeout_vote`]: crate::manager::ChainManager::timeout_vote
385/// [`fallback_vote`]: crate::manager::ChainManager::fallback_vote
386/// [`ChainManagerInfo::pending`]: crate::manager::ChainManagerInfo::pending
387/// [`Timeout`]: crate::block::Timeout
388/// [`FallbackVote`]: crate::manager::proof::timeouts::FallbackVote
389pub trait SimultaneousVotes: VoteConstructionSites {}