Skip to main content

linera_chain/manager/proof/
model.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! System model and the assumptions the safety argument rests on.
5//!
6//! The definitions here fix what a consensus instance is and what it means for two blocks to
7//! conflict; the assumptions are the things the implementation does *not* prove and that a
8//! deployment must supply. Everything in this module is a leaf of the dependency graph.
9//!
10//! The assumptions needed only for progress — synchrony, availability, leader fairness, an
11//! active driver — are stated separately in `linera_core::proof::assumptions`, because their
12//! evidence lives in that crate. Safety depends on none of them.
13
14/// **Definition (Consensus instance).** The protocol decides one block at a time, per chain.
15/// A *consensus instance* is a pair `(chain, height)`, and its state is one [`ChainManager`],
16/// reachable as [`ChainStateView::manager`], whose height is
17/// [`ChainTipState::next_block_height`].
18///
19/// [`ChainManager::reset`] destroys an instance and creates the next one: it calls `clear()` on
20/// every view, re-derives the leader distributions from the new ownership, and sets
21/// [`ChainManager::current_round`] to [`ChainOwnership::first_round`]. It is called from exactly
22/// two places in `linera_chain::chain`: `initialize_if_needed`, when the chain becomes active at
23/// height `0`, and `reset_chain_manager`, immediately after a confirmed block at height `h` has
24/// been executed, for height `h + 1`.
25///
26/// Consequently **every invariant in [`crate::manager::proof::locking`] is scoped to a single
27/// instance**: it holds from the moment the instance is created until it is reset, and says
28/// nothing across a reset. That is sound because a reset happens only once the height's block is
29/// committed, so a later instance decides a different height. Restoring state *across* a reset
30/// is the one exception, handled by [`SafetyStateRecovery`].
31///
32/// [`ChainManager`]: crate::manager::ChainManager
33/// [`ChainManager::reset`]: crate::manager::ChainManager::reset
34/// [`ChainManager::current_round`]: crate::manager::ChainManager::current_round
35/// [`ChainStateView::manager`]: crate::ChainStateView::manager
36/// [`ChainTipState::next_block_height`]: crate::ChainTipState::next_block_height
37/// [`ChainOwnership::first_round`]: linera_base::ownership::ChainOwnership::first_round
38/// [`SafetyStateRecovery`]: crate::manager::proof::locking::SafetyStateRecovery
39pub trait ConsensusInstance {}
40
41/// **Definition (Round order).** Rounds are [`Round`] values, totally ordered by the derived
42/// [`Ord`] on the enum, which orders first by variant and then by the contained `u32`:
43///
44/// ```text
45/// Round::Fast  <  Round::MultiLeader(0) < Round::MultiLeader(1) < …
46///              <  Round::SingleLeader(0) < Round::SingleLeader(1) < …
47///              <  Round::Validator(0)  < Round::Validator(1)  < …
48/// ```
49///
50/// In particular [`Round::Fast`] is the global minimum, which several arguments use directly:
51/// a guard of the form `x.round() < Round::Fast` is unsatisfiable.
52///
53/// The successor function is [`ChainOwnership::next_round`], which is *not* the successor of
54/// this order — it skips the multi-leader rounds a chain is not configured for and saturates
55/// into [`Round::Validator`]. It is monotone, which is all the round-advancement results need.
56///
57/// [`Round`]: linera_base::data_types::Round
58/// [`Round::Fast`]: linera_base::data_types::Round::Fast
59/// [`Round::Validator`]: linera_base::data_types::Round::Validator
60/// [`ChainOwnership::next_round`]: linera_base::ownership::ChainOwnership::next_round
61pub trait RoundOrder {}
62
63/// **Definition (Correct validator).** A validator is *correct* in an execution if every
64/// signature it produces was produced by an unmodified build of this code, driven through the
65/// public entry points of `linera_core::worker::WorkerState`, with a private key no other party
66/// holds. A validator that is not correct is *faulty*, and may sign anything at any time,
67/// including contradictory statements.
68///
69/// This is what licenses the arguments in [`crate::manager::proof::voting`]: for a correct
70/// validator, "it signed a validation vote for `B` in round `r`" implies its
71/// [`ChainManager`](crate::manager::ChainManager) state satisfied the guards on the path that
72/// produces such a vote, because that path is the only one that can produce it
73/// ([`VoteConstructionSites`]).
74///
75/// Note this is a statement about *signing*, not about availability. A correct validator may be
76/// slow or unreachable without becoming faulty, and in particular **it may crash at any time and
77/// restart**, losing whatever it had not yet persisted. Crash-recovery, not fail-stop, is the
78/// model: before GST crashes may be arbitrarily frequent and restarts arbitrarily slow; after
79/// GST, recovery is bounded by `linera_core::proof::assumptions::BoundedRecovery`.
80///
81/// That is what makes [`DurablePersistence`] load-bearing rather than hygienic. A validator that
82/// signed a vote and crashed before saving it would, on restart, have no record of having voted —
83/// and could vote again in the same round, breaking
84/// [`OneValidationVotePerRound`](crate::manager::proof::locking::OneValidationVotePerRound). That
85/// is a *safety* failure, not a lost message, and it is why the persistence obligation is stated
86/// as a condition of correctness rather than as an implementation detail.
87///
88/// [`DurablePersistence`]: self::DurablePersistence
89///
90/// [`VoteConstructionSites`]: crate::manager::proof::voting::VoteConstructionSites
91pub trait CorrectValidator {}
92
93/// **Definition (Conflicting blocks).** Two [`Block`]s *conflict* when they have the same
94/// [`chain_id`] and [`height`] but different hashes. Certificates certify [`ConfirmedBlock`] and
95/// [`ValidatedBlock`] values, each of which wraps a [`Block`] and hashes to that block's hash,
96/// so "certificates for conflicting blocks" is well defined.
97///
98/// Note that a [`Block`] is a [`ProposedBlock`] *together with* its
99/// [`BlockExecutionOutcome`]. Two blocks with the same proposal but different outcomes therefore
100/// conflict. This is deliberate: they lead to different chain states, so agreement must exclude
101/// them, and the exclusion is discharged by [`DeterministicExecution`].
102///
103/// Ancestry needs no separate definition here: [`ChainTipState::verify_block_chaining`] requires
104/// a proposal's height to equal the tip's next height and its `previous_block_hash` to equal the
105/// tip's block hash, so the committed blocks of a chain form a hash-linked list, one per height.
106///
107/// [`Block`]: crate::block::Block
108/// [`chain_id`]: crate::block::BlockHeader::chain_id
109/// [`height`]: crate::block::BlockHeader::height
110/// [`ConfirmedBlock`]: crate::block::ConfirmedBlock
111/// [`ValidatedBlock`]: crate::block::ValidatedBlock
112/// [`ProposedBlock`]: crate::data_types::ProposedBlock
113/// [`BlockExecutionOutcome`]: crate::data_types::BlockExecutionOutcome
114/// [`ChainTipState::verify_block_chaining`]: crate::ChainTipState::verify_block_chaining
115pub trait ConflictingBlocks {}
116
117/// **Assumption (Maximum Byzantine weight, per epoch).** For **every** epoch whose committee has
118/// not been revoked, the total [`Committee::weight`] of faulty validators *in that committee* is
119/// strictly less than its [`Committee::validity_threshold`] — at most `f⁺ − 1` where
120/// `f⁺ = ⌈N/3⌉`, equivalently strictly below one third of that committee's total weight.
121///
122/// This is the only fault bound assumed anywhere in the specification, and it is assumed once per
123/// live committee rather than once globally. The quantifier carries most of the content.
124///
125/// *Committees are bounded independently.* Membership and weights differ between epochs, so this
126/// is a separate hypothesis for each: a validator may hold weight in several committees, or in
127/// none, and being faulty is a property of a validator *within* a committee. Every quorum argument
128/// here reasons inside a single committee — two quorums drawn from different committees need not
129/// intersect at all — and [`EpochAgreement`] is what confines each argument to one.
130///
131/// *A revoked committee is assumed nothing.* Revocation is exactly the withdrawal of this
132/// hypothesis. Once the admin chain has written a removal event for an epoch — which is what
133/// `Storage::is_epoch_revoked` tests — that committee may be arbitrarily corrupt and its
134/// signatures establish nothing on their own. This is why material from a revoked epoch is
135/// admitted only when a still-trusted committee vouches for it, as
136/// `ChainWorkerState::select_message_bundles` does for bundles whose epoch has lapsed.
137///
138/// *Not revoking is not free.* Revocations are ordered
139/// ([`AdminOperation::RemoveCommittee`] requires them to be sequential), so the revoked epochs
140/// form a prefix and the assumed-correct ones a suffix. That suffix gains a committee whenever one
141/// is created and loses one only on revocation. A deployment that never revokes is therefore
142/// assuming, permanently, that *every committee it has ever created* still satisfies the bound —
143/// including validator sets long since rotated out. The assumption does not weaken with time; it
144/// accumulates.
145///
146/// [`AdminOperation::RemoveCommittee`]: linera_execution::system::AdminOperation::RemoveCommittee
147/// [`Committee::weight`]: linera_execution::committee::Committee::weight
148/// [`Committee::validity_threshold`]: linera_execution::committee::Committee::validity_threshold
149pub trait MaxByzantineWeight {}
150
151/// **Assumption (Epoch agreement).** All correct validators evaluate a given consensus instance
152/// against the same [`Committee`].
153///
154/// This is what makes [`Intersection`] applicable to two certificates for the same height: two
155/// quorums of *different* committees need not intersect at all. It is close to enforced rather
156/// than assumed, by two different mechanisms.
157///
158/// *Which committee judges a certificate* is a function of the block's own declared epoch, not of
159/// the judging node's state: `ChainWorkerState::process_confirmed_block` fetches
160/// `committee_for_epoch(block.header.epoch)` and verifies the certificate against that committee.
161/// Since [`ProposedBlock::epoch`] is covered by the block hash, two correct validators never
162/// disagree about which committee a given certificate is judged by. This is what the assumption
163/// needs, and it is unconditional.
164///
165/// *That the epoch is also the chain's current one* is enforced separately, by `check_block_epoch`,
166/// at three sites: `try_handle_block_proposal` before voting, `process_validated_block` before
167/// verifying the certificate, and `execute_contiguous_block` before applying a confirmed block.
168/// Note it is **not** applied in `process_confirmed_block` itself — deliberately, since a node
169/// catching up must accept certificates from earlier epochs — so a merely *preprocessed* block is
170/// never epoch-checked in this sense, which is consistent with preprocessing not advancing the tip
171/// ([`TipAdvancesOnlyOnValidCertificate`]).
172///
173/// The chain's current epoch at height `h` is a function of the committed blocks below `h`, which
174/// [`UniqueChain`] shows is unique — so the assumption is discharged for height `h` by the
175/// agreement result at heights below `h`, and the induction in [`UniqueChain`] is what makes
176/// that non-circular.
177///
178/// What remains genuinely assumed is that the committee for an epoch is itself agreed, which
179/// holds because it is published by a committed block on the admin chain.
180///
181/// [`Committee`]: linera_execution::committee::Committee
182/// [`ProposedBlock::epoch`]: crate::data_types::ProposedBlock::epoch
183/// [`Intersection`]: crate::data_types::proof::quorum::Intersection
184/// [`UniqueChain`]: crate::manager::proof::safety::UniqueChain
185/// [`TipAdvancesOnlyOnValidCertificate`]: crate::manager::proof::commit::TipAdvancesOnlyOnValidCertificate
186pub trait EpochAgreement {}
187
188/// **Assumption (Cryptographic soundness).** [`ValidatorSignature`] is existentially unforgeable:
189/// no party without a validator's secret key produces a signature that
190/// [`ValidatorSignature::check`] accepts for that validator's public key. [`CryptoHash`] is
191/// collision resistant, so distinct values — in particular distinct [`Block`]s and distinct
192/// [`VoteValue`]s — have distinct hashes.
193///
194/// Collision resistance is what lets the specification move between "the certificate's
195/// `value_hash`" and "the block", and between block equality and hash equality.
196///
197/// [`ValidatorSignature`]: linera_base::crypto::ValidatorSignature
198/// [`ValidatorSignature::check`]: linera_base::crypto::ValidatorSignature::check
199/// [`CryptoHash`]: linera_base::crypto::CryptoHash
200/// [`Block`]: crate::block::Block
201/// [`VoteValue`]: crate::data_types::VoteValue
202pub trait UnforgeableSignatures {}
203
204/// **Assumption (Durable persistence).** A correct validator persists its
205/// [`ChainManager`](crate::manager::ChainManager) state before releasing a vote to the network,
206/// and that state survives a crash.
207///
208/// The implementation is structured to make this hold: every path in
209/// `linera_core::chain_worker::state` that mutates the manager calls `self.save()` before
210/// returning the chain info response that carries the vote — `try_handle_block_proposal` after
211/// `create_vote`, `process_validated_block` after `create_final_vote`,
212/// `vote_for_leader_timeout` after `create_timeout_vote`, and `vote_for_fallback` after
213/// `vote_fallback`. The response projection is [`ChainManagerInfo`].
214///
215/// Without this, a crash could lose the record of a vote and let the validator vote again,
216/// breaking [`OneValidationVotePerRound`] and [`OneConfirmationVotePerRound`], which are the
217/// only places where the assumption is consumed. It is one half of a discipline whose other half
218/// — that an effect already persisted is never *lost* — is `linera_core::proof::availability`. A
219/// validator that violates it is faulty in the sense of [`CorrectValidator`], and is counted
220/// against [`MaxByzantineWeight`].
221///
222/// [`OneValidationVotePerRound`]: crate::manager::proof::locking::OneValidationVotePerRound
223/// [`OneConfirmationVotePerRound`]: crate::manager::proof::locking::OneConfirmationVotePerRound
224/// [`ChainManagerInfo`]: crate::manager::ChainManagerInfo
225pub trait DurablePersistence {}
226
227/// **Assumption (Sequential instance state).** The transitions of one consensus instance are
228/// mutually exclusive and each runs to completion: no two of them interleave their reads and
229/// writes of the same [`ChainManager`](crate::manager::ChainManager).
230///
231/// Exclusivity has two halves, and only the second is in this repository's control.
232///
233/// *Within a worker process*, `linera_core::worker::WorkerState` reaches a chain only through a
234/// per-chain `tokio::sync::RwLock<ChainWorkerState>` — `chain_write` for transitions, `chain_read`
235/// for queries. A transition holds the write side for its whole duration, and the manager is
236/// `!Sync`-by-construction behind the guard.
237///
238/// *Across the processes of one validator*, each chain belongs to exactly one worker, because
239/// shard assignment is static: `ValidatorInternalNetworkPreConfig::get_shard_id` in `linera-rpc`
240/// is a pure function of the validator's public key, the chain id and `shards.len()`, with no
241/// leases and no handoff. Senders route by that function, so a worker is only ever asked for the
242/// chains of its own shard.
243///
244/// That partition excludes *reads* as much as writes. All shards share one backing store, so
245/// nothing physically prevents a worker from reading another shard's keys; what makes it never
246/// happen is that a worker is never asked to. A chain's mutable state is therefore touched in
247/// neither direction by any process but its owner.
248///
249/// Chains do still share storage, but only *published* artifacts: blobs, which are content
250/// addressed, and events, which a reader reaches through `OracleResponse::Event`. Both are
251/// immutable once written and belong to no chain's view. So the boundary is not "no shared
252/// storage" but "no reading another chain's state": a cross-chain dependency is either delivered
253/// as a message or read from one of those two stores, never observed in the producing chain's
254/// [`ChainManager`](crate::manager::ChainManager) or inboxes.
255/// [`IncomingBundlesMatchTheLocalInbox`] is the form that takes for the message case.
256///
257/// The specification relies on the composition whenever it reasons about "the state immediately
258/// before" a vote — for instance in [`UnlockingJustification`], where the guard evaluated by
259/// [`ChainManager::check_proposed_block`] must still describe the state when
260/// [`ChainManager::create_vote`] runs a few statements later.
261///
262/// **Residual obligation.** The second half holds only while `shards.len()` is stable and worker
263/// processes do not overlap. Changing the shard count re-partitions every chain, and a rolling
264/// restart that runs a replacement alongside its predecessor puts two processes on the same
265/// chain; in both cases they compute the same owner and write the same shared keys. Nothing in
266/// the code detects either, so this is a deployment obligation, not an enforced invariant.
267///
268/// [`IncomingBundlesMatchTheLocalInbox`]: crate::manager::proof::commit::IncomingBundlesMatchTheLocalInbox
269/// [`UnlockingJustification`]: crate::manager::proof::safety::UnlockingJustification
270/// [`ChainManager::check_proposed_block`]: crate::manager::ChainManager::check_proposed_block
271/// [`ChainManager::create_vote`]: crate::manager::ChainManager::create_vote
272pub trait SequentialChainState {}
273
274/// **Assumption (Atomic persistence).** A single `WritableKeyValueStore::write_batch` — one batch
275/// against one root key — is applied atomically, within whatever key-count and size limits the
276/// backend imposes.
277///
278/// [`DurablePersistence`] is about *when* state is written; this is about the write being
279/// indivisible. Every invariant in [`crate::manager::proof::locking`] is stated over a state
280/// reached by whole transitions, so a torn write would put the manager in a state no transition
281/// produces — for instance a [`confirmed_vote`](field@crate::manager::ChainManager::confirmed_vote)
282/// stored without the [`locking_block`](crate::manager::ChainManager::locking_block) that
283/// [`ConfirmationOnlyInCurrentRound`] installs before it.
284///
285/// **Batches larger than the backend allows.** Atomicity comes from `write_batch` alone.
286/// Journaling adds none: it *preserves* all-or-nothing at sizes a remote store such as ScyllaDB
287/// will not accept in one `write_batch`, which a chain save can exceed.
288/// `linera_views::backends::journaling` writes the oversized batch into journal blocks and commits
289/// it by atomically updating a journal header; before any later read or write, a journal found
290/// present is replayed block by block, each block's write and its header update going in a single
291/// `write_batch`. A crash part-way therefore leaves a resumable journal rather than a torn
292/// state. That slow path requires exclusive access to the keys under the chain's root — it fails
293/// with `JournalingError::JournalRequiresExclusiveAccess` otherwise — which is exactly what
294/// [`SequentialChainState`] supplies.
295///
296/// **Two things are called `write_batch`, and only one is atomic.** The store-level one above is.
297/// `DbStorage::write_batch` is not: it takes a `MultiPartitionBatch` keyed by root key, opens a
298/// store per key, and issues one independent `write_entry` per partition with `try_join_all`. So a
299/// storage call that spans partitions — `write_blobs_and_certificate`, which batches a block's
300/// blobs together with its certificate — is atomic within each partition and not across them, and
301/// a crash can leave one partition written and another not. Nothing in this specification relies
302/// on cross-partition atomicity; `linera_core::proof::availability::BlockOutputsArePersisted`
303/// carries that weight with replay instead.
304///
305/// **What a failed save costs.** Three outcomes are distinguished, and only the last is expensive:
306///
307/// * *Cancelled* — the request future is dropped part-way. `RollbackGuard` in
308///   `linera_core::chain_worker::handle` rolls the view back on drop, so no partial staging
309///   survives into the next request.
310/// * *Failed outright* — the write did not take effect. The in-memory view still agrees with
311///   storage, the error propagates, and the worker keeps serving.
312/// * *Ambiguous* — journal resolution failed, so storage may be partly advanced and the view can
313///   no longer be trusted. `ViewError::must_reload_view` reports it, `ChainWorkerState::save` sets
314///   `poisoned`, `check_not_poisoned` refuses every later use of that worker, and
315///   `evict_poisoned_worker` drops it from the cache so the next request reloads the chain from
316///   storage.
317///
318/// The guarantee the proofs rest on is therefore not that storage is never partially written, but
319/// that a partially written chain is never *read back as state*: it is either completed by journal
320/// replay or discarded along with the worker that could not complete it.
321///
322/// It also underpins the mirror property in `linera_core::proof::availability`: re-deriving a
323/// worker's undelivered effects after a restart is only meaningful if the state they are derived
324/// from is itself consistent.
325///
326/// [`ConfirmationOnlyInCurrentRound`]: crate::manager::proof::voting::ConfirmationOnlyInCurrentRound
327pub trait StorageAtomicity {}
328
329/// **Assumption (Deterministic execution).** For a fixed chain state at a height, a fixed
330/// [`ProposedBlock`], a fixed set of published blobs and a fixed multi-leader round argument,
331/// block execution returns a unique [`BlockExecutionOutcome`].
332///
333/// This is what makes [`ConflictingBlocks`] a property of the *proposal* in the cases where the
334/// protocol re-executes rather than re-uses a certified outcome. Two correct validators handed
335/// the same proposal at the same height therefore compute the same [`Block`].
336///
337/// **Residual obligation.** The round argument is
338/// [`Round::multi_leader`](linera_base::data_types::Round::multi_leader), so an outcome may in
339/// principle depend on the round — the round is readable as an oracle
340/// ([`OracleResponse::Round`]). This matters in exactly one place,
341/// [`FastRetryPreservesBlock`], where a block confirmed in [`Round::Fast`] is re-executed in a
342/// later round. The gap is closed there by the fast round's own restriction
343/// (`WorkerError::FastBlockUsingOracles` rejects a fast block that recorded any oracle
344/// response), plus determinism: an execution that never queried the round oracle cannot diverge
345/// on the round's value. Note that the restriction is checked only when the *proposal's* round
346/// is fast, so the retry itself is not re-checked; the argument leans on determinism of the
347/// execution engine rather than on a runtime check at the retry.
348///
349/// [`ProposedBlock`]: crate::data_types::ProposedBlock
350/// [`BlockExecutionOutcome`]: crate::data_types::BlockExecutionOutcome
351/// [`Block`]: crate::block::Block
352/// [`OracleResponse::Round`]: linera_base::data_types::OracleResponse::Round
353/// [`FastRetryPreservesBlock`]: crate::manager::proof::safety::FastRetryPreservesBlock
354/// [`Round::Fast`]: linera_base::data_types::Round::Fast
355pub trait DeterministicExecution {}