Skip to main content

linera_core/proof/
assumptions.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The assumptions that progress needs and safety does not.
5//!
6//! Safety rests only on the assumptions in [`linera_chain::manager::proof::model`], all of which
7//! remain in force here. This module adds the ones that mention time, responsiveness, or the
8//! existence of somebody willing to drive the protocol. Every one of them can fail without
9//! endangering [`CommitAgreement`]; each failure costs progress only.
10//!
11//! [`CommitAgreement`]: linera_chain::manager::proof::safety::CommitAgreement
12
13/// **Assumption (Eventual synchrony).** There is a time GST and a bound Δ, both unknown to the
14/// protocol, such that every message sent between correct participants after GST is delivered
15/// within Δ, and every message sent before GST is delivered by GST + Δ.
16///
17/// "Participants" here includes clients: a Linera consensus round is driven by a client
18/// ([`ActiveCorrectDriver`]), so the relevant round trips are client-to-validator, not
19/// validator-to-validator.
20///
21/// Before GST nothing is claimed. In particular a network that reorders, delays or drops
22/// messages arbitrarily can keep [`ChainTipState::next_block_height`] fixed forever without
23/// violating any result in this specification.
24///
25/// [`ChainTipState::next_block_height`]: linera_chain::ChainTipState::next_block_height
26pub trait EventualSynchrony {}
27
28/// **Assumption (Correct validator availability).** After GST, every correct validator accepts
29/// requests and answers them within Δ, and its `linera_core::worker::WorkerState` completes each
30/// request in bounded local time.
31///
32/// This is stronger than [`CorrectValidator`], which permits a correct validator to be
33/// permanently crashed. It is needed because [`CorrectValidatorsFormQuorum`] only says the
34/// correct validators *hold* enough weight; progress needs them to answer.
35///
36/// The per-chain serialization of [`SequentialChainState`] means "bounded local time" also
37/// requires that no single chain's queue grows without bound — a chain saturated by requests can
38/// starve its own consensus without any validator being faulty.
39///
40/// [`CorrectValidator`]: linera_chain::manager::proof::model::CorrectValidator
41/// [`SequentialChainState`]: linera_chain::manager::proof::model::SequentialChainState
42/// [`CorrectValidatorsFormQuorum`]: linera_chain::data_types::proof::quorum::CorrectValidatorsFormQuorum
43pub trait CorrectValidatorAvailability {}
44
45/// **Assumption (Blob retention).** A correct validator that has processed a certified block keeps
46/// the blobs that block requires, for as long as any node may still need to execute it.
47///
48/// Content addressing makes a blob impossible to *forge* but does nothing to make it *exist*:
49/// [`CertifiedBlockIsAvailable`] needs someone to still be holding it. A validator writes them
50/// when it accepts the block — `write_blobs_and_certificate` — and records
51/// `BlobState { origin, last_used_by, epoch }` alongside.
52///
53/// **Currently discharged by omission.** Nothing deletes blobs: there is no collection pass in
54/// `linera-storage` or `linera-views`, so retention is unbounded and the assumption holds
55/// trivially. That is not a policy, and the shape of `BlobState` — recording which certificate
56/// last used a blob and in which epoch — suggests one was anticipated. Introducing collection
57/// would put this assumption at risk, and the rule would have to be keyed on those fields rather
58/// than on age.
59///
60/// Its counterpart is [`BlobAdmissionIsBounded`]: retention is only affordable because blobs that
61/// no certified block references cannot accumulate.
62///
63/// [`CertifiedBlockIsAvailable`]: super::availability::CertifiedBlockIsAvailable
64/// [`BlobAdmissionIsBounded`]: super::availability::BlobAdmissionIsBounded
65pub trait BlobRetention {}
66
67/// **Assumption (Bounded recovery).** After GST, a correct validator that crashes restarts and is
68/// again answering requests within Δ.
69///
70/// [`CorrectValidator`] admits a crash at any moment, discarding whatever was not persisted.
71/// Before GST that is unconstrained: crashes may be arbitrarily frequent and restarts arbitrarily
72/// slow, which is one of the ways the network is allowed to misbehave. After GST it must stop,
73/// or [`CorrectValidatorAvailability`] is unattainable — a validator that restarts more slowly
74/// than Δ is, from the protocol's point of view, permanently unavailable.
75///
76/// What the implementation must do to earn this is bounded work on restart: reload the chain
77/// views from storage rather than replay history. That holds for a validator that was only
78/// briefly down, since its saved state is close to the tip. It does **not** hold for one that has
79/// fallen far behind, where catching up is linear in the distance to the chain's latest
80/// checkpoint — see [`BoundedCatchUp`], which is where this assumption's real cost sits.
81///
82/// [`CorrectValidator`]: linera_chain::manager::proof::model::CorrectValidator
83/// [`BoundedCatchUp`]: super::availability::BoundedCatchUp
84pub trait BoundedRecovery {}
85
86/// **Assumption (An active correct driver).** Some correct owner of the chain runs a
87/// [`ChainClient`] that, from some point on, repeatedly and without giving up calls
88/// [`ChainClient::process_pending_block`] (or an operation that does, such as
89/// [`ChainClient::execute_operations`]) with a block to propose, and holds the signing key for
90/// the owner it proposes as.
91///
92/// **This assumption has no counterpart in most BFT protocols and is the single most important
93/// thing to understand about Linera's liveness.** A validator here never proposes a block and
94/// never advances a round on its own: it signs a timeout vote only when asked, through
95/// `ChainInfoQuery::request_leader_timeout` (see [`TimeoutVoteConditions`]), and there is no code
96/// path in `linera_core::worker` that constructs a [`BlockProposal`]. A microchain with no
97/// running client is not a chain that is slow; it is a chain that is stopped, by design.
98///
99/// In [`Round::Validator`] rounds the leaders are drawn from
100/// [`ChainManager::fallback_owners`], which [`ChainManager::reset`] populates with the
101/// committee's account keys — so the driver of a fallback round is a client run by a validator
102/// *operator*, still a client, and still assumed to exist.
103///
104/// [`ChainClient`]: crate::client::ChainClient
105/// [`ChainClient::process_pending_block`]: crate::client::ChainClient::process_pending_block
106/// [`ChainClient::execute_operations`]: crate::client::ChainClient::execute_operations
107/// [`BlockProposal`]: linera_chain::data_types::BlockProposal
108/// [`Round::Validator`]: linera_base::data_types::Round::Validator
109/// [`ChainManager::fallback_owners`]: linera_chain::manager::ChainManager::fallback_owners
110/// [`ChainManager::reset`]: linera_chain::manager::ChainManager::reset
111/// [`TimeoutVoteConditions`]: linera_chain::manager::proof::timeouts::TimeoutVoteConditions
112pub trait ActiveCorrectDriver {}
113
114/// **Assumption (Leader fairness).** The leader schedule selects the correct owner of
115/// [`ActiveCorrectDriver`] in infinitely many [`SingleLeader`] rounds.
116///
117/// The schedule is deterministic and identical at every validator: by [`LeaderEligibility`], the
118/// leader of `SingleLeader(n)` is drawn by seeding a `ChaCha8Rng` with
119/// `u64::from(n).rotate_left(32) + seed`, where `seed` is the block height, and sampling the
120/// stake-weighted `WeightedAliasIndex` over [`ChainOwnership::owners`]. The assumption is therefore about the generator, not about the
121/// protocol: it holds if ChaCha8, over the round-indexed seed sequence, selects each positive
122/// weight infinitely often. An owner of weight `0` is never selected, and
123/// `ChainOwnership::first_leader`, if set, deterministically owns `SingleLeader(0)`.
124///
125/// [`SingleLeader`]: linera_base::data_types::Round::SingleLeader
126/// [`ChainOwnership::owners`]: linera_base::ownership::ChainOwnership::owners
127/// [`LeaderEligibility`]: linera_chain::manager::proof::timeouts::LeaderEligibility
128pub trait LeaderFairness {}
129
130/// **Assumption (Round timeouts eventually exceed the round trip).** The round timeout grows
131/// without bound over successive rounds, so that eventually a round lasts longer than the time a
132/// correct leader needs to complete it after GST.
133///
134/// This is what [`ChainOwnership::round_timeout`] implements for the rounds that matter: for
135/// `SingleLeader(r)` and `Validator(r)` it returns
136/// `base_timeout + timeout_increment · r`, which is unbounded in `r` as long as
137/// [`TimeoutConfig::timeout_increment`] is non-zero. **With `timeout_increment` set to zero the
138/// assumption fails**, and a deployment whose round trip exceeds `base_timeout` can advance
139/// rounds forever without any of them lasting long enough to finish — the classic
140/// livelock this growth exists to prevent.
141///
142/// [`ChainOwnership::round_timeout`]: linera_base::ownership::ChainOwnership::round_timeout
143/// [`TimeoutConfig::timeout_increment`]: linera_base::ownership::TimeoutConfig::timeout_increment
144pub trait RoundTimeoutGrowth {}
145
146/// **Assumption (Clock accuracy).** Correct validators' clocks
147/// (`linera_storage::Clock::current_time`) advance in real time and agree within a bound small
148/// compared to the round timeout.
149///
150/// Two places consume this. A validator refuses to sign a timeout vote before its own
151/// [`round_timeout`] has passed ([`TimeoutVoteConditions`]), so a quorum's clocks must
152/// approximately agree for a timeout certificate to form at all. And a validator rejects a
153/// proposal whose timestamp is further in the future than
154/// `ChainWorkerConfig::block_time_grace_period` with `WorkerError::InvalidTimestamp`, so a
155/// leader whose clock runs fast cannot get its blocks accepted. The client reports the latter
156/// back: `Client::submit_block_proposal` warns once a
157/// [`validity_threshold`](linera_execution::committee::Committee::validity_threshold) of
158/// validators have reported skew.
159///
160/// [`round_timeout`]: linera_chain::manager::ChainManager::round_timeout
161/// [`TimeoutVoteConditions`]: linera_chain::manager::proof::timeouts::TimeoutVoteConditions
162pub trait ClockAccuracy {}
163
164/// **Assumption (Full reachability during synchronization).** After GST, a correct driver's
165/// [`ChainClient::synchronize_chain_state`] reaches *every* correct validator, not merely a
166/// quorum of them.
167///
168/// This is the weakest link in the liveness argument, and it is stated separately for that
169/// reason. [`LockRecovery`] needs the proposer to learn the highest lock held by *any* correct
170/// validator, because a single correct validator holding a lock above the proposer's will reject
171/// the proposal ([`UnlockingRequiresHigherCertificate`]) and may be exactly the weight that a
172/// quorum was missing.
173///
174/// **What the implementation actually guarantees is weaker.**
175/// `Client::synchronize_chain_from_committee` dispatches
176/// `synchronize_chain_state_from` to every validator but aggregates through
177/// `communicate_with_quorum`, which stops once a quorum has answered plus a grace period of
178/// `quorum_grace_period` (default [`DEFAULT_QUORUM_GRACE_PERIOD`], 0.2) times the time that took;
179/// still-pending responses are then dropped. A correct but slow validator holding the highest
180/// lock can therefore be missed.
181///
182/// **Why this is usually not observable.** A validator holds a lock at round `p` only because it
183/// received a [`ValidatedBlockCertificate`] at `p`, which the client that assembled it pushes to
184/// every validator; so the ordinary way for a lock to exist at one validator and not at a quorum
185/// is a partition that GST has since healed, followed by that validator being slow in exactly the
186/// synchronization that matters. `ChainClient::process_pending_block` also retries: on a rejection
187/// whose consensus-state snapshot is unchanged it performs one explicit fallback
188/// `synchronize_chain_state` and retries, and the caller's loop re-enters the whole procedure. The
189/// combination makes the assumption hold with probability tending to one over retries rather than
190/// deterministically.
191///
192/// **Residual obligation.** Making this a theorem rather than an assumption requires the
193/// synchronization step to wait for all correct validators — or, more cheaply, for a rejection
194/// carrying [`ChainError::HasIncompatibleConfirmedVote`] to trigger a targeted pull from the
195/// rejecting validator, as `NodeError::WrongRound` already does through
196/// `chain_client::Error::LocalNodeLagging`. It does not: the lag-report path in
197/// `RemoteNodeUpdater::sync_remote_if_needed` matches only the round and height mismatches. The
198/// fallback path is also noted as untested in the source (`TODO(#6453)`).
199///
200/// [`ChainClient::synchronize_chain_state`]: crate::client::ChainClient::synchronize_chain_state
201/// [`DEFAULT_QUORUM_GRACE_PERIOD`]: crate::DEFAULT_QUORUM_GRACE_PERIOD
202/// [`ValidatedBlockCertificate`]: linera_chain::types::ValidatedBlockCertificate
203/// [`ChainError::HasIncompatibleConfirmedVote`]: linera_chain::ChainError::HasIncompatibleConfirmedVote
204/// [`LockRecovery`]: super::progress::LockRecovery
205/// [`UnlockingRequiresHigherCertificate`]: linera_chain::manager::proof::voting::UnlockingRequiresHigherCertificate
206pub trait FullReachability {}