linera_core/proof/storage.rs
1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! What may be assumed about anything read back from storage.
5//!
6//! Every result elsewhere in this specification reads state before it reasons about it: a vote is
7//! justified by the manager state, a block by its ancestors, a bundle by an inbox cursor. What
8//! entitles the reader to act on those bytes is the subject here.
9//!
10//! Stored data divides by what its **validity proof** is — the artifact a reader could check to
11//! establish that what came back is what should have been there:
12//!
13//! | kind | validity proof | if it were wrong |
14//! |---|---|---|
15//! | *certified* | a certificate, checked once on admission | detected by re-verifying signatures |
16//! | *derived* | **none** | undetectable; can only be recomputed from the certified prefix |
17//! | *configuration* | none; assumed identical network-wide | not detectable |
18//!
19//! Content addressing sits beside this rather than inside it. A hash key proves *integrity* — that
20//! the bytes are the ones the key names — which is a different question from whether the data ought
21//! to be there at all. A blob's validity proof is a certificate like anything else's.
22//!
23//! The third row is the one that carries risk, and the specification has been quietly relying on
24//! it: the `ChainError::CorruptedChainState` sites in `linera_chain::chain` are assertions about
25//! derived data, made at the point of use because there is nothing to check it against earlier.
26//!
27//! The last statement turns the classification outward: two correct validators at equal heights
28//! agree on everything with a validity proof, and are entitled to differ on everything without one.
29//!
30//! Shared storage is partitioned by `RootKey` — `BlobId`, `BlockHash`, `Event`, `BlockByHeight`,
31//! `EventBlockHeight`, `ChainState`, `NetworkDescription`, `BlockExporterState` — and the
32//! classification runs across that partition rather than along it: `ChainState` alone holds fields
33//! of three different kinds.
34
35use linera_chain::manager::proof::model::{CorrectValidator, MaxByzantineWeight, StorageAtomicity};
36
37use super::availability::BlockOutputsArePersisted;
38
39/// **Lemma (Content addressing proves integrity, not validity).** For a blob, the key determines
40/// the value: a `BlobId` is a hash of the content together with its `BlobType`, so bytes stored
41/// under it are either the ones the key names or detectably wrong, with no appeal to who wrote
42/// them. Certificates are keyed the same way, by the hash of the block they confirm.
43///
44/// This settles *substitution* and nothing else. That a blob was paid for, and that anyone is
45/// obliged to keep its bytes available, are separate claims with a separate proof —
46/// [`BlobValidityRestsOnCertificates`].
47///
48/// *Proof.* `RootKey::BlobId(blob.id())` derives the storage root from `Blob::id`, which hashes the
49/// content. Recomputing the id of what comes back and comparing it to the key is therefore a
50/// complete check, requiring no committee, no signature and no trust in the store. ∎
51///
52/// **Where the check is actually performed.** At trust boundaries, and only there. A blob arriving
53/// from another node goes through `RemoteNode::download_blob`, which builds `Blob::new(blob)` —
54/// recomputing the id — and rejects the response when `blob.id() != blob_id`. A blob read back from
55/// this node's own storage does not: `DbStorage::read_blob` constructs it with
56/// `Blob::new_with_id_unchecked`, taking the store at its word.
57///
58/// That asymmetry is deliberate and worth stating, because it locates the residual trust exactly.
59/// Content addressing does not make storage trustworthy; it makes storage *auditable*, and the
60/// implementation spends that audit where data crosses from a party it does not trust. Within a
61/// validator, a store that returns the wrong bytes under a blob key is undetected — which
62/// [`StorageAtomicity`] does not cover either, being about whether a write lands, not about whether
63/// a read is faithful.
64///
65/// This is what [`AccountabilityScope`] means when it says blob integrity is free, and what
66/// `CheckpointRestoresExecutionState` relies on when it says a node fetching an execution-state dump
67/// cannot be handed different bytes.
68///
69/// [`AccountabilityScope`]: linera_chain::justification::proof::AccountabilityScope
70pub trait ContentAddressingProvesIntegrity: CorrectValidator {}
71
72/// **Lemma (A blob's validity rests on certificates, not on its hash).** A blob held by a correct
73/// validator is one that a confirmed block published — and so paid for — and every later block that
74/// uses it re-attests that it is still owed. Its hash establishes which bytes it is; its
75/// certificates establish that it is entitled to exist.
76///
77/// *Proof.* `BlobState` records exactly this and nothing more:
78///
79/// | field | what it proves |
80/// |---|---|
81/// | `origin` | `BlobOrigin::Published { chain_id, block_height }` names the confirmed block that published it, which is where publication was charged; `BlobOrigin::Genesis` is the one exception, holding for blobs every node has from the genesis config |
82/// | `last_used_by` | the hash of the most recent certificate that published *or used* the blob — a later quorum's attestation that it is still required |
83/// | `epoch` | the epoch of that certificate, so the attestation can be weighed against which committees are still trusted |
84///
85/// Publication is charged by the block that performs it, at `blob_published` per blob and
86/// `blob_byte_published` per byte, and admission is bounded in count and size — that is
87/// [`BlobAdmissionIsBounded`]. Use is re-recorded through `maybe_write_blob_states`, which carries
88/// forward the certificate that last needed the blob. ∎
89///
90/// **Re-certification is the same mechanism as elsewhere, applied to bytes.** A blob's entitlement
91/// does not expire with the committee that first certified it, because each subsequent use is a
92/// fresh attestation under a fresh epoch — the pattern
93/// `linera_chain::proof::checkpoints::CheckpointRecertifiesReferencedBlocks` applies to blocks an
94/// outbox still references. `BlobState`'s `epoch` field is what makes it checkable.
95///
96/// **What this does not give is availability.** Nothing here obliges anyone to still hold the bytes:
97/// a validity proof establishes that a blob *should* be retrievable, not that it *is*. That gap is
98/// [`BlobRetention`], which is currently discharged by omission, since nothing deletes blobs. The
99/// shape of `BlobState` is what a retention policy would have to be keyed on — a blob whose
100/// `last_used_by` certificate is in a still-trusted epoch is one some live block may still require,
101/// which is a different question from how old it is.
102///
103/// [`BlobAdmissionIsBounded`]: super::availability::BlobAdmissionIsBounded
104/// [`BlobRetention`]: super::assumptions::BlobRetention
105pub trait BlobValidityRestsOnCertificates: ContentAddressingProvesIntegrity {}
106
107/// **Lemma (Nothing enters shared storage without its validity proof having been checked).** A
108/// certificate, a blob or an event in a correct validator's shared storage was verified against the
109/// committee for its epoch before it was written.
110///
111/// *Code correspondence.*
112///
113/// | | |
114/// |---|---|
115/// | transition | `ChainWorkerState::process_confirmed_block` |
116/// | reads | `committee_for_epoch(block.header.epoch)` |
117/// | writes | `write_blobs_and_certificate`, then `write_events` |
118/// | precondition | `certificate.check` returned `Ok`, before either write |
119///
120/// *Proof.* `process_confirmed_block` resolves the committee for the block's declared epoch and
121/// calls `certificate.check` against it. Only afterwards does it write: the certificate and the
122/// block's required blobs through `write_blobs_and_certificate`, then the block's events through
123/// `write_events`. Both writes are inside the branch guarded by that check, and the ordering is
124/// [`BlockOutputsArePersisted`]'s. A blob admitted *ahead* of certification takes the other route,
125/// `handle_pending_blob`, which admits only blobs a pending proposal or validated block expects and
126/// fails with `WorkerError::UnexpectedBlob` otherwise. ∎
127///
128/// **The proof is checked once, not on every read.** Nothing re-verifies a certificate's signatures
129/// when it is read back, so a reader inside the validator relies on the admission check having
130/// happened rather than on the certificate in hand. The proof remains *attached* — a certificate
131/// read from storage can be re-verified, and is, when it crosses to another node — so this is a
132/// choice about where to spend verification, not a loss of evidence. It is the same shape as
133/// [`ContentAddressingProvesIntegrity`]'s asymmetry, for a different kind of proof.
134///
135/// **Events inherit their proof rather than carrying one.** An event has no signature of its own:
136/// it is valid because the block that emitted it is certified, and it is written in the same guarded
137/// branch. A reader that has the event but not that block is trusting the writer — which is what
138/// `linera_chain::proof::checkpoints::EventFloorTracksCheckpoints` means when it says a
139/// cross-chain read resolves only at or above a stream's floor.
140pub trait AdmissionChecksTheValidityProof:
141 BlobValidityRestsOnCertificates + BlockOutputsArePersisted
142{
143}
144
145/// **Invariant (Derived state agrees with the certified prefix).** The parts of a chain's state that
146/// are not certified — the block-height indexes, the outbox counters and queues, the inbox cursors —
147/// are functions of that chain's committed blocks, and equal the value that recomputing them from
148/// those blocks would give.
149///
150/// **This is the class with no validity proof.** A wrong index or a wrong counter is not detectable
151/// by rehashing or by re-verifying signatures, because nobody attested it and nothing determines it
152/// but the computation that produced it. It can only be *recomputed*. That is why this is stated as
153/// an invariant over transitions rather than as a property a reader can check.
154///
155/// *Proof.* Each is written only by the transition that commits a block, under the exclusive access
156/// of [`SequentialChainState`] and the atomicity of [`StorageAtomicity`], so the sequence of values
157/// it takes follows the sequence of committed blocks; and by
158/// `linera_chain::manager::proof::safety::UniqueChain` that sequence is unique. The base case is an
159/// empty chain, where every one of these is empty or zero. ∎
160///
161/// **Detection is partial, late, and by assertion.** Because there is no proof to check, the
162/// implementation catches violations only where a reader happens to require an entry that should be
163/// there. `ChainError::CorruptedChainState` is raised at seven sites in `linera_chain::chain`,
164/// including:
165///
166/// * `"message counter should be present"` — an outbox counter missing for a queue entry;
167/// * `"Missing outboxes"` — a `nonempty_outboxes` entry with no outbox behind it;
168/// * `"missing entry in block_hashes"`, at three separate call sites — the height index short of the
169/// tip.
170///
171/// Each fires at the point of use, which may be arbitrarily long after the write that broke the
172/// invariant, and none of them fires for a value that is present but *wrong*. A counter that is
173/// merely too small is not detected at all until the queue drains past it.
174///
175/// **Recovery is by recomputation, which is the only option available.** `reconcile_tracked_outboxes`
176/// rebuilds the outbox index, and `ChainStateView::restore_outboxes_from_unfinalized` rebuilds
177/// `outboxes`, `outbox_counters` and `nonempty_outboxes` from the on-chain
178/// `unfinalized_message_blocks` after a checkpoint bootstrap. Both are re-derivations from data that
179/// *does* have a validity proof, which is what makes them trustworthy where the derived state was
180/// not.
181///
182/// **Not covered here.** Fields with no invariant at all, because they are legitimately local and
183/// may differ between correct validators or be dropped without fault: `pending_proposed_blobs` and
184/// `pending_validated_blobs` (cleared when the manager is reset), `pre_checkpoint_block_trust`
185/// (transient, emptied as the certificates arrive), and `received_log`, whose order depends on when
186/// certificates were received rather than on what was committed.
187///
188/// [`SequentialChainState`]: linera_chain::manager::proof::model::SequentialChainState
189pub trait DerivedStateAgreesWithCertifiedPrefix:
190 AdmissionChecksTheValidityProof + StorageAtomicity + MaxByzantineWeight
191{
192}
193
194/// **Caveat (An inbox entry is never reclaimed).** Every structure cross-chain messaging uses is
195/// bounded except one: a recipient keeps an inbox entry for each chain that has *ever* sent it a
196/// message, permanently.
197///
198/// What is reclaimed: an outbox queue drains as bundles are delivered and confirmed, and the outbox
199/// itself is then removed — `outboxes.remove_entry(target)` once the queue is empty and not ahead of
200/// the tip, with `outbox_counters` and `nonempty_outboxes` cleared alongside. Queued and anticipated
201/// bundles leave `added_bundles` on consumption and `removed_bundles` on arrival. The sender's
202/// `unfinalized_message_blocks` is trimmed as recipients acknowledge.
203///
204/// What is not: nothing anywhere removes an entry from `ChainStateView::inboxes`. Once an origin has
205/// delivered a single bundle, its `InboxStateView` — cursors and empty queues — persists for the
206/// life of the chain. The residue is small per origin and unbounded in count, so the cost falls on
207/// exactly the chains a network wants to encourage: a widely used application chain pays for every
208/// counterparty it has ever had.
209///
210/// **Checkpointing preserves this rather than clearing it, by design.**
211/// `PreparedCheckpoint::inbox_cursors` records *every* inbox with a non-default
212/// `next_cursor_to_remove`, so a node bootstrapping from a checkpoint recreates the full set of
213/// origins rather than starting clean. That is deliberate and load-bearing: by
214/// `linera_chain::proof::checkpoints::CheckpointPreservesConsumptionBoundary` each origin's
215/// `restored_cursor` is what turns a re-pushed already-consumed bundle into a no-op instead of a
216/// duplicate consumption. Reclaiming an inbox would forget that boundary, so the two goals are in
217/// direct tension and the current design resolves it in favour of correctness.
218pub trait InboxEntriesAreNeverReclaimed: DerivedStateAgreesWithCertifiedPrefix {}
219
220/// **Theorem (Storage converges at equal heights).** Take two correct validators that agree on the
221/// tip height of every chain. Once cross-chain delivery has quiesced at both — no bundle derivable
222/// from a committed block is still undelivered internally — their storage agrees on everything the
223/// protocol determines:
224///
225/// | | agrees | why |
226/// |---|---|---|
227/// | execution state of every chain | yes, and *certifiably* so | it is a function of the committed prefix, and the last block's `state_hash` attests the value |
228/// | committed blocks, their certificates, their events | yes | same prefix, and each is certified data |
229/// | blobs a committed block requires | yes | named by the blocks, which agree |
230/// | derived indexes and counters | yes | functions of the same prefix ([`DerivedStateAgreesWithCertifiedPrefix`]) |
231/// | inbox consumption boundaries | yes | fixed by which bundles the committed blocks consumed |
232/// | inbox queues and outbox queues | yes, **only after quiescence** | they hold what is delivered but not yet consumed, which is a function of the prefix *plus* delivery progress |
233///
234/// This is the first statement here about two validators rather than one, and it is what would make
235/// a divergence *detectable*: at equal heights, two correct validators cannot differ on any row
236/// above, so a difference convicts one of them of being faulty — which is the missing half of
237/// [`AccountabilityScope`], where a mis-executed block leaves no forensic residue.
238///
239/// *Proof.* Fix a chain and a common tip height `h`.
240///
241/// *The prefixes coincide.* By [`CommitAgreement`] at most one block is certified per height, and by
242/// `UniqueChain` the committed sequence below `h` is unique. Both validators reached `h` only
243/// through valid certificates (`TipAdvancesOnlyOnValidCertificate`), so they hold the same blocks at
244/// every height below `h`. Note the hypothesis is only about *heights*: agreement on content follows
245/// rather than being assumed, and it is [`MaxByzantineWeight`] that makes it follow.
246///
247/// *Execution state follows the prefix.* By [`DerivedStateAgreesWithCertifiedPrefix`] each
248/// validator's execution state equals the result of executing its committed prefix, and by
249/// `DeterministicExecution` executing the same prefix yields the same result. The equality is
250/// moreover *witnessed*: the `state_hash` in the block at `h - 1` is covered by that block's hash and
251/// certified, so the agreed value is one a quorum attested rather than one each validator merely
252/// computed.
253///
254/// *Derived state follows too*, by the same lemma — the height indexes, outbox counters and
255/// `nonempty_*` sets are functions of the prefix.
256///
257/// *Message state needs the quiescence hypothesis.* What a chain has *consumed* is fixed by its
258/// committed blocks, so inbox consumption boundaries agree immediately. What is *queued* is not: a
259/// bundle is derived from a committed block of the sending chain and then delivered by that
260/// validator's own worker for that chain ([`InboxHoldsOnlySentBundles`]), so at any instant one
261/// validator may have delivered internally what the other has not. Since the sending chains are at
262/// equal heights, both derive the same bundles ([`EffectsSurviveRestart`], sender half); once
263/// delivery has quiesced both have delivered all of them, and by
264/// [`BundleConsumedAtMostOnce`] neither has consumed one twice. The queues therefore coincide. ∎
265///
266/// **What does not converge, and need not.** Three classes, all legitimate.
267///
268/// *Retained history.* A validator that bootstrapped from a checkpoint holds a pruned chain: it has
269/// the execution state without the blocks below the checkpoint, and events below a stream's floor
270/// are gone ([`EventFloorTracksCheckpoints`]). So two validators at the same heights may hold
271/// genuinely different *sets* of blocks and events, and the theorem above claims agreement only on
272/// what they both retain. This is the sharpest limit on any consistency check built from it: a
273/// missing block is not evidence of a fault.
274///
275/// *Work ahead of the tip.* `next_height_to_preprocess` may exceed the tip by different amounts,
276/// since preprocessing a block does not advance it. One validator may hold certificates and outbox
277/// updates for blocks the other has not seen.
278///
279/// *Local fields.* `pending_proposed_blobs`, `pending_validated_blobs`, `pre_checkpoint_block_trust`
280/// and `received_log` are per-validator by construction — the last records the order in which
281/// certificates arrived, which is not a function of anything committed.
282///
283/// **Quiescence is a hypothesis, not a guarantee.** Nothing here says delivery ever quiesces; that
284/// would need the outbox to be drained, which no statement provides. So this is a conditional
285/// convergence result, and the condition is exactly the one the messaging theme has yet to
286/// discharge.
287///
288/// [`CommitAgreement`]: linera_chain::manager::proof::safety::CommitAgreement
289/// [`MaxByzantineWeight`]: linera_chain::manager::proof::model::MaxByzantineWeight
290/// [`AccountabilityScope`]: linera_chain::justification::proof::AccountabilityScope
291/// [`EventFloorTracksCheckpoints`]: linera_chain::proof::checkpoints::EventFloorTracksCheckpoints
292/// [`InboxHoldsOnlySentBundles`]: super::availability::InboxHoldsOnlySentBundles
293/// [`EffectsSurviveRestart`]: super::availability::EffectsSurviveRestart
294/// [`BundleConsumedAtMostOnce`]: super::availability::BundleConsumedAtMostOnce
295pub trait StorageConvergesAtEqualHeights: DerivedStateAgreesWithCertifiedPrefix {}