linera_chain/manager/proof/commit.rs
1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The commit rule: what it takes for a block to become final, and what a node does about it.
5//!
6//! The protocol has a two-step commit rule outside the fast round — validate, then confirm — and
7//! a one-step rule inside it. This module pins down both, and connects "a confirmed block
8//! certificate exists" to the observable state a node reaches.
9
10use crate::{
11 data_types::proof::quorum::CertificateCarriesCorrectVote,
12 manager::proof::{
13 model::MaxByzantineWeight,
14 voting::{ConfirmationNeedsValidatedCertificate, ProposalGate},
15 },
16};
17
18/// **Definition (Committed block).** A block `B` at height `h` of a chain is *committed* when a
19/// [`ConfirmedBlockCertificate`] for `B`, valid for the committee of its epoch, exists — that
20/// is, when a quorum has cast confirmation votes for `B`.
21///
22/// The commit rule is therefore:
23///
24/// | round | rule |
25/// |---|---|
26/// | [`Round::Fast`] | a quorum of confirmation votes on a super owner's fast proposal, with no validation step |
27/// | any other round `r` | a quorum of validation votes for `B` in round `r`, then a quorum of confirmation votes for `B` in round `r` |
28///
29/// Commitment is a property of the *world*, not of any one node: a block can be committed before
30/// any particular correct validator learns of it. The node-local reflection of it is
31/// [`TipAdvancesOnlyOnValidCertificate`].
32///
33/// [`ConfirmedBlockCertificate`]: crate::types::ConfirmedBlockCertificate
34/// [`Round::Fast`]: linera_base::data_types::Round::Fast
35pub trait CommittedBlock {}
36
37/// **Lemma (A commit outside the fast round rests on a validated block certificate).** If a
38/// [`ConfirmedBlockCertificate`] for `B` is certified in a round `r` other than
39/// [`Round::Fast`](linera_base::data_types::Round::Fast), then a valid
40/// [`ValidatedBlockCertificate`] for `B` in the *same* round `r` exists.
41///
42/// *Proof.* By [`CertificateCarriesCorrectVote`] some correct validator cast a confirmation vote
43/// for `B` in round `r`. By [`ConfirmationNeedsValidatedCertificate`], since `r` is not the fast
44/// round, a valid [`ValidatedBlockCertificate`] for `B` in round `r` existed when it did so. ∎
45///
46/// This is what lets the safety argument reason exclusively about *validated* certificates:
47/// every commit above the fast round is backed by one, in its own round.
48///
49/// [`ConfirmedBlockCertificate`]: crate::types::ConfirmedBlockCertificate
50/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
51pub trait CommitRestsOnValidation:
52 CertificateCarriesCorrectVote + ConfirmationNeedsValidatedCertificate
53{
54}
55
56/// **Lemma (The tip advances only on a verified certificate).** A correct validator's
57/// [`ChainTipState::next_block_height`] passes from `h` to `h + 1`, and its
58/// [`block_hashes`](crate::ChainStateView) records a hash at `h`, only for a block carried by a
59/// [`ConfirmedBlockCertificate`] that has passed [`check`] against the committee of the block's
60/// epoch.
61///
62/// *Proof.* The tip register is advanced in one place in `linera_chain::chain`, at the end of
63/// [`ChainStateView::apply_confirmed_block`] (`tip.next_block_height.try_add_assign_one()`).
64/// That method has a single call site in the workspace outside tests, in
65/// `ChainWorkerState::execute_contiguous_block`; `execute_block_with_checkpoint_restore` reaches
66/// it by delegating there after installing the snapshot. Both are reached only through
67/// `ChainWorkerState::process_confirmed_block`, whose every non-early-return path first
68/// evaluates `certificate.check(&committee)?` with `committee` fetched for `block.header.epoch`.
69/// The early returns — the `tip.next_block_height > height` skip and the `Preprocess` dispatch —
70/// do not advance the tip. ∎
71///
72/// **Where this is fragile.** [`ChainStateView::apply_confirmed_block`] is `pub`, on a type this
73/// crate re-exports, and it takes a [`ConfirmedBlock`] and no [`Committee`] — so it cannot verify
74/// anything, and nothing about its signature confines it to verified callers. The enumeration
75/// above holds by current usage, not by visibility. A new caller must perform the certificate
76/// check itself.
77///
78/// Two paths deserve explicit mention because they weaken the *precondition* without weakening
79/// this lemma. `pre_checkpoint_block_trust` lets a hash recorded by an earlier checkpoint
80/// certificate bypass the already-processed skip; and a checkpoint block may install a state
81/// snapshot rather than replay its ancestors. Neither bypasses `certificate.check`, so a block
82/// entering the tip is always quorum-certified; what they bypass is the *re-execution* of
83/// ancestors, which is a matter of state-transition correctness rather than of agreement.
84///
85/// [`ChainTipState::next_block_height`]: crate::ChainTipState::next_block_height
86/// [`ConfirmedBlockCertificate`]: crate::types::ConfirmedBlockCertificate
87/// [`ConfirmedBlock`]: crate::block::ConfirmedBlock
88/// [`ChainStateView::apply_confirmed_block`]: crate::ChainStateView::apply_confirmed_block
89/// [`Committee`]: linera_execution::committee::Committee
90/// [`check`]: crate::types::ConfirmedBlockCertificate::check
91pub trait TipAdvancesOnlyOnValidCertificate {}
92
93/// **Lemma (Every certified block was executed by a correct validator).** If a valid
94/// [`ValidatedBlockCertificate`] for a block `B` exists, then some correct validator executed
95/// `B`'s [`ProposedBlock`] itself and obtained `B`'s [`BlockExecutionOutcome`]. The same follows
96/// for a [`ConfirmedBlockCertificate`] outside the fast round, by
97/// [`CommitRestsOnValidation`]; inside the fast round it holds directly, a fast proposal carrying
98/// no outcome.
99///
100/// This is what stands between the protocol and a committed block whose outcome is fabricated.
101/// It is *not* accountability: a validator that votes for a mis-executed block leaves no
102/// extractable proof (see [`AccountabilityScope`]). And unlike the results in
103/// [`crate::justification::proof`], it needs [`MaxByzantineWeight`] — so validity, unlike
104/// agreement, degrades above the fault bound with no forensic residue.
105///
106/// *Proof.* Induction on the certificate's round, well founded because rounds are totally
107/// ordered. By [`CertificateCarriesCorrectVote`] some correct validator `v` cast a validation vote
108/// for `B` in that round, so by [`ProposalGate`] it ran `ChainWorkerState::try_handle_block_proposal`
109/// to acceptance on a proposal for `B`. That function computes the block as
110///
111/// ```text
112/// let block = if let Some(outcome) = outcome { outcome.clone().with(proposal.content.block.clone()) }
113/// else { self.execute_block(…).await? };
114/// ```
115///
116/// and [`BlockProposal::check_invariants`] admits a carried `outcome` only together with an
117/// [`OriginalProposal::Regular`] certificate. So:
118///
119/// * a **fresh** proposal and a **fast retry** both carry `outcome: None` and are therefore
120/// executed by `v` itself — the base case;
121/// * a **regular retry** is not re-executed, but `check_invariants` requires its certificate to
122/// satisfy `certificate.check_value(&ValidatedBlock::new(outcome.with(block)))`, i.e. to certify
123/// exactly this `B`, and `content.round > certificate.round`; the caller verified it with
124/// `certificate.check(&committee)`. The induction hypothesis at that strictly lower round
125/// supplies the correct validator that executed `B`. ∎
126///
127/// **All eight outputs, not just the state.** [`BlockHeader`] commits to each component of the
128/// outcome separately — [`state_hash`], [`messages_hash`], [`events_hash`], [`blobs_hash`],
129/// [`oracle_responses_hash`], [`operation_results_hash`], [`previous_message_blocks_hash`] and
130/// [`previous_event_blocks_hash`] — and this lemma covers all of them, since the correct validator
131/// computed the whole [`BlockExecutionOutcome`]. That matters because the components differ
132/// sharply in reach: `state_hash` is local to the chain, whereas `messages` and `events` leave it
133/// and are consumed by other chains, and `blobs` are content-addressed
134/// ([`BlobId`](linera_base::identifiers::BlobId) is a hash of the content) and so are the only
135/// component that is self-verifying without any execution at all.
136///
137/// **What this does not give.** The correct validator executed the proposal, but the execution
138/// *replays* whatever oracle answers it recorded; a later re-execution of the confirmed block
139/// feeds `outcome.oracle_responses` back in rather than re-deriving them. So the lemma certifies
140/// that the outcome follows from the proposal *and those oracle answers*, not that the answers
141/// were truthful. Oracle results are attested by quorum, which is inherent — they are not
142/// reproducible functions of the chain state.
143///
144/// [`ValidatedBlockCertificate`]: crate::types::ValidatedBlockCertificate
145/// [`ConfirmedBlockCertificate`]: crate::types::ConfirmedBlockCertificate
146/// [`ProposedBlock`]: crate::data_types::ProposedBlock
147/// [`BlockExecutionOutcome`]: crate::data_types::BlockExecutionOutcome
148/// [`BlockProposal::check_invariants`]: crate::data_types::BlockProposal::check_invariants
149/// [`OriginalProposal::Regular`]: crate::data_types::OriginalProposal::Regular
150/// [`BlockHeader`]: crate::block::BlockHeader
151/// [`state_hash`]: crate::block::BlockHeader::state_hash
152/// [`messages_hash`]: crate::block::BlockHeader::messages_hash
153/// [`events_hash`]: crate::block::BlockHeader::events_hash
154/// [`blobs_hash`]: crate::block::BlockHeader::blobs_hash
155/// [`oracle_responses_hash`]: crate::block::BlockHeader::oracle_responses_hash
156/// [`operation_results_hash`]: crate::block::BlockHeader::operation_results_hash
157/// [`previous_message_blocks_hash`]: crate::block::BlockHeader::previous_message_blocks_hash
158/// [`previous_event_blocks_hash`]: crate::block::BlockHeader::previous_event_blocks_hash
159/// [`MaxByzantineWeight`]: crate::manager::proof::model::MaxByzantineWeight
160/// [`AccountabilityScope`]: crate::justification::proof::AccountabilityScope
161pub trait CertifiedBlockWasExecuted:
162 CertificateCarriesCorrectVote + ProposalGate + MaxByzantineWeight + CommitRestsOnValidation
163{
164}
165
166/// **Lemma (Incoming bundles are matched against the validator's own inbox).** A correct
167/// validator casts a validation or fast-confirmation vote for a block only if every
168/// [`IncomingBundle`] the block consumes is already present in its own inbox for that origin, and
169/// is *equal* to the bundle it holds there.
170///
171/// *Code correspondence.*
172///
173/// | | |
174/// |---|---|
175/// | transition | `ChainStateView::remove_bundles_from_inboxes`, called from `ChainWorkerState::try_handle_block_proposal` with `must_be_present = true` |
176/// | reads | the chain's inboxes, the block's timestamp and incoming bundles |
177/// | writes | the inboxes (rolled back before voting — `try_handle_block_proposal` calls `chain.rollback()`) |
178/// | precondition | none beyond [`ProposalGate`] |
179///
180/// *Proof.* `try_handle_block_proposal` calls
181/// `remove_bundles_from_inboxes(block.timestamp, true, block.incoming_bundles())` before executing
182/// and voting. For each bundle that helper calls `Inbox::remove_bundle` and, because
183/// `must_be_present` is set, rejects with [`ChainError::MissingCrossChainUpdates`] unless it
184/// returned `true` — which happens only on the branch that found a bundle already in
185/// `added_bundles` and checked `bundle == &previous_bundle`. So a bundle the validator has not
186/// received, or one that differs in any field from what it received, blocks the vote. ∎
187///
188/// **The flag is deliberately not set when applying a certified block.**
189/// `ChainWorkerState::execute_contiguous_block` passes `must_be_present = false`, so a bundle that
190/// has not arrived yet is recorded in `removed_bundles` and reconciled when it does. That is the
191/// right asymmetry — by then a quorum has already voted, and this lemma has done its work at
192/// voting time — but it means the guarantee lives in the *proposal* path only.
193///
194/// **What populates the inbox decides what this is worth.** Bundles enter through
195/// `ChainWorkerState::process_cross_chain_update`, fed by the *same validator's* worker for the
196/// sending chain. That worker derives them from the sending block's `messages` field, and whether
197/// that field is the validator's own work depends on how it processed the sender:
198///
199/// * if it **executed** the sender's block, `execute_contiguous_block` re-executed it and rejected
200/// a mismatch against the certificate ([`CertifiedBlockWasExecuted`] and the note there), so the
201/// bundles are the validator's own work;
202/// * if it only **preprocessed** the sender's block, `preprocess_certified_block` updated outboxes
203/// and event streams *without executing*, so the bundles are taken from the sender's certificate
204/// at face value.
205///
206/// So cross-chain integrity degrades with how much of the chain graph each validator executes —
207/// a deployment property, not a protocol one. Under [`MaxByzantineWeight`] this is still sound,
208/// since [`CertifiedBlockWasExecuted`] guarantees *some* correct validator executed the sending
209/// block; above the fault bound it is not, and the resulting damage is not confined to one chain
210/// (see [`AccountabilityScope`]).
211///
212/// [`IncomingBundle`]: crate::data_types::IncomingBundle
213/// [`ChainError::MissingCrossChainUpdates`]: crate::ChainError::MissingCrossChainUpdates
214/// [`MaxByzantineWeight`]: crate::manager::proof::model::MaxByzantineWeight
215/// [`AccountabilityScope`]: crate::justification::proof::AccountabilityScope
216pub trait IncomingBundlesMatchTheLocalInbox: ProposalGate + CertifiedBlockWasExecuted {}
217
218/// **Lemma (Event reads resolve against the validator's own storage).** When a correct validator
219/// votes on a proposal that reads an event, the value it votes for is the one in *its own* storage
220/// under the [`EventId`] the block cites — resolved locally, exactly as an oracle call is, and
221/// never taken from the proposer.
222///
223/// This is what stands between a proposer and a block that claims to have read something nobody
224/// published. It is the event analogue of [`IncomingBundlesMatchTheLocalInbox`], but the mechanism is
225/// not the same one, and the difference decides how a discrepancy shows up.
226///
227/// *A bundle is checked; an event is produced.* A [`ProposedBlock`] names the incoming bundles it
228/// consumes, so a validator can compare them against its inbox and reject on mismatch. It carries
229/// no `oracle_responses` — those exist only in [`BlockExecutionOutcome`] — so there is nothing to
230/// compare an event against. The validator simply reads its own storage and records what it finds.
231///
232/// So the two fail differently. A wrong bundle is *rejected*: `remove_bundles_from_inboxes` requires
233/// presence and equality. A wrong event is not rejected at all — the validator computes a different
234/// outcome, hence a different [`Block`], and votes for that instead. The proposer's intended block
235/// simply never gathers a quorum. Both keep a fabricated value out of a certificate; only the first
236/// produces an error anyone can point at.
237///
238/// *Code correspondence.*
239///
240/// | | |
241/// |---|---|
242/// | transition | the event oracle in `ExecutionStateActor`, reached from `ChainWorkerState::execute_block` during `try_handle_block_proposal` |
243/// | reads | the validator's own event storage, through `ExecutionRuntimeContext::get_event` |
244/// | writes | the block's `oracle_responses`, appending `OracleResponse::Event` |
245/// | precondition | none beyond [`ProposalGate`] |
246///
247/// *Proof.* Validating a proposal executes the block afresh, with no recorded outcome to replay
248/// ([`CertifiedBlockWasExecuted`]). `TransactionTracker::oracle` therefore finds no replayed
249/// response and runs its closure, which calls `get_event` on this validator's own storage and
250/// fails with `ExecutionError::EventsNotFound` when the event is absent. The value it records is
251/// the value that came back. So a validator cannot vote for a block whose event it does not hold,
252/// nor for content differing from its own. ∎
253///
254/// **Replay checks the identifier, not the content.** When a block *does* carry a recorded outcome
255/// — a regular retry, or a certified block being applied — `oracle` returns the recorded response
256/// without consulting storage, and `to_event` only checks that the recorded [`EventId`] matches the
257/// one requested, returning `ExecutionError::OracleResponseMismatch` otherwise. The bytes are taken
258/// as given.
259///
260/// That asymmetry matches the one [`IncomingBundlesMatchTheLocalInbox`] records for
261/// `must_be_present = false`, and it is deliberate for the same reason: by the time a block is
262/// certified, a quorum has already voted, and this lemma has done its work at voting time. It is
263/// also why a fabricated `events` field is beyond [`AccountabilityScope`] — the fabrication is
264/// caught by the voters or not at all, and leaves no evidence afterwards.
265///
266/// **What this does not say.** That the event was legitimately published by the chain the
267/// [`EventId`] names is not checked here; it follows from how the event entered storage in the
268/// first place, which is `linera_core::proof::storage::AdmissionChecksTheValidityProof` — events
269/// are written only in the branch guarded by `certificate.check`, so they inherit the publishing
270/// block's certification rather than carrying a proof of their own. Nor does it say the event is
271/// still *readable*: a checkpoint may have pruned it below the stream's floor
272/// ([`EventFloorTracksCheckpoints`]).
273///
274/// [`EventId`]: linera_base::identifiers::EventId
275/// [`ProposedBlock`]: crate::data_types::ProposedBlock
276/// [`BlockExecutionOutcome`]: crate::data_types::BlockExecutionOutcome
277/// [`Block`]: crate::block::Block
278/// [`ProposalGate`]: crate::manager::proof::voting::ProposalGate
279/// [`AccountabilityScope`]: crate::justification::proof::AccountabilityScope
280/// [`EventFloorTracksCheckpoints`]: crate::proof::checkpoints::EventFloorTracksCheckpoints
281pub trait EventReadsResolveLocally: ProposalGate + CertifiedBlockWasExecuted {}