linera_chain/manager/proof/timeouts.rs
1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Leaders, round timeouts, and how a height leaves a round it cannot finish in.
5//!
6//! Nothing here is needed for safety — [`CommitAgreement`] holds however rounds advance, and
7//! indeed whether or not they advance at all. These results exist to be consumed by the progress
8//! lemmas in `linera_core::proof::progress`, and to record precisely which rounds can be left by
9//! which means. The last statement, [`RoundsWithoutTimeout`], is a caveat rather than a
10//! guarantee, and is the main reason the liveness theorem is conditional in the way it is.
11//!
12//! **Round advancement is not autonomous.** A validator never advances a round on its own initiative:
13//! it signs a timeout vote only when a client asks it to, through
14//! `ChainInfoQuery::request_leader_timeout`, and it never proposes a block at all. Round
15//! advancement is therefore driven entirely from `linera_core::client`, which is why the
16//! corresponding assumption (`ActiveCorrectDriver`) lives in that crate.
17//!
18//! [`CommitAgreement`]: crate::manager::proof::safety::CommitAgreement
19
20use crate::{
21 data_types::proof::quorum::CertificateCarriesCorrectVote,
22 manager::proof::{
23 model::ConsensusInstance,
24 rounds::{CurrentRoundMonotone, RoundFloor},
25 },
26};
27
28/// **Lemma (Timeout vote conditions).** A correct validator signs a
29/// [`Timeout`](crate::block::Timeout) vote in round `r` only if, at that moment:
30///
31/// 1. `r` equals its [`ChainManager::current_round`];
32/// 2. its [`round_timeout`] is `Some(t)` with `local_time ≥ t`;
33/// 3. it has not already signed a timeout vote in round `r`;
34///
35/// and the value it signs is `Timeout::new(chain_id, height, epoch)` for the chain's pending
36/// height and current epoch.
37///
38/// *Code correspondence.*
39///
40/// | | |
41/// |---|---|
42/// | transition | [`ChainManager::create_timeout_vote`] |
43/// | reads | [`current_round`], [`round_timeout`], [`timeout_vote`] |
44/// | writes | [`timeout_vote`] |
45/// | precondition | `height` equals `ChainTipState::next_block_height`, checked by the caller `ChainWorkerState::vote_for_leader_timeout` |
46///
47/// *Proof.* Direct reading of [`ChainManager::create_timeout_vote`]: it returns `Ok(false)`
48/// without a key pair, then `ensure!(round == self.current_round())`, then
49/// `let Some(round_timeout) = *self.round_timeout.get() else { return Err(RoundDoesNotTimeOut) }`,
50/// then `ensure!(local_time >= round_timeout)`, then returns `Ok(false)` if
51/// `timeout_vote.round == round`. Only after all four does it construct the vote. ∎
52///
53/// [`ChainManager::create_timeout_vote`]: crate::manager::ChainManager::create_timeout_vote
54/// [`ChainManager::current_round`]: method@crate::manager::ChainManager::current_round
55/// [`current_round`]: field@crate::manager::ChainManager::current_round
56/// [`round_timeout`]: crate::manager::ChainManager::round_timeout
57/// [`timeout_vote`]: crate::manager::ChainManager::timeout_vote
58pub trait TimeoutVoteConditions {}
59
60/// **Lemma (A timeout certificate proves a correct validator's round expired).** If a valid
61/// [`TimeoutCertificate`] is certified in round `r` for a chain and height, then some correct
62/// validator was in round `r` at that height, with a configured round timeout that had elapsed.
63///
64/// *Proof.* By [`CertificateCarriesCorrectVote`] a correct validator cast a timeout vote with
65/// the certificate's payload, whose round is `r`. By [`TimeoutVoteConditions`] its
66/// [`ChainManager::current_round`] was `r` and its [`round_timeout`] had elapsed. ∎
67///
68/// This is what makes a timeout certificate meaningful rather than merely well-formed: it cannot
69/// be manufactured by faulty validators ahead of time, because a quorum contains a correct
70/// validator whose own clock had to have passed the deadline.
71///
72/// [`TimeoutCertificate`]: crate::types::TimeoutCertificate
73/// [`ChainManager::current_round`]: method@crate::manager::ChainManager::current_round
74/// [`round_timeout`]: crate::manager::ChainManager::round_timeout
75pub trait TimeoutCertificateProvesRoundReached:
76 TimeoutVoteConditions + CertificateCarriesCorrectVote
77{
78}
79
80/// **Lemma (A timeout certificate advances the round).** After a correct validator processes a
81/// valid [`TimeoutCertificate`] for round `r` at its pending height, its
82/// [`ChainManager::current_round`] is at least `ChainOwnership::next_round(r)`, or
83/// `Round::Validator(u32::MAX)` if that is `None`.
84///
85/// *Code correspondence.*
86///
87/// | | |
88/// |---|---|
89/// | transition | [`ChainManager::handle_timeout_certificate`] |
90/// | reads | [`timeout`] |
91/// | writes | [`timeout`], and via `update_current_round`: [`current_round`], [`round_timeout`] |
92/// | precondition | `certificate.check(committee)`, epoch equality and pending-height equality, all checked by `ChainWorkerState::process_timeout` |
93///
94/// *Proof.* [`ChainManager::handle_timeout_certificate`] returns early when the stored
95/// [`timeout`] is already in a round `≥ r`; otherwise it stores the certificate and calls
96/// `update_current_round`. In the first case the stored certificate's round `r' ≥ r`, and a
97/// previous application of this lemma already raised the round to `next_round(r') ≥ next_round(r)`
98/// by monotonicity of [`ChainOwnership::next_round`] together with [`CurrentRoundMonotone`]. In
99/// the second, [`RoundFloor`] includes `next_round(timeout.round)` in the maximum. ∎
100///
101/// [`TimeoutCertificate`]: crate::types::TimeoutCertificate
102/// [`ChainManager::current_round`]: method@crate::manager::ChainManager::current_round
103/// [`ChainManager::handle_timeout_certificate`]: crate::manager::ChainManager::handle_timeout_certificate
104/// [`ChainOwnership::next_round`]: linera_base::ownership::ChainOwnership::next_round
105/// [`timeout`]: crate::manager::ChainManager::timeout
106/// [`current_round`]: field@crate::manager::ChainManager::current_round
107/// [`round_timeout`]: crate::manager::ChainManager::round_timeout
108pub trait TimeoutCertificateAdvancesRound: RoundFloor + CurrentRoundMonotone {}
109
110/// **Lemma (Which rounds can be skipped without a timeout).** For a correct validator:
111///
112/// * a [`SingleLeader`] round above `SingleLeader(0)`, and any [`Validator`] round, is left only
113/// via a [`TimeoutCertificate`] or a locking block in a higher round;
114/// * [`Round::Fast`] and [`MultiLeader`] rounds are additionally left by any authenticated
115/// proposal in a higher round.
116///
117/// *Proof.* By [`RoundFloor`] the round can rise only from a timeout certificate, the lock, or
118/// [`proposed`] / [`signed_proposal`]. For the proposal inputs:
119///
120/// * [`ChainManager::update_signed_proposal`] returns `false` immediately for
121/// `proposal.content.round > Round::SingleLeader(0)`, so [`signed_proposal`] never carries a
122/// higher round;
123/// * [`proposed`] is written only by the private `update_proposed`, called from
124/// [`ChainManager::create_vote`], which by [`ProposalGate`] runs only after
125/// [`ChainManager::check_proposed_block`] returned `Accept`; and that method requires
126/// `new_round == current_round` on the `Round::SingleLeader(_) | Round::Validator(_)` arm, so
127/// it cannot raise the round either. On the `MultiLeader(_) | SingleLeader(0)` arm it requires
128/// only `new_round >= current_round`, which can raise it.
129///
130/// The lock case is not an exception to the intent: a [`LockingBlock::Regular`] in round `r` is a
131/// [`ValidatedBlockCertificate`], hence by [`CertificateCarriesCorrectVote`] evidence that a
132/// quorum — including a correct validator — was already in round `r`. So the validator is
133/// following the round the chain has demonstrably reached, not being pushed past a leader's turn.
134/// ∎
135///
136/// [`SingleLeader`]: linera_base::data_types::Round::SingleLeader
137/// [`Validator`]: linera_base::data_types::Round::Validator
138/// [`MultiLeader`]: linera_base::data_types::Round::MultiLeader
139/// [`Round::Fast`]: linera_base::data_types::Round::Fast
140/// [`TimeoutCertificate`]: crate::types::TimeoutCertificate
141/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
142/// [`LockingBlock::Regular`]: crate::manager::LockingBlock::Regular
143/// [`ChainManager::update_signed_proposal`]: crate::manager::ChainManager::update_signed_proposal
144/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
145/// [`ChainManager::create_vote`]: crate::manager::ChainManager::create_vote
146/// [`ProposalGate`]: crate::manager::proof::voting::ProposalGate
147/// [`proposed`]: crate::manager::ChainManager::proposed
148/// [`signed_proposal`]: crate::manager::ChainManager::signed_proposal
149pub trait SingleLeaderRoundsNeedTimeout: RoundFloor + CertificateCarriesCorrectVote {}
150
151/// **Lemma (Leader eligibility).** In a [`SingleLeader`] or [`Validator`] round, exactly one
152/// owner may propose, namely `ChainManager::round_leader(round)`; in a [`MultiLeader`] round any
153/// chain owner may (or anyone, when
154/// [`open_multi_leader_rounds`](linera_base::ownership::ChainOwnership::open_multi_leader_rounds));
155/// in [`Round::Fast`] only a super owner may. A super owner may additionally propose in any
156/// non-[`Validator`] round.
157///
158/// The leader of round `n` is drawn by seeding a [`ChaCha8Rng`](rand_chacha::ChaCha8Rng) with
159/// `u64::from(n).rotate_left(32) + seed` and sampling the stake-weighted
160/// [`WeightedAliasIndex`](rand_distr::WeightedAliasIndex) built from the owners — or, for a
161/// [`Validator`] round, from [`fallback_owners`], which
162/// [`ChainManager::reset`] populates with the committee's account keys and weights. The seed is
163/// the block height, so the leader schedule is fixed per instance and identical at every correct
164/// validator.
165///
166/// *Proof.* [`ChainManager::can_propose`] returns `!round.is_validator()` for a super owner, and
167/// otherwise dispatches: `false` for [`Round::Fast`],
168/// `ownership.can_propose_in_multi_leader_round(owner)` for [`MultiLeader`], and
169/// `self.round_leader(round) == Some(owner)` for the other two. It is enforced at the entry
170/// point: `ChainWorkerState::try_handle_block_proposal` rejects a proposal with
171/// `WorkerError::InvalidOwner` unless `chain.manager.can_propose(&owner, proposal.content.round)`,
172/// where `owner` is recovered from the proposal's signature. The leader computation is the
173/// private `compute_round_leader` / `round_leader_index` pair. ∎
174///
175/// [`SingleLeader`]: linera_base::data_types::Round::SingleLeader
176/// [`Validator`]: linera_base::data_types::Round::Validator
177/// [`MultiLeader`]: linera_base::data_types::Round::MultiLeader
178/// [`Round::Fast`]: linera_base::data_types::Round::Fast
179/// [`fallback_owners`]: crate::manager::ChainManager::fallback_owners
180/// [`ChainManager::reset`]: crate::manager::ChainManager::reset
181/// [`ChainManager::can_propose`]: crate::manager::ChainManager::can_propose
182pub trait LeaderEligibility {}
183
184/// **Lemma (Fallback).** [`ChainManager::vote_fallback`] signs a timeout vote in the fixed round
185/// `Round::SingleLeader(u32::MAX)`, at most once per instance, and only while
186/// [`ChainManager::current_round`] is below `Round::Validator(0)`. A quorum of such votes forms a
187/// [`TimeoutCertificate`] whose `next_round` is `Round::Validator(0)`, moving the height into
188/// validator-led rounds.
189///
190/// *Proof.* The method returns `false` if [`fallback_vote`] is already set or
191/// `current_round >= Round::Validator(0)`, and otherwise signs `Timeout::new(chain_id, height,
192/// epoch)` at `Round::SingleLeader(u32::MAX)`. By [`ChainOwnership::next_round`], the successor
193/// of `SingleLeader(r)` is `SingleLeader(r + 1)` unless that overflows, and it does at
194/// `u32::MAX`, so the successor is `Round::Validator(0)`; combine with
195/// [`TimeoutCertificateAdvancesRound`]. ∎
196///
197/// Note what is *not* checked here, unlike [`TimeoutVoteConditions`]: there is no
198/// [`round_timeout`] comparison. The precondition is external — `ChainWorkerState::
199/// vote_for_fallback` only calls it after reading the admin chain's epoch event and confirming
200/// that `fallback_duration` has elapsed since the next epoch was created. The method's own
201/// documentation states this obligation.
202///
203/// [`ChainManager::vote_fallback`]: crate::manager::ChainManager::vote_fallback
204/// [`ChainManager::current_round`]: method@crate::manager::ChainManager::current_round
205/// [`TimeoutCertificate`]: crate::types::TimeoutCertificate
206/// [`fallback_vote`]: crate::manager::ChainManager::fallback_vote
207/// [`round_timeout`]: crate::manager::ChainManager::round_timeout
208/// [`ChainOwnership::next_round`]: linera_base::ownership::ChainOwnership::next_round
209pub trait FallbackVote: TimeoutCertificateAdvancesRound + ConsensusInstance {}
210
211/// **Caveat (Rounds that never time out).** [`ChainOwnership::round_timeout`] returns `None` —
212/// so that [`ChainManager::create_timeout_vote`] fails with
213/// [`ChainError::RoundDoesNotTimeOut`] and no timeout certificate can ever form — in three cases:
214///
215/// | round | `round_timeout` is `None` when |
216/// |---|---|
217/// | [`Round::Fast`] | `timeout_config.fast_round_duration` is `None`, **which is the default**, or `owners` is empty |
218/// | [`MultiLeader(r)`] | `r + 1 != multi_leader_rounds`, i.e. every multi-leader round but the last |
219/// | [`SingleLeader`], [`Validator`] | never |
220///
221/// Consequences, which the liveness argument must and does respect:
222///
223/// * A non-final multi-leader round is left only by a proposal in a higher round
224/// ([`SingleLeaderRoundsNeedTimeout`]). This is by design — multi-leader rounds are skippable —
225/// but it means "wait for the timeout" is not a strategy there.
226/// * With a super owner and the default [`TimeoutConfig`], the fast round has **no timeout at
227/// all**. If the super owner issues two conflicting fast proposals, correct validators split
228/// between them ([`FastConfirmationNeedsEmptyLock`] pins each to the first it sees), neither
229/// reaches a quorum, and — since only a super owner may open a later round while the current
230/// round is fast, by the `is_super(&proposal.owner()) || !current_round.is_fast()` guard in
231/// [`ChainManager::check_proposed_block`] — the height cannot progress until that same super
232/// owner proposes again in a higher round. A super owner that stops there wedges the chain
233/// permanently. This is the caveat the module documentation of [`crate::manager`] states as
234/// "super owners must be careful to make only one block proposal", stated precisely.
235///
236/// Both are liveness properties; [`CommitAgreement`](crate::manager::proof::safety::CommitAgreement)
237/// is unaffected.
238///
239/// [`ChainOwnership::round_timeout`]: linera_base::ownership::ChainOwnership::round_timeout
240/// [`ChainManager::create_timeout_vote`]: crate::manager::ChainManager::create_timeout_vote
241/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
242/// [`ChainError::RoundDoesNotTimeOut`]: crate::ChainError::RoundDoesNotTimeOut
243/// [`Round::Fast`]: linera_base::data_types::Round::Fast
244/// [`MultiLeader(r)`]: linera_base::data_types::Round::MultiLeader
245/// [`SingleLeader`]: linera_base::data_types::Round::SingleLeader
246/// [`Validator`]: linera_base::data_types::Round::Validator
247/// [`TimeoutConfig`]: linera_base::ownership::TimeoutConfig
248/// [`FastConfirmationNeedsEmptyLock`]: crate::manager::proof::voting::FastConfirmationNeedsEmptyLock
249pub trait RoundsWithoutTimeout: TimeoutVoteConditions + SingleLeaderRoundsNeedTimeout {}
250
251/// **Remark (In a multi-leader round, the round a validator is in is its own).** Above the fast
252/// round, the protocol has two ways to leave a round, and they differ in kind rather than only in
253/// trigger. A [`TimeoutCertificate`] is quorum-signed, compact, and convinces anyone who receives
254/// it. A skipped multi-leader round leaves no artifact at all: by
255/// [`SingleLeaderRoundsNeedTimeout`] it ends when *some owner* proposes higher, which is a
256/// unilateral act, and by [`RoundsWithoutTimeout`] no timeout certificate for it can ever exist.
257///
258/// Three consequences run through the rest of the specification.
259///
260/// *Correct validators legitimately disagree about the round.* A validator's
261/// [`current_round`](crate::manager::ChainManager::current_round) is a maximum over what it has
262/// happened to receive, so in the multi-leader regime two correct validators can sit in different
263/// rounds with neither being behind in any blameable sense. This is why
264/// [`ChainManager::check_proposed_block`] accepts `new_round >= current_round` there while
265/// demanding equality in single-leader and validator rounds — the weaker test is what makes
266/// disagreement survivable. It is also why the progress lemmas open by *assuming* every correct
267/// validator is in the same round: in this regime that is a real hypothesis, not a state the
268/// protocol reaches by itself.
269///
270/// *Catching a validator up costs more than a certificate.* A validator that missed the proposal
271/// which ended a round cannot be handed a proof of that fact, because none exists. It has to be
272/// sent the chain information itself — which is exactly why `WrongRound` appears as its own class,
273/// with its own push route, in `linera_core::proof::availability::MissingDependenciesAreRecoverable`,
274/// and why that class is the one where the *requester* may turn out to be the party that is
275/// behind. Were every round to end in a timeout certificate, that class would collapse to
276/// forwarding one certificate.
277///
278/// *Advancing the round and unlocking must key on different evidence.* Since one owner can raise
279/// the round, the round must never by itself license abandoning a lock — otherwise an owner could
280/// discard a locked block by proposing higher. So [`UnlockingRequiresHigherCertificate`] keys
281/// unlocking on a [`ValidatedBlockCertificate`] from a higher round, evidence that a quorum moved,
282/// and never on `current_round`. The two questions "which round am I in" and "what may I abandon"
283/// are deliberately kept apart, and the safety argument depends on their separation:
284/// [`SingleLeaderRoundsNeedTimeout`] makes the same point for the lock as a round *input*, where a
285/// [`LockingBlock::Regular`] raises the round only because it is itself quorum evidence.
286///
287/// [`TimeoutCertificate`]: crate::types::TimeoutCertificate
288/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
289/// [`UnlockingRequiresHigherCertificate`]: crate::manager::proof::voting::UnlockingRequiresHigherCertificate
290/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
291/// [`LockingBlock::Regular`]: crate::manager::LockingBlock::Regular
292pub trait MultiLeaderRoundsAreLocal: SingleLeaderRoundsNeedTimeout + RoundsWithoutTimeout {}