Skip to main content

linera_core/proof/
progress.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Progress lemmas: the individual steps a correct driver can force after GST.
5//!
6//! Each result here says that one step of the protocol *completes*, given that the previous ones
7//! did. They are assembled into the liveness theorems in [`super::liveness`].
8//!
9//! The driver is [`ChainClient::process_pending_block`], whose body
10//! (`process_pending_block_inner`) performs, in order: request a timeout if the round has expired
11//! ([`TimeoutCertificateForms`], [`RoundAdvancement`]); finalize a locking block already in the
12//! current round; otherwise choose a block — the locking block if there is one
13//! ([`LockRecovery`]) — and a round ([`EventuallyCorrectLeader`]); submit the proposal
14//! ([`ProposalAccepted`], [`ValidationQuorumForms`]); and finalize it
15//! ([`FinalizationQuorumForms`]).
16//!
17//! [`ChainClient::process_pending_block`]: crate::client::ChainClient::process_pending_block
18
19use linera_chain::{
20    data_types::proof::quorum::CorrectValidatorsFormQuorum,
21    manager::proof::{
22        commit::CommittedBlock,
23        locking::{LockRoundMonotone, UniqueValidatedBlockPerRound},
24        rounds::CurrentRoundMonotone,
25        timeouts::{
26            LeaderEligibility, RoundsWithoutTimeout, SingleLeaderRoundsNeedTimeout,
27            TimeoutCertificateAdvancesRound, TimeoutVoteConditions,
28        },
29        voting::{
30            ConfirmationNeedsValidatedCertificate, ConfirmationOnlyInCurrentRound, ProposalGate,
31            UnlockingRequiresHigherCertificate,
32        },
33    },
34};
35
36use super::assumptions::{
37    ActiveCorrectDriver, ClockAccuracy, CorrectValidatorAvailability, EventualSynchrony,
38    FullReachability, LeaderFairness, RoundTimeoutGrowth,
39};
40
41/// **Lemma (A timeout certificate forms).** Suppose that after GST every correct validator is in
42/// the same round `r` at the chain's pending height, that `r` has a configured timeout
43/// ([`RoundsWithoutTimeout`]), and that the timeout has elapsed on every correct validator's
44/// clock. Then a correct driver's [`ChainClient::request_leader_timeout`] returns a valid
45/// [`TimeoutCertificate`] for round `r` within `2Δ` plus local processing.
46///
47/// *Proof.* [`ChainClient::request_leader_timeout`] issues
48/// `CommunicateAction::RequestTimeout { chain_id, height, round }` with `round` read from its
49/// local [`ChainManagerInfo::current_round`] and `height` from `ChainInfo::next_block_height`,
50/// through `Client::communicate_chain_action`. Each recipient runs
51/// `ChainWorkerState::vote_for_leader_timeout`, which checks the height against
52/// [`ChainTipState::next_block_height`] and calls
53/// [`ChainManager::create_timeout_vote`]. By [`TimeoutVoteConditions`] its four conditions hold
54/// under the hypotheses, so every correct validator signs; by
55/// [`CorrectValidatorAvailability`] and [`EventualSynchrony`] every such vote arrives within Δ
56/// of the request.
57///
58/// The votes aggregate: `communicate_with_quorum` groups by the full signed payload
59/// `(value_hash, round, unlocking_round, first_round, justification_commitment)`, and every
60/// timeout vote for this height carries the same `Timeout::new(chain_id, height, epoch)` value
61/// — identical by [`ClockAccuracy`]-independent construction, since the epoch is the chain's —
62/// with `unlocking_round: None`, `first_round: false` and no justification commitment. So all
63/// correct votes land in one group, which by [`CorrectValidatorsFormQuorum`] reaches
64/// [`quorum_threshold`], and `communicate_with_quorum` returns it. ∎
65///
66/// The hypothesis "every correct validator is in round `r`" is not free — see
67/// [`RoundAdvancement`], which is what establishes it for the next round.
68///
69/// [`ChainClient::request_leader_timeout`]: crate::client::ChainClient::request_leader_timeout
70/// [`TimeoutCertificate`]: linera_chain::types::TimeoutCertificate
71/// [`ChainManagerInfo::current_round`]: linera_chain::manager::ChainManagerInfo::current_round
72/// [`ChainTipState::next_block_height`]: linera_chain::ChainTipState::next_block_height
73/// [`ChainManager::create_timeout_vote`]: linera_chain::manager::ChainManager::create_timeout_vote
74/// [`quorum_threshold`]: linera_execution::committee::Committee::quorum_threshold
75pub trait TimeoutCertificateForms:
76    TimeoutVoteConditions
77    + RoundsWithoutTimeout
78    + CorrectValidatorsFormQuorum
79    + CorrectValidatorAvailability
80    + EventualSynchrony
81    + ClockAccuracy
82{
83}
84
85/// **Lemma (Round advancement).** After GST, a correct driver can bring every correct validator
86/// into a common round strictly above `r`, within `O(Δ)`, provided `r` has a configured timeout.
87/// Consequently the common round grows without bound as long as the driver keeps trying.
88///
89/// *Proof.* By [`TimeoutCertificateForms`] the driver obtains a [`TimeoutCertificate`] for `r`.
90/// [`ChainClient::request_leader_timeout`] then feeds it to its own node and calls
91/// `Client::communicate_chain_updates`, which delivers it to the validators; each correct
92/// recipient runs `ChainWorkerState::process_timeout`, which verifies it against the committee
93/// and calls [`ChainManager::handle_timeout_certificate`]. By
94/// [`TimeoutCertificateAdvancesRound`] each then has a current round of at least
95/// `ChainOwnership::next_round(r) > r`, and by [`CurrentRoundMonotone`] it stays there.
96///
97/// They are in a *common* round because [`RoundFloor`] makes the round a deterministic function
98/// of the evidence held, and after this step every correct validator holds the same highest
99/// timeout certificate — unless some hold additional evidence (a higher lock or proposal), which
100/// only moves them higher, and which the driver's own synchronization
101/// ([`FullReachability`]) then propagates. Unboundedness follows by induction, using
102/// [`RoundTimeoutGrowth`] to know that each successive round again has a finite timeout. ∎
103///
104/// **This is strictly weaker than liveness.** It says rounds advance, not that a block is
105/// committed; an execution in which the driver forever advances rounds without ever committing
106/// satisfies this lemma. Turning it into progress is what [`RoundProgress`] does, and it needs
107/// [`EventuallyCorrectLeader`] and [`LockRecovery`] besides.
108///
109/// [`TimeoutCertificate`]: linera_chain::types::TimeoutCertificate
110/// [`ChainClient::request_leader_timeout`]: crate::client::ChainClient::request_leader_timeout
111/// [`ChainManager::handle_timeout_certificate`]: linera_chain::manager::ChainManager::handle_timeout_certificate
112/// [`RoundFloor`]: linera_chain::manager::proof::rounds::RoundFloor
113/// [`RoundProgress`]: super::liveness::RoundProgress
114pub trait RoundAdvancement:
115    TimeoutCertificateForms
116    + TimeoutCertificateAdvancesRound
117    + CurrentRoundMonotone
118    + RoundTimeoutGrowth
119    + SingleLeaderRoundsNeedTimeout
120{
121}
122
123/// **Lemma (Eventually a correct owner leads a round that starts after GST).** Under
124/// [`ActiveCorrectDriver`], [`LeaderFairness`] and [`RoundAdvancement`], there are infinitely
125/// many [`SingleLeader`] rounds after GST whose leader is the correct driver's owner.
126///
127/// *Proof.* By [`RoundAdvancement`] the common round grows without bound after GST, so infinitely
128/// many single-leader rounds begin after GST — the round sequence passes through
129/// `SingleLeader(0), SingleLeader(1), …` by [`ChainOwnership::next_round`], and only leaves them
130/// for [`Validator`] rounds on `u32` overflow or via fallback. By [`LeaderFairness`] the driver's
131/// owner is the leader of infinitely many of them, and by [`LeaderEligibility`] being the leader
132/// is exactly what `ChainManager::can_propose` requires. ∎
133///
134/// In [`Validator`] rounds the same argument applies with the committee's account keys as the
135/// owner set; the correct driver is then a validator operator's client.
136///
137/// [`SingleLeader`]: linera_base::data_types::Round::SingleLeader
138/// [`Validator`]: linera_base::data_types::Round::Validator
139/// [`ChainOwnership::next_round`]: linera_base::ownership::ChainOwnership::next_round
140pub trait EventuallyCorrectLeader:
141    RoundAdvancement + LeaderFairness + ActiveCorrectDriver + LeaderEligibility
142{
143}
144
145/// **Lemma (Lock recovery).** Under [`FullReachability`], after a correct driver completes
146/// [`ChainClient::synchronize_chain_state`] its local
147/// [`ChainManagerInfo::requested_locking`] has a round at least as high as the
148/// [`confirmed_vote`] round of every correct validator — and, if any correct validator holds a
149/// lock at all, is a [`ValidatedBlockCertificate`] for the same block that the highest such
150/// validator locked.
151///
152/// *Proof.* Two steps.
153///
154/// *Every correct validator's lock dominates its own confirmation.* If a correct validator's
155/// [`confirmed_vote`] is in round `p`, then either `p` is [`Round::Fast`] — and by
156/// [`FastConfirmationNeedsEmptyLock`] it then holds a `LockingBlock::Fast` at that same round —
157/// or by [`ConfirmationNeedsValidatedCertificate`] it confirmed via
158/// [`ChainManager::create_final_vote`], which installs the certificate as the lock *before*
159/// signing ([`ConfirmationOnlyInCurrentRound`]). Either way its lock round is `≥ p`, and stays so
160/// by [`LockRoundMonotone`].
161///
162/// *The driver collects the maximum.* `Client::synchronize_chain_state_from` reads each
163/// validator's [`ChainManagerInfo`] with manager values, and for a
164/// `LockingBlock::Regular(cert)` calls `try_process_locking_block_from`, which feeds the
165/// certificate to the local node's `process_validated_block`; that calls
166/// [`ChainManager::create_final_vote`], whose `update_locking` keeps the higher of the two by
167/// [`LockRoundMonotone`]. A `LockingBlock::Fast` is instead replayed as a proposal. Iterating
168/// over the validators reached — all of the correct ones, by [`FullReachability`] — leaves the
169/// local lock at the maximum. That it is a certificate *for the locked block* is immediate,
170/// since a lock *is* the certificate; and by [`UniqueValidatedBlockPerRound`] two correct
171/// validators locked at the same round hold certificates for the same block. ∎
172///
173/// This is what makes [`ProposalAccepted`] possible: the driver re-proposes the block it just
174/// recovered, so no correct validator's [`UnlockingRequiresHigherCertificate`] guard can reject
175/// it.
176///
177/// [`ChainClient::synchronize_chain_state`]: crate::client::ChainClient::synchronize_chain_state
178/// [`ChainManagerInfo`]: linera_chain::manager::ChainManagerInfo
179/// [`ChainManagerInfo::requested_locking`]: linera_chain::manager::ChainManagerInfo::requested_locking
180/// [`confirmed_vote`]: field@linera_chain::manager::ChainManager::confirmed_vote
181/// [`ValidatedBlockCertificate`]: linera_chain::types::ValidatedBlockCertificate
182/// [`Round::Fast`]: linera_base::data_types::Round::Fast
183/// [`ChainManager::create_final_vote`]: linera_chain::manager::ChainManager::create_final_vote
184/// [`FastConfirmationNeedsEmptyLock`]: linera_chain::manager::proof::voting::FastConfirmationNeedsEmptyLock
185pub trait LockRecovery:
186    FullReachability
187    + ConfirmationNeedsValidatedCertificate
188    + ConfirmationOnlyInCurrentRound
189    + LockRoundMonotone
190    + UniqueValidatedBlockPerRound
191{
192}
193
194/// **Lemma (A recovered proposal is accepted).** Let `r` be a [`SingleLeader`] or [`Validator`]
195/// round beginning after GST whose leader is the correct driver's owner
196/// ([`EventuallyCorrectLeader`]), let every correct validator be in round `r`
197/// ([`RoundAdvancement`]), and let the driver have completed lock recovery
198/// ([`LockRecovery`]). Then the proposal the driver submits in round `r` passes
199/// [`ChainManager::check_proposed_block`] at every correct validator.
200///
201/// *Proof.* By [`ProposalGate`] acceptance is exactly `check_proposed_block` returning
202/// [`Accept`], so we take its guards in order, for a correct validator `v` in round `r`.
203///
204/// * *Proposer eligibility.* `try_handle_block_proposal` requires
205///   `chain.manager.can_propose(&owner, r)`, which holds by [`LeaderEligibility`] since the
206///   driver is `r`'s leader; the driver selects `r` through `ChainClient::round_for_new_proposal`,
207///   which consults the same `ChainManagerInfo::should_propose`.
208/// * *Round.* The `SingleLeader(_) | Validator(_)` arm requires `r == v.current_round()`, which
209///   is the hypothesis.
210/// * *Validation vote.* Requires `r > v.validated_vote.round`. By [`VoteRoundBelowCurrentRound`]
211///   any earlier validation vote of `v` is in a round `≤ v.current_round() = r`, and `= r` is
212///   excluded: a vote in round `r` needs a proposal in round `r` accepted by `v`, and by
213///   [`LeaderEligibility`] the only proposer `v` accepts in `r` is the driver, which has made no
214///   other proposal in `r`.
215/// * *Lock.* Requires `r > v.locking_block.round()`. A lock round above `r` would by
216///   [`RoundFloor`] put `v.current_round()` above `r`, contradicting the hypothesis; and a lock
217///   round *equal* to `r` would require a [`ValidatedBlockCertificate`] in round `r`, which by
218///   [`CertificateCarriesCorrectVote`] would require a correct validator's validation vote in
219///   round `r` — excluded by the previous point.
220/// * *Confirmed vote.* This is the one that needs [`LockRecovery`]. If `v` has a confirmed vote
221///   in round `p`, then by [`LockRecovery`] the driver's recovered lock has round `t ≥ p` and
222///   certifies the same block `B` that `v` confirmed if `t = p`. The driver proposes `B` as a
223///   [`Regular`] retry carrying that certificate (`process_pending_block_inner` takes the
224///   `if let Some(locking) = info.manager.requested_locking` branch and builds
225///   `BlockProposal::new_retry_regular`). By [`UnlockingRequiresHigherCertificate`] the guard
226///   then requires `p ≤ t` when the blocks match, which holds. (When `t > p` and the blocks
227///   differ, the guard requires `p < t`, which also holds.)
228///
229/// A `LockingBlock::Fast` lock is retried as `BlockProposal::new_retry_fast`, and the same guard
230/// requires `v.confirmed_vote.round.is_fast()` and a matching block, which holds because a fast
231/// confirmation is the only confirmation possible below the fast round's successor. ∎
232///
233/// [`SingleLeader`]: linera_base::data_types::Round::SingleLeader
234/// [`Validator`]: linera_base::data_types::Round::Validator
235/// [`ChainManager::check_proposed_block`]: linera_chain::manager::ChainManager::check_proposed_block
236/// [`Accept`]: linera_chain::manager::Outcome::Accept
237/// [`ValidatedBlockCertificate`]: linera_chain::types::ValidatedBlockCertificate
238/// [`Regular`]: linera_chain::data_types::OriginalProposal::Regular
239/// [`VoteRoundBelowCurrentRound`]: linera_chain::manager::proof::rounds::VoteRoundBelowCurrentRound
240/// [`CertificateCarriesCorrectVote`]: linera_chain::data_types::proof::quorum::CertificateCarriesCorrectVote
241/// [`RoundFloor`]: linera_chain::manager::proof::rounds::RoundFloor
242pub trait ProposalAccepted:
243    EventuallyCorrectLeader
244    + RoundAdvancement
245    + LockRecovery
246    + ProposalGate
247    + UnlockingRequiresHigherCertificate
248    + LeaderEligibility
249{
250}
251
252/// **Lemma (The validation quorum forms).** Under the hypotheses of [`ProposalAccepted`], the
253/// driver obtains a valid [`ValidatedBlockCertificate`] for its block in round `r` within `2Δ`
254/// plus local processing.
255///
256/// *Proof.* By [`ProposalAccepted`] every correct validator accepts the proposal, so
257/// `ChainWorkerState::try_handle_block_proposal` reaches
258/// [`ChainManager::create_vote`], which — `r` not being the fast round — signs a validation vote
259/// for the block in round `r`. Every such vote carries the same signed payload: same block hash,
260/// same round, and the same `unlocking_round`/`justification_commitment` pair, which
261/// [`ChainManager::create_vote`] derives from the proposal's own
262/// [`Regular`](linera_chain::data_types::OriginalProposal::Regular) certificate — identical
263/// across validators because the proposal is. So all correct votes fall into one group of
264/// `communicate_with_quorum`, which by [`CorrectValidatorsFormQuorum`] reaches the quorum
265/// threshold; by [`CorrectValidatorAvailability`] and [`EventualSynchrony`] they arrive within Δ.
266/// `Client::submit_block_proposal` assembles them into a certificate, whose justification chain
267/// is the retried certificate's `full_justification`. ∎
268///
269/// Note the blob preconditions: a validator missing a blob the proposal requires answers
270/// `WorkerError::BlobsNotFound` instead of voting. That does not cost a round. The retry is
271/// *per-validator*, inside `RemoteNodeUpdater::send_block_proposal`'s loop: the arm matching
272/// `BlobsNotFound | InactiveChain` sends the proposal's published blobs with `send_pending_blobs`
273/// and re-submits to that validator alone, so the other validators' votes are unaffected and the
274/// quorum round is not restarted. It terminates because the loop drains its `blob_ids` — the set
275/// is fixed by the proposal and each pass takes it with `mem::take`. So the `2Δ` bound above
276/// absorbs it as a constant factor rather than an extra round of the protocol.
277///
278/// [`ValidatedBlockCertificate`]: linera_chain::types::ValidatedBlockCertificate
279/// [`ChainManager::create_vote`]: linera_chain::manager::ChainManager::create_vote
280pub trait ValidationQuorumForms:
281    ProposalAccepted + CorrectValidatorsFormQuorum + CorrectValidatorAvailability + EventualSynchrony
282{
283}
284
285/// **Lemma (The finalization quorum forms).** Given a valid [`ValidatedBlockCertificate`] for
286/// block `B` in round `r`, and every correct validator in round `r` after GST, the driver obtains
287/// a valid [`ConfirmedBlockCertificate`] for `B` within `2Δ` plus local processing — so `B`
288/// becomes a [`CommittedBlock`].
289///
290/// *Proof.* `Client::finalize_block` sends `CommunicateAction::FinalizeBlock` to every validator.
291/// A correct recipient runs `ChainWorkerState::process_validated_block`, which verifies the
292/// certificate, checks [`ChainManager::check_validated_block`] — whose guards are
293/// `new_round ≥ validated_vote.round`, satisfied since no correct validator voted above `r`, and
294/// `new_round > locking_block.round()`, satisfied since no lock in round `r` existed before this
295/// certificate — and calls [`ChainManager::create_final_vote`]. That signs, because by
296/// [`ConfirmationOnlyInCurrentRound`] it requires the current round to equal `r`, which holds by
297/// hypothesis and is re-established by its own `update_locking`/`update_current_round` prelude.
298///
299/// All confirmation votes again share one payload: the value is `ConfirmedBlock::new(B)`, the
300/// round is `r`, and the `first_round` flag and justification commitment are derived from `r` and
301/// the certificate, identically at every validator. So they aggregate;
302/// [`CorrectValidatorsFormQuorum`] gives the threshold. `Client::finalize_block` then attaches
303/// the justification chain the quorum committed to and returns the certificate, which is a
304/// [`CommittedBlock`] by definition. ∎
305///
306/// [`ValidatedBlockCertificate`]: linera_chain::types::ValidatedBlockCertificate
307/// [`ConfirmedBlockCertificate`]: linera_chain::types::ConfirmedBlockCertificate
308/// [`ChainManager::check_validated_block`]: linera_chain::manager::ChainManager::check_validated_block
309/// [`ChainManager::create_final_vote`]: linera_chain::manager::ChainManager::create_final_vote
310pub trait FinalizationQuorumForms:
311    ValidationQuorumForms
312    + ConfirmationOnlyInCurrentRound
313    + ConfirmationNeedsValidatedCertificate
314    + CommittedBlock
315    + CorrectValidatorsFormQuorum
316{
317}