linera_core/proof/notifications.rs
1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! What a notification tells a client, and what it does not.
5//!
6//! A [`Notification`] tells a client that a chain has changed. From a correct validator it is sound
7//! — what it reports really happened — but from any validator it may simply never arrive, and it
8//! carries nothing that could be checked if it did.
9//!
10//! The channel is **lossy by model**, not merely unreliable in practice, so nothing here may assume
11//! a notification arrives. That would be alarming if clients used notifications merely to go
12//! faster. They do not: a `ChainListener` acts on them, processing inboxes and following new
13//! chains, so an application's own liveness can rest on one arriving.
14//!
15//! What makes a lossy channel tolerable is that the dependence is *self-repairing*, in three ways
16//! that are independent of each other: a client subscribes to every validator, so one silence is
17//! covered by the rest; establishing a stream resynchronizes chain state, so a gap is closed rather
18//! than replayed; and handlers bring a chain up to date rather than applying the change they were
19//! told about, so any later notification does the work of every lost one. All three need the client
20//! to be connected to somebody.
21//!
22//! [`Notification`]: crate::worker::Notification
23
24use linera_chain::manager::proof::model::{CorrectValidator, StorageAtomicity};
25
26use super::availability::{BlockOutputsArePersisted, InboxHoldsOnlySentBundles};
27
28/// **Lemma (A correct validator's notification reports a change that is already persisted).** If a
29/// client receives a [`Notification`] from a correct validator, the state change it names is in
30/// that validator's storage. Nothing is claimed about a faulty validator's notifications, which by
31/// [`CorrectValidator`] may report anything at all.
32///
33/// *Proof.* Notifications are not sent where they are built. Each is pushed onto the
34/// `notifications` field of a [`NetworkActions`] value which the handler *returns*, and every
35/// handler that changes state calls `save()` before returning it — a failed save propagates with
36/// `?`, so the actions are dropped and nothing is dispatched. Site by site:
37///
38/// * `Reason::NewBlock` and `Reason::NewEvents` are pushed in
39/// `ChainWorkerState::execute_contiguous_block` *before* its `self.save()?`, so a save that
40/// fails discards them along with the actions that carry them.
41/// * `Reason::NewEvents` is pushed again in `preprocess_certified_block`, there *after* the save
42/// rather than before it, which reaches the same conclusion more directly.
43/// * `Reason::NewRound` is built inside `create_network_actions` from
44/// `ChainManager::current_round`, and travels out in the same returned value.
45/// * `Reason::NewIncomingBundle` is pushed in `WorkerState::handle_cross_chain_request` only once
46/// `process_cross_chain_update` has returned `CrossChainUpdateResult::Updated`, and that result
47/// crosses a `chain_write` batch, which takes the lock and saves once before replying.
48///
49/// The caller dispatches through `Notifier::notify_chain` on the returned value alone, so no path
50/// reaches a client without a completed save. By [`StorageAtomicity`] that save is all or nothing,
51/// so a client is never told about a half-written change. ∎
52///
53/// **A notification carries no evidence.** [`Notification`] is a `chain_id` and a [`Reason`]: no
54/// signature, no certificate, not even a field naming the sender. Validators are not asked to sign
55/// them.
56///
57/// Knowing *who* sent one is a deployment matter rather than a protocol one. Under
58/// `NetworkProtocol::Grpc(TlsConfig::Tls)` the transport authenticates which validator the stream
59/// came from, so a third party cannot inject notifications into it; `TlsConfig::ClearText` is an
60/// equally valid configuration, so the specification cannot assume even that. And where it does
61/// hold it authenticates the *origin*, never the content, and only to the client holding the
62/// connection — a client cannot show anyone else what a validator told it. A notification is
63/// therefore never evidence in the sense the [accountability results] use: none is convictable, and
64/// none can be forwarded as proof of anything.
65///
66/// So the qualifier above is not a technicality a client can discharge by inspection. A fabricated
67/// notification is indistinguishable *by content* from a sound one, and the only way to learn
68/// whether the change happened is to ask. What a client may take from one is that it is worth
69/// querying now, and never what the answer will be. That the named block is *the* block at that
70/// height, rather than one validator's idea of it, is [`CommitAgreement`]'s and not this lemma's.
71///
72/// [accountability results]: linera_chain::justification::proof
73///
74/// [`Notification`]: crate::worker::Notification
75/// [`NetworkActions`]: crate::worker::NetworkActions
76/// [`Reason`]: crate::worker::Reason
77/// [`CorrectValidator`]: linera_chain::manager::proof::model::CorrectValidator
78/// [`CommitAgreement`]: linera_chain::manager::proof::safety::CommitAgreement
79pub trait NotificationImpliesPersistedChange: CorrectValidator + StorageAtomicity {}
80
81/// **Definition (The notification channel is lossy).** The channel carrying [`Notification`]s from
82/// a validator to a client may drop any message, without notice to either side. Delivery is never
83/// retried, never acknowledged, and never durable.
84///
85/// *Where this sits in the fault model.* Network loss needs no separate treatment — a notification
86/// is a message between participants, so [`EventualSynchrony`] already permits it to be dropped
87/// before GST and forbids that after. What this definition adds are three *local* ways to lose one,
88/// which the network model does not describe: `Notifier::notify_chain` returns immediately when the
89/// chain has no entry in its sender map; a failed `sender.send` is ignored and the dead sender
90/// reaped; and [`NotificationImpliesPersistedChange`] orders the save before the dispatch, leaving a
91/// window in which a change is persisted and the process dies before anything is sent.
92///
93/// The third behaves like a crash and is modelled as one: [`CorrectValidator`] permits a crash at
94/// any time, freely before GST and with bounded recovery after. So no result may assume a
95/// *particular* notification arrives, and a result may assume that a client which stays connected
96/// to a reachable correct validator eventually learns.
97///
98/// **The model has no notion of a partial crash, and this is where one would be needed.** A
99/// validator whose notification dispatch stops while its consensus path keeps running is not
100/// described by anything here: it is not faulty, since it signs nothing wrong; it is not crashed,
101/// since the process is alive and answering; and it is not merely "slow or unreachable", the escape
102/// [`CorrectValidator`] does allow, because it responds normally to everything except this. The
103/// crash model is whole-process — a validator stops and restarts, losing unpersisted state — with
104/// nothing between up and down. Component-level failure inside a live validator is outside the
105/// model, and [`LostNotificationsAreRepaired`] shows it is not a harmless omission: it defeats two
106/// of the three mechanisms that would otherwise repair a loss.
107///
108/// This is why the alternative statement — "no result depends on notification delivery" — is not
109/// worth making. In a model where the channel is lossy it cannot fail to hold; it is a property of
110/// the specification rather than of the system.
111///
112/// What repairs an individual loss is [`LostNotificationsAreRepaired`].
113///
114/// [`Notification`]: crate::worker::Notification
115/// [`EventualSynchrony`]: super::assumptions::EventualSynchrony
116pub trait NotificationChannelIsLossy {}
117
118/// **Lemma (A lost notification is repaired).** A client that misses a notification still reaches
119/// the state it would have reached, provided it remains connected to at least one correct validator
120/// that has the change. Three independent mechanisms do this, and none of them replays the lost
121/// message.
122///
123/// *Redundancy.* `ChainClient::listen` opens one subscription per validator in the committee, via
124/// `update_notification_streams`, and processes them concurrently. Every validator that processes a
125/// change notifies about it, so a client loses a notification only when *every* validator it is
126/// subscribed to fails to deliver that one — not when any single one does.
127///
128/// *Resynchronization on (re)subscribe.* Establishing a stream is not just an attachment point: the
129/// same future calls `Client::synchronize_chain_state_from` against that validator before yielding
130/// the stream, precisely because, in the code's own words, "we may have missed notifications since
131/// the last time we synchronized". Since `update_notification_streams` is re-run on every
132/// `Reason::NewBlock`, and a dropped connection is re-established through the same path, a gap in
133/// the stream is closed by *state synchronization* rather than by recovering the messages that fell
134/// in it.
135///
136/// *Coalescing.* No handler applies the change it was told about; each brings the chain up to date.
137/// In `linera_client::chain_listener`, `Reason::NewIncomingBundle` and `Reason::NewEvents` both
138/// reduce to `maybe_notify_inbox_processing`, whose waiting loop runs
139/// `ChainClient::process_inbox_without_prepare` — draining *everything* pending, not the one bundle
140/// named. `Reason::NewBlock` calls `update_wallet` and re-derives event subscriptions. Handlers are
141/// therefore idempotent and depend only on current state, so one run after `k` notifications
142/// achieves what `k` runs would. `Notify::notify_one` stores a permit when no task is waiting, so a
143/// notification arriving *during* a pass starts another rather than being swallowed. ∎
144///
145/// **What is not repaired.** Every mechanism above assumes a failure that is either whole-process or
146/// network-level, because those are the failures the model has. A validator that is up, answering
147/// RPCs and voting normally, but silently delivering no notifications — the partial crash
148/// [`NotificationChannelIsLossy`] describes — defeats two of the three. Redundancy survives only if
149/// such a failure is independent across validators, which a systematic one is not. Resynchronization
150/// never runs, because it is triggered by establishing a stream and no stream ever drops: the client
151/// holds a healthy connection to a validator that has stopped talking.
152///
153/// Coalescing is then irrelevant, since nothing arrives to coalesce. The client is not told it is
154/// learning nothing, and nothing escalates: `ChainListener::next_action` selects over the
155/// notification streams, the cancellation token and its command channel, and there is no timer among
156/// them. Pending bundles stay unprocessed and subscribed events unread for as long as the condition
157/// lasts, which — being outside the fault model — is not bounded by GST or by anything else the
158/// specification states.
159pub trait LostNotificationsAreRepaired: NotificationChannelIsLossy {}
160
161/// **Lemma (A notification is backed by a certificate the validator can serve — except for a new
162/// round).** For every notification a correct validator emits other than `Reason::NewRound`, that
163/// validator holds a quorum-signed certificate establishing what the notification reports, and
164/// will hand it to anyone who asks. A recipient can therefore not merely learn that something
165/// happened but verify it, and carry the evidence to other validators.
166///
167/// *Proof.* Site by site, for the reasons a validator emits.
168///
169/// * `Reason::NewBlock` and `Reason::NewEvents` are reachable only through
170/// `ChainWorkerState::process_confirmed_block`, which verifies the [`ConfirmedBlockCertificate`]
171/// with `certificate.check` and writes it with `write_blobs_and_certificate` *before* dispatching
172/// to the path that emits them ([`BlockOutputsArePersisted`]). The certificate is in storage
173/// before the notification exists.
174/// * `Reason::NewIncomingBundle` is emitted once `process_cross_chain_update` reports
175/// `CrossChainUpdateResult::Updated`. By [`InboxHoldsOnlySentBundles`] that bundle reached the
176/// inbox from another worker of the *same validator*, built from its persisted outbox for a block
177/// that validator had processed — so the sending block's certificate is in this validator's
178/// storage too. Note it certifies a block of the *sending* chain, not of the chain the
179/// notification names.
180///
181/// Serving them is the ordinary node surface: `download_certificate`, `download_certificates` and
182/// `download_certificates_by_heights`. A node that receives one of these notifications can fetch
183/// the certificate, verify it against the committee for its epoch, and push it onward —
184/// `send_confirmed_certificate`
185/// is exactly that path. ∎
186///
187/// This is the precise sense in which a notification is worth acting on despite carrying no
188/// evidence itself. The message is unsigned and, at best, authenticated only to its recipient by
189/// the transport; what it points at is quorum-signed and transferable to anyone. The hint is
190/// non-transferable, the thing it hints at is not.
191///
192/// **`Reason::NewRound` has no such backing, and cannot.** `ChainManager::update_current_round`
193/// takes a maximum over four inputs, and only two are certificates: a [`TimeoutCertificate`], or a
194/// locking block, which is a [`ValidatedBlockCertificate`]. The other two are `proposed` and
195/// `signed_proposal` — one owner's signature, not a quorum's. A round raised by a proposal in a
196/// higher multi-leader round therefore has nothing portable behind it, which is
197/// [`MultiLeaderRoundsAreLocal`] seen from the notification side: a recipient that wants to reach
198/// that round must be sent the proposal itself, because no compact proof of it exists to send.
199///
200/// **`Reason::BlockExecuted` is out of scope here.** It is emitted by `linera_core::client`, not by
201/// a validator, so it is a client telling itself something rather than a claim one node makes to
202/// another.
203///
204/// [`ConfirmedBlockCertificate`]: linera_chain::types::ConfirmedBlockCertificate
205/// [`ValidatedBlockCertificate`]: linera_chain::types::ValidatedBlockCertificate
206/// [`TimeoutCertificate`]: linera_chain::types::TimeoutCertificate
207/// [`MultiLeaderRoundsAreLocal`]: linera_chain::manager::proof::timeouts::MultiLeaderRoundsAreLocal
208pub trait NotificationIsCertificateBacked:
209 NotificationImpliesPersistedChange + BlockOutputsArePersisted + InboxHoldsOnlySentBundles
210{
211}