linera_chain/manager/proof/voting.rs
1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Voting rules: what a correct validator's state must look like for it to sign.
5//!
6//! These are *local implementation properties*. Each one follows from a single method's control
7//! flow together with the guards its call sites apply, with no induction over executions. They
8//! are what [`crate::manager::proof::locking`] inducts over, and what
9//! [`CertificateCarriesCorrectVote`] converts into constraints on the certificates that can
10//! exist at all.
11//!
12//! Every statement below is accompanied by a *code correspondence* table naming the method that
13//! implements the transition, the fields it reads and writes, and the preconditions its callers
14//! establish.
15//!
16//! [`CertificateCarriesCorrectVote`]: crate::data_types::proof::quorum::CertificateCarriesCorrectVote
17
18use crate::manager::proof::model::{CorrectValidator, SequentialChainState};
19
20/// **Lemma (Vote construction sites).** A correct validator signs a block-related vote only in
21/// [`ChainManager::create_vote`] and [`ChainManager::create_final_vote`], and a timeout vote only
22/// in [`ChainManager::create_timeout_vote`] and [`ChainManager::vote_fallback`]. Specifically:
23///
24/// | vote kind | round of the vote | sole producer |
25/// |---|---|---|
26/// | [`Validated`] | `proposal.content.round`, never [`Round::Fast`] | [`ChainManager::create_vote`], `else` branch |
27/// | [`Confirmed`] | [`Round::Fast`] | [`ChainManager::create_vote`], `if round.is_fast()` branch |
28/// | [`Confirmed`] | `validated.round` | [`ChainManager::create_final_vote`] |
29/// | [`Timeout`] | [`ChainManager::current_round`] | [`ChainManager::create_timeout_vote`] |
30/// | [`Timeout`] | `Round::SingleLeader(u32::MAX)` | [`ChainManager::vote_fallback`] |
31///
32/// *Proof.* By [`ValidatorVote`], a vote exists only if one of [`Vote::new`],
33/// [`Vote::new_with_unlocking_round`] or [`Vote::new_with_first_round`] was called with the
34/// validator's key. Outside test-only code there are exactly five such calls in the workspace,
35/// all in `linera_chain::manager`, and they are the five rows above: `Vote::new` in
36/// [`create_timeout_vote`] and in [`vote_fallback`]; `Vote::new_with_first_round` in the
37/// fast-round branch of [`create_vote`] and in [`create_final_vote`];
38/// `Vote::new_with_unlocking_round` in the non-fast branch of [`create_vote`]. In each case the
39/// round passed is the one tabulated: [`create_vote`] passes `proposal.content.round`, guarded
40/// by `round.is_fast()` into one branch or the other; [`create_final_vote`] passes
41/// `validated.round`; [`create_timeout_vote`] passes its `round` argument, which it has just
42/// checked to equal [`ChainManager::current_round`]; [`vote_fallback`] passes the constant
43/// `Round::SingleLeader(u32::MAX)`. By [`CorrectValidator`] no other code path of a correct
44/// validator holds its key. ∎
45///
46/// **Where this is fragile.** The claim is an exhaustive-search argument over call sites, so it
47/// is invalidated by adding a sixth call. A new signing site must either be shown to preserve
48/// [`OneValidationVotePerRound`] and [`OneConfirmationVotePerRound`], or be added to this table.
49///
50/// [`ChainManager::create_vote`]: crate::manager::ChainManager::create_vote
51/// [`ChainManager::create_final_vote`]: crate::manager::ChainManager::create_final_vote
52/// [`ChainManager::create_timeout_vote`]: crate::manager::ChainManager::create_timeout_vote
53/// [`ChainManager::vote_fallback`]: crate::manager::ChainManager::vote_fallback
54/// [`ChainManager::current_round`]: method@crate::manager::ChainManager::current_round
55/// [`create_vote`]: crate::manager::ChainManager::create_vote
56/// [`create_final_vote`]: crate::manager::ChainManager::create_final_vote
57/// [`create_timeout_vote`]: crate::manager::ChainManager::create_timeout_vote
58/// [`vote_fallback`]: crate::manager::ChainManager::vote_fallback
59/// [`Validated`]: crate::types::CertificateKind::Validated
60/// [`Confirmed`]: crate::types::CertificateKind::Confirmed
61/// [`Timeout`]: crate::types::CertificateKind::Timeout
62/// [`Round::Fast`]: linera_base::data_types::Round::Fast
63/// [`ValidatorVote`]: crate::data_types::proof::objects::ValidatorVote
64/// [`Vote::new`]: crate::data_types::Vote::new
65/// [`Vote::new_with_unlocking_round`]: crate::data_types::Vote::new_with_unlocking_round
66/// [`Vote::new_with_first_round`]: crate::data_types::Vote::new_with_first_round
67/// [`OneValidationVotePerRound`]: crate::manager::proof::locking::OneValidationVotePerRound
68/// [`OneConfirmationVotePerRound`]: crate::manager::proof::locking::OneConfirmationVotePerRound
69pub trait VoteConstructionSites: CorrectValidator {}
70
71/// **Lemma (Proposal gate).** A correct validator reaches [`ChainManager::create_vote`] for a
72/// proposal `p` only after [`ChainManager::check_proposed_block`] returned
73/// [`Outcome::Accept`] for `p` against the same manager state.
74///
75/// Similarly, it reaches [`ChainManager::create_final_vote`] for a certificate `c` only after
76/// [`ChainManager::check_validated_block`] returned [`Outcome::Accept`] for `c`, and after
77/// `c.check(committee)` succeeded.
78///
79/// *Proof.* Both methods have exactly one caller in the workspace outside tests, in
80/// `linera_core::chain_worker::state`:
81///
82/// * `create_vote` is called at the end of `ChainWorkerState::try_handle_block_proposal`, which
83/// earlier `match`es on `chain.manager.check_proposed_block(&proposal)` and returns without
84/// voting on both non-`Accept` arms — `Ok(Outcome::Skip)` returns the unchanged chain info,
85/// and `Err(_)` returns the error (after, at most, recording the proposal via
86/// [`ChainManager::update_signed_proposal`], which casts no vote).
87/// * `create_final_vote` is called at the end of `ChainWorkerState::process_validated_block`,
88/// which earlier evaluates `certificate.check(&committee)?` and then
89/// `should_skip_validated_block()?`, a closure wrapping
90/// `chain.manager.check_validated_block(&certificate)`. A `Skip` outcome returns early; an
91/// `Err` propagates via `?`. Only `Ok(Accept)` falls through.
92///
93/// By [`SequentialChainState`] no other transition on this instance interleaves, so the state
94/// the guard inspected is the state `create_vote` / `create_final_vote` then mutates. ∎
95///
96/// **Where this is fragile.** The guards are at the call sites, not inside the signing methods:
97/// `create_final_vote` in particular re-checks only [`ChainManager::current_round`], and would
98/// happily sign a second confirmation in the same round if invoked directly. A new caller must
99/// replicate the guards. This is the single largest gap between "the module is correct" and "the
100/// module cannot be misused".
101///
102/// [`ChainManager::create_vote`]: crate::manager::ChainManager::create_vote
103/// [`ChainManager::create_final_vote`]: crate::manager::ChainManager::create_final_vote
104/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
105/// [`ChainManager::check_validated_block`]: crate::manager::ChainManager::check_validated_block
106/// [`ChainManager::update_signed_proposal`]: crate::manager::ChainManager::update_signed_proposal
107/// [`ChainManager::current_round`]: method@crate::manager::ChainManager::current_round
108/// [`Outcome::Accept`]: crate::manager::Outcome::Accept
109pub trait ProposalGate: SequentialChainState {}
110
111/// **Lemma (Validation rounds strictly increase).** If a correct validator's
112/// [`validated_vote`] holds a vote in round `s`, it casts no further validation vote in any
113/// round `≤ s` while that field still holds that vote.
114///
115/// *Code correspondence.*
116///
117/// | | |
118/// |---|---|
119/// | transition | [`ChainManager::check_proposed_block`] |
120/// | reads | [`proposed`], [`validated_vote`], [`locking_block`], [`confirmed_vote`], [`current_round`], [`ownership`] |
121/// | writes | nothing |
122/// | precondition | none |
123/// | establishes | this lemma, [`UnlockingRequiresHigherCertificate`] |
124///
125/// *Proof.* By [`ProposalGate`] a validation vote in round `r` requires
126/// [`ChainManager::check_proposed_block`] to have returned `Accept` for a proposal in round `r`.
127/// That method contains
128///
129/// ```text
130/// if let Some(vote) = self.validated_vote() {
131/// ensure!(new_round > vote.round, ChainError::InsufficientRoundStrict(vote.round));
132/// }
133/// ```
134///
135/// so `Accept` with `validated_vote == Some(_, s)` requires `r > s`. ∎
136///
137/// Note the qualifier "while that field still holds that vote":
138/// [`ChainManager::create_final_vote`] clears [`validated_vote`], so this lemma alone does not
139/// give a per-round bound over the whole life of an instance. [`CastValidationRoundFloor`]
140/// supplies what is missing.
141///
142/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
143/// [`ChainManager::create_final_vote`]: crate::manager::ChainManager::create_final_vote
144/// [`validated_vote`]: field@crate::manager::ChainManager::validated_vote
145/// [`confirmed_vote`]: field@crate::manager::ChainManager::confirmed_vote
146/// [`current_round`]: field@crate::manager::ChainManager::current_round
147/// [`locking_block`]: crate::manager::ChainManager::locking_block
148/// [`proposed`]: crate::manager::ChainManager::proposed
149/// [`ownership`]: crate::manager::ChainManager::ownership
150/// [`UnlockingRequiresHigherCertificate`]: self::UnlockingRequiresHigherCertificate
151/// [`CastValidationRoundFloor`]: crate::manager::proof::locking::CastValidationRoundFloor
152pub trait ValidationRoundStrictlyIncreases: ProposalGate {}
153
154/// **Lemma (A validation vote past a lock needs a higher certificate).** Suppose a correct
155/// validator casts a validation vote for block `B` in round `r`, and let `(A, p)` be the value
156/// of its [`confirmed_vote`] immediately before. Then `p` is defined only if the proposal
157/// carried an [`OriginalProposal`], and:
158///
159/// * if the proposal is a **regular retry** carrying a certificate `c` (necessarily a valid
160/// [`ValidatedBlockCertificate`] for `B`, in a round `c.round < r`), then
161/// `p ≤ c.round` when `A` matches `B`'s proposal, and `p < c.round` otherwise;
162/// * if the proposal is a **fast retry**, then `p` is [`Round::Fast`] and `A` matches `B`'s
163/// proposal;
164/// * a **fresh** proposal is rejected outright.
165///
166/// This is the hinge of the safety argument: it says a correct validator abandons a block it has
167/// confirmed only when shown a quorum that validated the new block in a round *strictly above*
168/// its own confirmation.
169///
170/// *Code correspondence.*
171///
172/// | | |
173/// |---|---|
174/// | transition | [`ChainManager::check_proposed_block`], final `ensure!` |
175/// | reads | [`confirmed_vote`], `proposal.original_proposal` |
176/// | writes | nothing |
177/// | precondition | the proposal passed `check_invariants`, `check_signature` and — for a regular retry — `certificate.check(committee)`, all in `try_handle_block_proposal` |
178///
179/// *Proof.* By [`ProposalGate`], `Accept` was returned, so the final `ensure!` of
180/// [`ChainManager::check_proposed_block`] held. With `vote = (A, p)` it evaluates
181///
182/// ```text
183/// match proposal.original_proposal.as_ref() {
184/// None => false,
185/// Some(OriginalProposal::Regular { certificate }) =>
186/// if vote.value().matches_proposed_block(new_block) {
187/// vote.round <= certificate.round
188/// } else {
189/// vote.round < certificate.round
190/// },
191/// Some(OriginalProposal::Fast(_)) =>
192/// vote.round.is_fast() && vote.value().matches_proposed_block(new_block),
193/// }
194/// ```
195///
196/// which is the case distinction claimed. That the retried certificate is *valid* and certifies
197/// exactly `B` comes from the caller: `try_handle_block_proposal` calls
198/// `certificate.check(&committee)?` on the `Regular` arm, and
199/// [`BlockProposal::check_invariants`] — also called there — requires
200/// `certificate.check_value(&ValidatedBlock::new(outcome.with(block)))`, i.e. the certificate
201/// certifies the very block being proposed, and `content.round > certificate.round`. ∎
202///
203/// Note `matches_proposed_block` compares the [`ProposedBlock`] only, not the execution outcome;
204/// [`FastRetryPreservesBlock`] is where that gap is closed.
205///
206/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
207/// [`confirmed_vote`]: field@crate::manager::ChainManager::confirmed_vote
208/// [`OriginalProposal`]: crate::data_types::OriginalProposal
209/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
210/// [`BlockProposal::check_invariants`]: crate::data_types::BlockProposal::check_invariants
211/// [`ProposedBlock`]: crate::data_types::ProposedBlock
212/// [`Round::Fast`]: linera_base::data_types::Round::Fast
213/// [`FastRetryPreservesBlock`]: crate::manager::proof::safety::FastRetryPreservesBlock
214pub trait UnlockingRequiresHigherCertificate: ProposalGate {}
215
216/// **Lemma (No validation vote in the fast round).** A correct validator never casts a
217/// [`Validated`](crate::types::CertificateKind::Validated) vote in
218/// [`Round::Fast`](linera_base::data_types::Round::Fast).
219///
220/// *Proof.* By [`VoteConstructionSites`] validation votes are produced only in the `else` branch
221/// of `if round.is_fast()` in [`ChainManager::create_vote`], where `round` is the vote's round.
222/// ∎
223///
224/// [`ChainManager::create_vote`]: crate::manager::ChainManager::create_vote
225pub trait NoValidationInFastRound: VoteConstructionSites {}
226
227/// **Lemma (A fast confirmation requires an empty lock and no prior vote).** If a correct
228/// validator casts a confirmation vote in [`Round::Fast`], then immediately before that vote its
229/// [`locking_block`], [`validated_vote`] and [`confirmed_vote`] were all `None`; and immediately
230/// after, [`locking_block`] holds a [`LockingBlock::Fast`] for the very block confirmed.
231///
232/// *Proof.* By [`VoteConstructionSites`] such a vote comes from the `if round.is_fast()` branch
233/// of [`ChainManager::create_vote`], so the proposal's round is [`Round::Fast`]. By
234/// [`ProposalGate`], [`ChainManager::check_proposed_block`] returned `Accept` for it. Since
235/// [`Round::Fast`] is the minimum of the round order ([`RoundOrder`]):
236///
237/// * the [`locking_block`] guard `ensure!(locking_block.round() < new_round)` is unsatisfiable
238/// for `new_round == Round::Fast`, so [`locking_block`] was `None`;
239/// * the [`validated_vote`] guard `ensure!(new_round > vote.round)` is likewise unsatisfiable,
240/// so [`validated_vote`] was `None`;
241/// * for [`confirmed_vote`], [`BlockProposal::check_invariants`] forces a fast-round proposal to
242/// have `original_proposal == None` — a `Fast` original requires `content.round > Round::Fast`
243/// and a `Regular` original requires `content.round > certificate.round ≥ Round::Fast` — so
244/// the `ensure!` of [`UnlockingRequiresHigherCertificate`] takes the `None => false` arm and
245/// would reject. Hence [`confirmed_vote`] was `None`.
246///
247/// For the post-state: with `original_proposal == None` and `round.is_fast()`, the third arm of
248/// the `match` in [`ChainManager::create_vote`] runs `update_locking(LockingBlock::Fast(
249/// proposal.clone()), …)` under `self.locking_block.get().is_none()`, which we just established,
250/// so the lock is installed on the proposal being confirmed. ∎
251///
252/// [`Round::Fast`]: linera_base::data_types::Round::Fast
253/// [`ChainManager::create_vote`]: crate::manager::ChainManager::create_vote
254/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
255/// [`BlockProposal::check_invariants`]: crate::data_types::BlockProposal::check_invariants
256/// [`LockingBlock::Fast`]: crate::manager::LockingBlock::Fast
257/// [`locking_block`]: crate::manager::ChainManager::locking_block
258/// [`validated_vote`]: field@crate::manager::ChainManager::validated_vote
259/// [`confirmed_vote`]: field@crate::manager::ChainManager::confirmed_vote
260/// [`RoundOrder`]: crate::manager::proof::model::RoundOrder
261pub trait FastConfirmationNeedsEmptyLock:
262 VoteConstructionSites + ProposalGate + UnlockingRequiresHigherCertificate
263{
264}
265
266/// **Lemma (A non-fast confirmation requires a validated certificate in the same round).** If a
267/// correct validator casts a confirmation vote for block `A` in a round `r` other than
268/// [`Round::Fast`](linera_base::data_types::Round::Fast), then a [`ValidatedBlockCertificate`]
269/// for `A` in round `r`, valid for the committee of its epoch, existed at that moment.
270///
271/// *Code correspondence.*
272///
273/// | | |
274/// |---|---|
275/// | transition | [`ChainManager::create_final_vote`] |
276/// | reads | [`locking_block`], [`current_round`], [`ownership`] |
277/// | writes | [`locking_block`], [`locking_blobs`], [`current_round`], [`round_timeout`], [`confirmed_vote`], [`validated_vote`] |
278/// | precondition | `certificate.check(committee)` and [`ChainManager::check_validated_block`] both succeeded ([`ProposalGate`]) |
279/// | preserves | [`LockRoundMonotone`], [`ConfirmedVoteRoundMonotone`], [`CastValidationRoundFloor`] |
280///
281/// *Proof.* By [`VoteConstructionSites`] a confirmation vote in a non-fast round comes from
282/// [`ChainManager::create_final_vote`], whose vote round is `validated.round` for its
283/// [`ValidatedBlockCertificate`] argument `validated`, and whose voted value is
284/// `ConfirmedBlock::new(validated.inner().block().clone())` — the same block. By
285/// [`ProposalGate`] the caller verified `certificate.check(&committee)` before passing it in. ∎
286///
287/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
288/// [`ChainManager::create_final_vote`]: crate::manager::ChainManager::create_final_vote
289/// [`ChainManager::check_validated_block`]: crate::manager::ChainManager::check_validated_block
290/// [`locking_block`]: crate::manager::ChainManager::locking_block
291/// [`locking_blobs`]: crate::manager::ChainManager::locking_blobs
292/// [`current_round`]: field@crate::manager::ChainManager::current_round
293/// [`round_timeout`]: crate::manager::ChainManager::round_timeout
294/// [`ownership`]: crate::manager::ChainManager::ownership
295/// [`confirmed_vote`]: field@crate::manager::ChainManager::confirmed_vote
296/// [`validated_vote`]: field@crate::manager::ChainManager::validated_vote
297/// [`LockRoundMonotone`]: crate::manager::proof::locking::LockRoundMonotone
298/// [`ConfirmedVoteRoundMonotone`]: crate::manager::proof::locking::ConfirmedVoteRoundMonotone
299/// [`CastValidationRoundFloor`]: crate::manager::proof::locking::CastValidationRoundFloor
300pub trait ConfirmationNeedsValidatedCertificate: VoteConstructionSites + ProposalGate {}
301
302/// **Lemma (A non-fast confirmation happens only in the current round).** When
303/// [`ChainManager::create_final_vote`] casts a confirmation vote in round `r`,
304/// [`ChainManager::current_round`] equals `r` at that moment — and it had already been raised to
305/// at least `r` earlier in the same call.
306///
307/// *Proof.* [`ChainManager::create_final_vote`] executes, in order:
308/// `update_locking(LockingBlock::Regular(validated), blobs)`, which by [`RoundFloor`] leaves the
309/// lock at a round `≥ r`; then `update_current_round(local_time)`, which by the same result
310/// leaves [`ChainManager::current_round`] at least the lock's round, hence `≥ r`; then
311///
312/// ```text
313/// if self.current_round() != round { return Ok(()); }
314/// ```
315///
316/// so the vote is cast only when the two are equal. ∎
317///
318/// This ordering is what makes the guard safe against a stale
319/// [`current_round`](field@crate::manager::ChainManager::current_round): the lock is folded back
320/// into the round *before* the comparison, so a manager whose round register was reset below its
321/// lock cannot be induced to confirm in the lower round. [`SafetyStateRecovery`] uses this.
322///
323/// [`ChainManager::create_final_vote`]: crate::manager::ChainManager::create_final_vote
324/// [`ChainManager::current_round`]: method@crate::manager::ChainManager::current_round
325/// [`RoundFloor`]: crate::manager::proof::rounds::RoundFloor
326/// [`SafetyStateRecovery`]: crate::manager::proof::locking::SafetyStateRecovery
327pub trait ConfirmationOnlyInCurrentRound:
328 VoteConstructionSites + crate::manager::proof::rounds::RoundFloor
329{
330}