Skip to main content

linera_core/proof/
availability.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! What a certified block guarantees to everyone else, and what a crash costs.
5//!
6//! Agreement ([`CommitAgreement`]) says at most one block is certified per height. It says nothing
7//! about anyone being *able to act on* that block. The results here supply the other half: once a
8//! quorum has certified a block, any node — a validator that was down, a validator that did not
9//! exist yet, a client — can obtain it and everything needed to execute it.
10//!
11//! [`CommitAgreement`]: linera_chain::manager::proof::safety::CommitAgreement
12
13use linera_chain::manager::proof::{
14    commit::{CommittedBlock, IncomingBundlesMatchTheLocalInbox},
15    model::{CorrectValidator, SequentialChainState, StorageAtomicity},
16};
17
18use super::assumptions::{
19    BlobRetention, BoundedRecovery, CorrectValidatorAvailability, EventualSynchrony,
20};
21
22/// **Lemma (A block's outputs are persisted before it counts as processed).** When a correct
23/// validator's [`ChainTipState::next_block_height`] passes a height, the outputs of the block at
24/// that height are already in storage: the blobs it publishes, the events it emits, and the
25/// certificate itself. A crash before that point costs nothing but repeated work — the tip is what
26/// marks a block processed, so the block is handled again on restart and the writes are redone.
27///
28/// *Code correspondence.*
29///
30/// | | |
31/// |---|---|
32/// | transition | `ChainWorkerState::process_confirmed_block` |
33/// | writes | `write_blobs_and_certificate`, then `write_events`, then `maybe_write_blob_states`, and only then the tip |
34/// | re-entry guard | `tip.next_block_height > height`, returning `BlockOutcome::Skipped` |
35///
36/// *Proof.* Three parts.
37///
38/// *Ordering.* None of these writes is atomic with any other — not even within a call, since
39/// `write_blobs_and_certificate` fans out to one write per storage partition
40/// ([`StorageAtomicity`]). `process_confirmed_block` issues the three writes and only afterwards
41/// dispatches to `execute_contiguous_block` (or `execute_block_with_checkpoint_restore`), which is
42/// where `tip_state` is set and `save()` runs. The writes are three separate awaited calls, not
43/// one batch, so a crash can land between them; what the argument needs is only that all of them
44/// precede the tip.
45///
46/// *The tip is the guard.* On restart the certificate is offered again, and the early return
47/// `if !in_trust_set && tip.next_block_height > height` decides whether the block is skipped. That
48/// test reads persisted chain state, which by [`StorageAtomicity`] is consistent, so a crash
49/// before `save()` leaves the block unprocessed and every write is reissued.
50///
51/// *The repeats are byte-identical.* The events come from `block.body.events` and the blobs from
52/// `get_required_blobs` over the block's `required_blob_ids` and `created_blobs` — all fields of an
53/// already-certified block rather than products of execution. Nothing is recomputed, so the
54/// argument needs no appeal to [`DeterministicExecution`]. ∎
55///
56/// **The order is load-bearing, not incidental.** The outputs and the tip go to different key
57/// spaces of the same backing store, so one batch could in principle span them; none does. The
58/// ordering is what stands in for that atomicity, and only one order works. Outputs first costs at
59/// most repeated work, because the block is reprocessed. The tip first would be unrecoverable: the
60/// guard would classify the block as already processed and return `BlockOutcome::Skipped`, so the
61/// outputs would be missing permanently with nothing left to notice it.
62///
63/// **What becomes visible early.** Certificates, blobs and events are written to storage shared
64/// across chains — the channel by which chains observe each other at all — while the tip and the
65/// inboxes are per-chain state no other worker reads ([`SequentialChainState`]). Between the two
66/// writes, then, a block's outputs are globally readable while the producing chain has not yet
67/// recorded the block locally. That is harmless because `certificate.check` precedes every one of
68/// these writes: what becomes visible early is content a quorum has already certified, and by
69/// [`CommitAgreement`] no conflicting block can ever be certified at that height. An uncertified
70/// block reaches none of these stores.
71///
72/// [`CommitAgreement`]: linera_chain::manager::proof::safety::CommitAgreement
73///
74/// **Preprocessing persists outputs with no tip to record them.** In `Preprocess` mode, or `Auto`
75/// with an unbridgeable gap, `preprocess_certified_block` updates outboxes and event streams and
76/// deliberately does not advance the tip. The outputs of such a block are in storage, but the
77/// guard above will not short-circuit a later offer of the same certificate, so the writes are
78/// simply redone.
79///
80/// **What this does not cover.** It places a block's outputs in the *producing* validator's
81/// storage. That the resulting bundles reach the recipient chain's inbox is
82/// [`EffectsSurviveRestart`]; that the outbox is ever drained is stated nowhere.
83///
84/// [`ChainTipState::next_block_height`]: linera_chain::ChainTipState::next_block_height
85/// [`DeterministicExecution`]: linera_chain::manager::proof::model::DeterministicExecution
86pub trait BlockOutputsArePersisted: CommittedBlock + StorageAtomicity + CorrectValidator {}
87
88/// **Lemma (A certified block and its dependencies are retrievable).** Once a block is a
89/// [`CommittedBlock`], any node that can reach a quorum can obtain the certificate, the ancestors
90/// it needs, and every blob and event the block requires, and can then execute it.
91///
92/// This is what a *uniform* agreement statement needs beyond agreement itself: not merely that
93/// correct validators do not disagree, but that a node which took no part in the decision — one
94/// that was crashed throughout, or joined afterwards — reaches the same state.
95///
96/// *Proof.* The dependencies are of three kinds, each retrievable from any validator holding the
97/// block.
98///
99/// * *The certificate and its ancestors.* `Client::download_certificates` fetches from the
100///   validator set up to a target height, and
101///   `receive_certificate_with_checked_signatures` re-verifies before applying, so retrieval
102///   requires trusting no individual source.
103/// * *Blobs.* `Client::update_local_node_with_blobs_from` fetches by [`BlobId`] across validators,
104///   hedged. Content addressing supplies *integrity* for free — a wrong blob is detectable by
105///   hashing, so one honest source suffices — but says nothing about *availability*, which is the
106///   property actually needed here and which rests on [`BlobRetention`].
107/// * *Events.* Read across chains as `OracleResponse::Event`, so they are recorded in the block
108///   itself; a validator missing the *publishing* chain's state answers `EventsNotFound`, and the
109///   updater's response is to push the admin chain (`update_admin_chain`) or the publishing
110///   chain's certificates.
111///
112/// Each is served by every validator that has processed the block, and a quorum has by definition
113/// voted for it, so under [`CorrectValidatorAvailability`] and [`EventualSynchrony`] a reachable
114/// quorum yields all three. ∎
115///
116/// **What this does not bound.** *That* the dependencies are retrievable does not say how long
117/// retrieval takes; see [`BoundedCatchUp`].
118///
119/// [`CommittedBlock`]: linera_chain::manager::proof::commit::CommittedBlock
120/// [`BlobId`]: linera_base::identifiers::BlobId
121/// [`BlobRetention`]: super::assumptions::BlobRetention
122pub trait CertifiedBlockIsAvailable:
123    BlockOutputsArePersisted + CorrectValidatorAvailability + EventualSynchrony + BlobRetention
124{
125}
126
127/// **Caveat (Catch-up is not time-bounded).** The work a node must do to reach a chain's tip is
128/// proportional to the number of blocks it must replay, which is the height above the chain's
129/// latest checkpoint — and nothing in the protocol bounds that distance.
130///
131/// The mechanism to bound it exists. A block whose sole transaction is
132/// `SystemOperation::Checkpoint` publishes the chain's execution state as a blob;
133/// `Client::bootstrap_chain_from_checkpoint` installs it and resumes downloading from that height,
134/// so the blocks below are never replayed. `ChainWorkerState::reset_and_reexecute_chain` uses the
135/// same shortcut, replaying only from `latest_checkpoint_height`.
136///
137/// **Nothing schedules it.** `ChainClient::checkpoint` is invoked from one place in the workspace,
138/// the `linera` CLI. There is no policy, no interval, and no protocol rule requiring a chain to
139/// checkpoint — so on a chain that never does, catch-up is linear in the chain's whole history and
140/// [`BoundedRecovery`] cannot be discharged for a node that has fallen far behind.
141///
142/// This is the sharp edge of an otherwise-clean property: [`CertifiedBlockIsAvailable`] says a
143/// recovering or joining node *can* reach the tip; making that *quick* is a deployment obligation
144/// resting on checkpoint frequency that the protocol does not enforce.
145///
146/// [`BoundedRecovery`]: super::assumptions::BoundedRecovery
147pub trait BoundedCatchUp: CertifiedBlockIsAvailable + BoundedRecovery {}
148
149/// **Lemma (A locked block's blobs travel with the lock).** Whenever a correct validator holds a
150/// locking block, it also holds the blobs that block requires, and any node that can reach it can
151/// obtain them.
152///
153/// This is what makes [`LockRecovery`] executable rather than merely permitted. Re-proposing a
154/// locked block means proposing the block itself, which cannot be done without the blobs it
155/// publishes and reads — and the party that has to do it is often not the party that created the
156/// lock.
157///
158/// *Proof.* Three parts.
159///
160/// *The validator keeps them.* [`ChainManager`] writes the required blobs into
161/// [`locking_blobs`] in the same transition that installs the lock, clearing the map first, so it
162/// always describes the current lock and never an earlier one. [`ManagerSafetySnapshot`] carries
163/// `locking_blobs` alongside the lock, so a restore cannot reinstate a lock without its blobs.
164///
165/// *They are reachable.* `ValidatorNode::download_pending_blob` resolves through
166/// [`ChainManager::pending_blob`], which consults the proposer's blobs and then `locking_blobs`.
167/// So a lock held by a reachable correct validator is a lock whose blobs are downloadable, without
168/// the original proposer being involved at all.
169///
170/// *A recovering client collects them.* In `Client::synchronize_chain_state`, installing a
171/// [`ValidatedBlockCertificate`] on the local node fails with `LocalNodeError::BlobsNotFound` when
172/// they are absent; each missing blob is then downloaded from the remote node, installed with
173/// `handle_pending_blobs`, and the certificate retried. The proposal path does the same for a fast
174/// lock. ∎
175///
176/// **Re-proposal itself does not fetch.** `ChainClient` reads the blobs for a re-proposal from its
177/// own local node, and fails with an internal error rather than a retry if they are not there. The
178/// fetching happens only in the synchronization above, so this lemma is a statement about that
179/// path having run, not about a fallback at proposal time.
180///
181/// [`LockRecovery`]: super::progress::LockRecovery
182/// [`ChainManager`]: linera_chain::manager::ChainManager
183/// [`ChainManager::pending_blob`]: linera_chain::manager::ChainManager::pending_blob
184/// [`locking_blobs`]: linera_chain::manager::ChainManager::locking_blobs
185/// [`ManagerSafetySnapshot`]: linera_chain::manager::ManagerSafetySnapshot
186/// [`ValidatedBlockCertificate`]: linera_chain::types::ValidatedBlockCertificate
187pub trait LockingBlobsTravelWithTheLock: CorrectValidator + CorrectValidatorAvailability {}
188
189/// **Lemma (A client can obtain everything a submission depends on).** A client can always find, on
190/// the network, the data it needs in order to submit
191///
192/// 1. a **valid block proposal** — one every correct validator will accept — or
193/// 2. a **confirmed certificate**, to a validator that does not yet have it;
194///
195/// and having found it, it can hand that data to any validator that is missing it.
196///
197/// The two halves are *discovery* and *supply*, and they are separate claims. Discovery is what a
198/// client does before it has a block at all; supply is what happens when a validator turns out to
199/// be behind. The second is easy once the first has happened, because building the proposal is what
200/// puts the data in the client's own storage.
201///
202/// # What a valid proposal requires
203///
204/// Validity is not one condition, so the dependencies are not one kind. `try_handle_block_proposal`
205/// checks the following in order, and each check is a demand on data the validator must already
206/// hold. The right-hand column is how a client comes to hold it too.
207///
208/// | the proposal is valid only if | the validator needs | error when it lacks it | how a client finds it |
209/// |---|---|---|---|
210/// | the chain exists at all | the chain description blob | `InactiveChain` | from the creating chain's block |
211/// | its height and parent hash continue the chain (`verify_block_chaining`) | that chain's certified prefix | `UnexpectedBlockHeight` | `ChainClient::synchronize_chain_state` |
212/// | its round is one this validator can accept ([`ProposalGate`]) | the consensus state at that height | `WrongRound` | the same, plus `ChainClient::prepare_chain` |
213/// | it declares the chain's current epoch (`check_block_epoch`) | the committee for that epoch, hence the admin chain's epoch event | `EventsNotFound` on the epoch stream | by following the admin chain, which every client does |
214/// | every incoming bundle it consumes is present in the inbox, equal, and in cursor order | the *sending* chains' message-bearing blocks, already delivered into that inbox | `MissingCrossChainUpdates` | `ChainClient::find_received_certificates` |
215/// | its transactions execute correctly | every blob the block publishes or reads, and every event it reads | `BlobsNotFound`, `EventsNotFound` | `download_blob` / `download_pending_blob`, and `Client::sync_events_from_node` |
216///
217/// The inbox row is the one that needs care, because "valid" there means more than "the bundle
218/// exists". `remove_bundles_from_inboxes` runs with `must_be_present = true`, so the bundle must
219/// already be in *that validator's* inbox and equal to what it holds
220/// ([`IncomingBundlesMatchTheLocalInbox`]); and consumption must respect cursor order, skipping only
221/// bundles every message of which is skippable ([`DeliveryAndConsumptionAreOrdered`]). A client
222/// therefore cannot make a proposal valid by supplying a bundle in isolation: it supplies the
223/// sending chain's blocks, and the validator derives the inbox from them itself.
224///
225/// # Discovery
226///
227/// Each row's last column is a request to the network, not a lookup in something the client is
228/// assumed to have. The client learns of incoming messages by asking validators for their received
229/// logs (`find_received_certificates`), of blobs by downloading them, of events by
230/// `sync_events_from_node`, and of its own chain's state by synchronizing it.
231///
232/// *This half is quorum-dependent, and the code says so.* `find_received_certificates` is
233/// documented as best effort: it finds only certificates confirmed among sufficiently many
234/// validators of the sending chain's *current* committee — which holds "whenever a sender's chain
235/// is still in use and is regularly upgraded to new committees". A message from a chain that has
236/// since gone quiet across a reconfiguration is the case that is not covered, which is the
237/// availability question in [`super::assumptions::BlobRetention`]'s family rather than a defect in
238/// the supply argument below.
239///
240/// # Supply
241///
242/// *Every dependency is by then available at the requester, locally.* Building the proposal means
243/// the client's own local node executed the block, and execution consumes exactly the data in the
244/// table; so holding it is a precondition of having a proposal to submit, not a coincidence.
245/// `linera_core::updater` states the messaging case as an invariant of local storage: it is
246/// "guaranteed to hold every block we needed to build a proposal", because a bundle can only be
247/// consumed after its ordered message-bearing predecessors were downloaded.
248///
249/// For case (2) the argument is shorter: the block is certified, so its dependencies are
250/// retrievable at all ([`CertifiedBlockIsAvailable`]), and a client that processed the certificate
251/// wrote them as it went ([`BlockOutputsArePersisted`]). The blob arm of `send_confirmed_certificate`
252/// says so outright — "the certificate is confirmed, so the blobs must be in storage" — and treats
253/// a miss as an error rather than something to wait for.
254///
255/// This is a *local* availability claim, and it is stronger than the network-wide one:
256/// [`CertifiedBlockIsAvailable`] says the data can be obtained from some quorum, whereas here it is
257/// already in the hand of the party that must supply it. That is why the pushes read local storage
258/// and nothing else — `read_certificates_for_heights`, `read_blobs_from_storage`,
259/// `get_next_height_to_preprocess` — and why no class waits on a third party. ∎
260///
261/// The one case where local availability is not immediate is a lock the client is *recovering*
262/// rather than one it created: there the blobs were collected during synchronization, by
263/// [`LockingBlobsTravelWithTheLock`].
264///
265/// # Two things this makes possible
266///
267/// **A client need not follow a whole chain to supply what came from it.** A chain it merely
268/// receives from is stored only at its message-bearing heights, and `send_chain_information` pushes
269/// exactly those, silently skipping heights it does not have; the validator executes the contiguous
270/// prefix and *preprocesses* any block above a gap, which is enough to deliver that block's
271/// bundles. So a sparse chain the client never fully held is still enough to make the inbox row
272/// true at the validator.
273///
274/// **The set to push is derived from local storage, not from the error.**
275/// `MissingCrossChainUpdates` names only the bundles the current proposal needs and omits
276/// already-consumed ancestors the validator must execute first, so deriving the push from it would
277/// be unreliable; `send_chain_information` sends the whole locally-held range instead.
278///
279/// [`ProposalGate`]: linera_chain::manager::proof::voting::ProposalGate
280/// [`DeliveryAndConsumptionAreOrdered`]: super::availability::DeliveryAndConsumptionAreOrdered
281///
282/// **Why the pushes terminate.** Each class carries a well-founded measure.
283/// `send_confirmed_certificate` latches `sent_admin_chain` / `sent_blobs` / `sent_blocks`, so each
284/// class is attempted once. `send_block_proposal` drains its `blob_ids` with `mem::take` and
285/// records `publisher_chain_ids_sent` per publishing chain. `MissingCrossChainUpdates` reports
286/// every missing bundle at once, so it too is a latch: the whole reported set is synced in one
287/// batch, and a validator that still reports missing bundles afterwards is not retried. A
288/// dependency that nobody can supply therefore surfaces as an error rather than looping.
289///
290/// [`LocalNodeLagging`]: crate::client::chain_client::Error::LocalNodeLagging
291/// [`IncomingBundlesMatchTheLocalInbox`]: linera_chain::manager::proof::commit::IncomingBundlesMatchTheLocalInbox
292/// [`ValidationQuorumForms`]: super::progress::ValidationQuorumForms
293pub trait MissingDependenciesAreRecoverable:
294    LockingBlobsTravelWithTheLock + CorrectValidatorAvailability + EventualSynchrony
295{
296}
297
298/// **Lemma (Effects are a function of persisted state).** Everything a chain worker emits to
299/// other chains is derivable from that chain's saved state, and re-emitting it is harmless. So a
300/// worker that crashes between persisting a transition and dispatching its effects loses nothing:
301/// on restart it re-derives them, and the recipients absorb the repeats.
302///
303/// *Proof.* Two halves, one per side of the delivery.
304///
305/// *The sender re-derives.* `ChainWorkerState::create_network_actions` does not read the
306/// transition's result. It calls `reconcile_tracked_outboxes` and then `build_network_actions`,
307/// which builds the pending cross-chain requests from the reconciled **outbox index** — part of
308/// the chain's view, and therefore part of what `save()` wrote. By [`StorageAtomicity`] that view
309/// is consistent after any crash, so the same set of actions is derivable again.
310///
311/// *The recipient absorbs repeats.* A redelivered bundle is filtered out before it reaches the
312/// inbox: `ChainWorkerState::select_message_bundles` drops every bundle whose height is below the
313/// inbox's `next_block_height_to_receive`, logging them as repeated. `Inbox::add_bundle` would not
314/// absorb one in any case — it requires the [`Cursor`] to be at least `next_cursor_to_add` and
315/// rejects anything lower with `InboxError::IncorrectOrder`. Its two reconciliation branches cover
316/// different situations: `removed_bundles` a bundle this chain consumed *by anticipation* before
317/// delivery, and `restored_cursor` a bundle whose effects a checkpoint restore has already baked
318/// into the state. Delivery is therefore at-least-once with idempotent effect. ∎
319///
320/// `reset_and_reexecute_chain` relies on exactly this from the other direction: having wiped and
321/// replayed a chain, it returns a `CrossChainRequest::RevertConfirm` to every known sender,
322/// asking them to re-derive and resend anything the replay dropped from the inbox.
323///
324/// **The crash windows.** [`CorrectValidator`] admits a crash at any point, and the four windows
325/// have different mechanisms — only two are this lemma:
326///
327/// | crash window | what covers it |
328/// |---|---|
329/// | before `save()` | the transition is rolled back and the client retries; needs re-execution to reproduce the outcome |
330/// | after `save()`, before dispatch | this lemma, sender half |
331/// | during `save()` | [`StorageAtomicity`] |
332/// | after dispatch, recipient restarts | this lemma, recipient half |
333///
334/// [`Cursor`]: linera_base::data_types::Cursor
335/// [`StorageAtomicity`]: linera_chain::manager::proof::model::StorageAtomicity
336/// [`CorrectValidator`]: linera_chain::manager::proof::model::CorrectValidator
337pub trait EffectsSurviveRestart:
338    StorageAtomicity + SequentialChainState + CorrectValidator
339{
340}
341
342/// **Lemma (An inbox holds only bundles its origin really sent).** Every [`MessageBundle`] in a
343/// correct validator's inbox for an origin was produced by a block of that origin which the *same
344/// validator* has processed.
345///
346/// *Proof.* Bundles enter an inbox at exactly one place, `Inbox::add_bundle`, reached only from
347/// `ChainWorkerState::process_cross_chain_update`. That handler serves a
348/// `CrossChainRequest::UpdateRecipient`, and cross-chain requests are internal to one validator:
349/// `linera_rpc` routes each to the shard owning the target chain, so the request comes from
350/// another worker of the same validator, which built it in `build_network_actions` from its own
351/// persisted outbox for a block it had processed ([`EffectsSurviveRestart`], sender half). No
352/// other validator's word enters, and by [`SequentialChainState`] no other process writes this
353/// chain's inboxes. `select_message_bundles` additionally drops bundles whose epoch has been
354/// revoked, unless they were already anticipated. ∎
355///
356/// This is the premise [`IncomingBundlesMatchTheLocalInbox`] leaves open: that lemma proves a voter
357/// matches consumed bundles against its own inbox, which is worth exactly as much as the inbox's
358/// own provenance.
359///
360/// [`MessageBundle`]: linera_chain::data_types::MessageBundle
361pub trait InboxHoldsOnlySentBundles: CorrectValidator + SequentialChainState {}
362
363/// **Lemma (A bundle is consumed at most once).** No two blocks of a chain consume the same
364/// [`MessageBundle`] from the same origin, even though delivery is at-least-once.
365///
366/// Together with [`EffectsSurviveRestart`]'s at-least-once delivery this is the exactly-once
367/// property for *consumption*. It is not exactly-once *delivery*: the same bundle may arrive any
368/// number of times, and nothing here says it arrives at all.
369///
370/// *Proof.* Four filters, one per way a repeat can present itself:
371///
372/// * *Redelivery.* `select_message_bundles` drops bundles below the inbox's
373///   `next_block_height_to_receive`, which has advanced past every height already received.
374/// * *Order.* Should one slip through, `Inbox::add_bundle` requires the [`Cursor`] to be at least
375///   `next_cursor_to_add` — set to the previous cursor plus one on every successful add — and
376///   fails with `InboxError::IncorrectOrder` otherwise.
377/// * *Anticipation.* A bundle consumed before it arrived sits in `removed_bundles`; on arrival it
378///   is matched by cursor, checked for equality and deleted rather than queued, so it is never
379///   offered for consumption a second time.
380/// * *Checkpoint restore.* A bundle below `restored_cursor` is dropped, its effects being already
381///   part of the restored state.
382///
383/// Consumption itself removes the bundle: `remove_bundles_from_inboxes` pops it from
384/// `added_bundles`, and by [`IncomingBundlesMatchTheLocalInbox`] a correct validator does not vote for
385/// a block consuming a bundle that is not there. ∎
386///
387/// **Scoped to one validator.** Every clause above is about one validator's own inboxes. That all
388/// correct validators consume the same bundles in the same blocks follows from agreement on the
389/// block sequence ([`UniqueChain`]), not from anything here.
390///
391/// [`MessageBundle`]: linera_chain::data_types::MessageBundle
392/// [`Cursor`]: linera_base::data_types::Cursor
393/// [`UniqueChain`]: linera_chain::manager::proof::safety::UniqueChain
394pub trait BundleConsumedAtMostOnce:
395    InboxHoldsOnlySentBundles + EffectsSurviveRestart + IncomingBundlesMatchTheLocalInbox
396{
397}
398
399/// **Lemma (Unpaid blob storage is bounded).** A validator's storage of blobs that no certified
400/// block references is bounded, so pushing data at a validator buys neither storage nor
401/// availability.
402///
403/// This is the companion to [`BlobRetention`]. Retention says a *certified* block's blobs stay
404/// available; without a matching bound on *uncertified* ones, "blobs are available" would be an
405/// invitation to store anything for free.
406///
407/// *Proof.* Blobs arrive ahead of certification only through
408/// `ChainWorkerState::handle_pending_blob`, which admits one only if it is *expected*: it must
409/// belong to a pending proposal or validated block for this chain, or the call fails with
410/// `WorkerError::UnexpectedBlob`. Admission is then bounded twice over by the committee's
411/// [`ResourceControlPolicy`] — in count, `ensure!(count < policy.maximum_published_blobs)` with
412/// `WorkerError::TooManyPublishedBlobs`, and in size, by `check_blob_size` against
413/// `maximum_blob_size`. The staging areas themselves are per-chain view state
414/// (`pending_proposed_blobs`, `pending_validated_blobs`) and are cleared when the chain manager is
415/// reset for the next height.
416///
417/// Publication that does survive is charged: the policy prices it at `blob_published` per blob and
418/// `blob_byte_published` per byte, paid by the block that publishes it. ∎
419///
420/// **The economic side is out of scope.** That those prices *cover* the cost of keeping a blob for
421/// as long as [`BlobRetention`] requires is an economic question this specification does not
422/// address; it treats the fee schedule as given.
423///
424/// [`ResourceControlPolicy`]: linera_execution::ResourceControlPolicy
425/// [`BlobRetention`]: super::assumptions::BlobRetention
426pub trait BlobAdmissionIsBounded: CorrectValidator {}
427
428/// **Lemma (Bundles are delivered and consumed in order).** For a given (sender, recipient) pair,
429/// bundles enter an inbox in strictly increasing [`Cursor`] order and are consumed in strictly
430/// increasing cursor order. A bundle may be passed over only if every message in it is *skippable*.
431///
432/// *Proof.* Three guards, one per way order could break.
433///
434/// *Within a batch.* `ChainWorkerState::select_message_bundles` walks the incoming bundles and
435/// rejects the request with `WorkerError::InvalidCrossChainRequest` unless their heights are
436/// non-decreasing, so a batch is already ordered when it reaches the inbox.
437///
438/// *Across batches, on delivery.* `Inbox::add_bundle` requires `cursor >= next_cursor_to_add` and
439/// fails with `InboxError::IncorrectOrder` otherwise, then sets `next_cursor_to_add` to
440/// `cursor + 1`. Delivery positions are therefore strictly increasing.
441///
442/// *On consumption.* `Inbox::remove_bundle` requires `cursor >= next_cursor_to_remove` on the same
443/// terms. Before consuming, it drains queued bundles below that cursor — and each one must satisfy
444/// `is_skippable()`, or the block is rejected with `InboxError::UnskippableBundle`. ∎
445///
446/// **What "skippable" excludes is the point.** `PostedMessage::is_skippable` is false for
447/// `MessageKind::Protected` and `MessageKind::Tracked` unconditionally, and false for `Simple` or
448/// `Bouncing` messages carrying a non-zero grant. So a recipient may leave ordinary zero-grant
449/// messages unconsumed, and may not silently drop a protected or tracked one, or one carrying funds:
450/// consuming a later bundle forces it to account for those first. Ordering here is a *safety*
451/// property — it constrains which blocks are valid — not a delivery guarantee.
452///
453/// [`Cursor`]: linera_base::data_types::Cursor
454pub trait DeliveryAndConsumptionAreOrdered: InboxHoldsOnlySentBundles {}
455
456/// **Lemma (Delivery is repaired on demand, not guaranteed by the sender).** A validator makes a
457/// bounded effort to deliver a bundle to its own worker for the recipient chain, and no more. What
458/// makes delivery dependable is that a client needing the bundle can always cause it: the recipient
459/// side of [`MissingDependenciesAreRecoverable`] is a repair path, not merely a diagnosis.
460///
461/// *Proof.* Two halves.
462///
463/// *The sender's effort is bounded.* Outgoing cross-chain requests are handed to a bounded channel
464/// with `try_send`, which drops on overflow, and `forward_cross_chain_queries` abandons a request
465/// once `retries >= cross_chain_max_retries`. The persistent outbox keeps the entry, and
466/// `ChainWorkerState::create_network_actions` re-derives *all* pending requests from it — but every
467/// call site is a request handler for the sending chain, so nothing re-emits while that chain is
468/// idle. Delivery is therefore best effort in the same sense the notification channel is
469/// ([`NotificationChannelIsLossy`]), and for the same reason: no retry outlives the process that
470/// scheduled it.
471///
472/// *The recipient can force it.* A client proposing a block that consumes the bundle is told
473/// `MissingCrossChainUpdate` by any validator lacking it, and `send_block_proposal` answers by
474/// sending that validator the *sending chain's* certificates, which re-derives the delivery there.
475/// `CrossChainMessageDelivery::Blocking` lets a client wait for delivery on an ordinary
476/// `send_chain_information` request rather than guess. Both are per-validator and on demand. ∎
477///
478/// **So the guarantee is conditional on someone wanting the message.** A recipient that is not
479/// actively proposing gets no assurance its inbox is complete, and two correct validators can differ
480/// on an inbox indefinitely — the sender-side asymmetry that
481/// `super::storage::StorageConvergesAtEqualHeights` needs quiescence to rule out. Whether an idle
482/// sending chain should retry on its own is
483/// [issue #6799](https://github.com/linera-io/linera-protocol/issues/6799).
484///
485/// [`NotificationChannelIsLossy`]: super::notifications::NotificationChannelIsLossy
486pub trait DeliveryIsRepairedOnDemand: DeliveryAndConsumptionAreOrdered {}