Skip to main content

linera_chain/
inbox.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use allocative::Allocative;
5use linera_base::{
6    data_types::{ArithmeticError, BlockHeight, Cursor},
7    ensure,
8    identifiers::ChainId,
9};
10#[cfg(with_testing)]
11use linera_views::context::MemoryContext;
12use linera_views::{
13    context::Context,
14    queue_view::QueueView,
15    register_view::RegisterView,
16    views::{ClonableView, View},
17    ViewError,
18};
19use thiserror::Error;
20
21use crate::{data_types::MessageBundle, ChainError};
22
23#[cfg(test)]
24#[path = "unit_tests/inbox_tests.rs"]
25mod inbox_tests;
26
27#[cfg(with_metrics)]
28pub(crate) mod metrics {
29    use linera_base::prometheus_util::{exponential_bucket_interval, register_histogram_vec};
30    use prometheus::HistogramVec;
31
32    linera_base::declare_metrics! {
33        pub static INBOX_SIZE: HistogramVec =
34            register_histogram_vec(
35                "inbox_size",
36                "Inbox size",
37                &[],
38                exponential_bucket_interval(1.0, 2_000_000.0),
39            );
40
41        pub static REMOVED_BUNDLES: HistogramVec =
42            register_histogram_vec(
43                "removed_bundles",
44                "Number of bundles removed by anticipation",
45                &[],
46                exponential_bucket_interval(1.0, 10_000.0),
47            );
48    }
49}
50
51/// The state of an inbox.
52/// * An inbox is used to track bundles received and executed locally.
53/// * A `MessageBundle` consists of a logical cursor `(height, index)` and some message
54///   content `messages`.
55/// * On the surface, an inbox looks like a FIFO queue: the main APIs are `add_bundle` and
56///   `remove_bundle`.
57/// * However, bundles can also be removed before they are added. When this happens,
58///   the bundles removed by anticipation are tracked in a separate queue. Any bundle added
59///   later will be required to match the first removed bundle and so on.
60/// * The cursors of added bundles (resp. removed bundles) must be increasing over time.
61/// * Reconciliation of added and removed bundles is allowed to skip some added bundles.
62///   However, the opposite is not true: every removed bundle must be eventually added.
63#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
64#[derive(Allocative, Debug, ClonableView, View)]
65#[allocative(bound = "C")]
66pub struct InboxStateView<C>
67where
68    C: Clone + Context,
69{
70    /// We have already added all the messages below this height and index.
71    pub next_cursor_to_add: RegisterView<C, Cursor>,
72    /// We have already removed all the messages below this height and index.
73    pub next_cursor_to_remove: RegisterView<C, Cursor>,
74    /// These bundles have been added and are waiting to be removed.
75    pub added_bundles: QueueView<C, MessageBundle>,
76    /// These bundles have been removed by anticipation and are waiting to be added.
77    /// At least one of `added_bundles` and `removed_bundles` should be empty.
78    pub removed_bundles: QueueView<C, MessageBundle>,
79    /// When this inbox was restored from a checkpoint, the sender's
80    /// `next_cursor_to_remove` at that time. Bundles with cursors strictly below this
81    /// are silently dropped from `add_bundle` / `remove_bundle`: their effects are
82    /// already baked into the restored execution state, so re-delivering them on this
83    /// validator (e.g. when a sender re-pushes) must be a no-op rather than re-enter
84    /// the inbox or the removed-by-anticipation queue.
85    pub restored_cursor: RegisterView<C, Cursor>,
86}
87
88#[derive(Error, Debug)]
89pub(crate) enum InboxError {
90    #[error(transparent)]
91    ViewError(#[from] ViewError),
92    #[error(transparent)]
93    ArithmeticError(#[from] ArithmeticError),
94    #[error("Cannot reconcile {bundle:?} with {previous_bundle:?}")]
95    UnexpectedBundle {
96        bundle: MessageBundle,
97        previous_bundle: MessageBundle,
98    },
99    #[error("{bundle:?} is out of order. Block and height should be at least: {next_cursor:?}")]
100    IncorrectOrder {
101        bundle: MessageBundle,
102        next_cursor: Cursor,
103    },
104    #[error(
105        "{bundle:?} cannot be skipped: it must be received before the next \
106        messages from the same origin"
107    )]
108    UnskippableBundle { bundle: MessageBundle },
109}
110
111impl From<(ChainId, ChainId, InboxError)> for ChainError {
112    fn from(value: (ChainId, ChainId, InboxError)) -> Self {
113        let (chain_id, origin, error) = value;
114        match error {
115            InboxError::ViewError(e) => ChainError::ViewError(e),
116            InboxError::ArithmeticError(e) => ChainError::ArithmeticError(e),
117            InboxError::UnexpectedBundle {
118                bundle,
119                previous_bundle,
120            } => ChainError::UnexpectedMessage {
121                chain_id,
122                origin,
123                bundle: Box::new(bundle),
124                previous_bundle: Box::new(previous_bundle),
125            },
126            InboxError::IncorrectOrder {
127                bundle,
128                next_cursor,
129            } => ChainError::IncorrectMessageOrder {
130                chain_id,
131                origin,
132                bundle: Box::new(bundle),
133                next_height: next_cursor.height,
134                next_index: next_cursor.index,
135            },
136            InboxError::UnskippableBundle { bundle } => ChainError::CannotSkipMessage {
137                chain_id,
138                origin,
139                bundle: Box::new(bundle),
140            },
141        }
142    }
143}
144
145impl<C> InboxStateView<C>
146where
147    C: Context + Clone + 'static,
148{
149    /// Converts the internal cursor for added bundles into an externally-visible block height.
150    /// This makes sense because the rest of the system always adds bundles one block at a time.
151    pub fn next_block_height_to_receive(&self) -> Result<BlockHeight, ChainError> {
152        let cursor = self.next_cursor_to_add.get();
153        if cursor.index == 0 {
154            Ok(cursor.height)
155        } else {
156            Ok(cursor.height.try_add_one()?)
157        }
158    }
159
160    /// Observes the current inbox size in the metrics histogram.
161    pub fn observe_size_metric(&self) {
162        #[cfg(with_metrics)]
163        metrics::INBOX_SIZE
164            .with_label_values(&[])
165            .observe(self.added_bundles.count() as f64);
166    }
167
168    /// Reconciles this inbox with the producer's snapshot at checkpoint time.
169    ///
170    /// `next_cursor_to_remove` is chain-state-derived and takes the snapshot's
171    /// value (a lagging validator's higher pre-restore advancement is being
172    /// rolled back along with the execution state). `next_cursor_to_add` only
173    /// ratchets up — a lagging validator may already have received bundles past
174    /// the snapshot, and forgetting those deliveries would either lose them or
175    /// trip the inbox-gap check on the next cross-chain update. Pre-existing
176    /// `added_bundles` entries strictly below the cutoff are dropped (their
177    /// effects are baked into the restored execution state) and the
178    /// anticipated-remove queue is cleared since it came from pre-restore
179    /// blocks the rollback has invalidated.
180    ///
181    /// Errors if `restored_cursor` already sits past `cursor`: that means a
182    /// later checkpoint has already bootstrapped this inbox, and the chain
183    /// worker's dispatch should never have routed an earlier one here.
184    pub async fn restore_from_checkpoint(&mut self, cursor: Cursor) -> Result<(), ChainError> {
185        ensure!(
186            *self.restored_cursor.get() <= cursor,
187            ChainError::InternalError(format!(
188                "cannot restore inbox at cursor {cursor:?}: already bootstrapped \
189                 from a later checkpoint at cursor {previous:?}",
190                previous = *self.restored_cursor.get(),
191            ))
192        );
193        self.restored_cursor.set(cursor);
194        if *self.next_cursor_to_add.get() < cursor {
195            self.next_cursor_to_add.set(cursor);
196        }
197        self.next_cursor_to_remove.set(cursor);
198        while let Some(front) = self.added_bundles.front().await? {
199            if front.cursor() >= cursor {
200                break;
201            }
202            self.added_bundles.delete_front();
203        }
204        self.removed_bundles.clear();
205        Ok(())
206    }
207
208    /// Consumes a bundle from the inbox.
209    ///
210    /// Returns `true` if the bundle was already known, i.e. it was present in `added_bundles`.
211    pub(crate) async fn remove_bundle(
212        &mut self,
213        bundle: &MessageBundle,
214    ) -> Result<bool, InboxError> {
215        // Record the latest cursor.
216        let cursor = bundle.cursor();
217        if cursor < *self.restored_cursor.get() {
218            // Bundle's effects are already in the restored execution state; treat the
219            // consumption as a no-op without touching `removed_bundles` so the queue
220            // doesn't fill with bundles a sender will never push again.
221            return Ok(true);
222        }
223        ensure!(
224            cursor >= *self.next_cursor_to_remove.get(),
225            InboxError::IncorrectOrder {
226                bundle: bundle.clone(),
227                next_cursor: *self.next_cursor_to_remove.get(),
228            }
229        );
230        // Discard added bundles with lower cursors (if any).
231        while let Some(previous_bundle) = self.added_bundles.front().await? {
232            if previous_bundle.cursor() >= cursor {
233                break;
234            }
235            ensure!(
236                previous_bundle.is_skippable(),
237                InboxError::UnskippableBundle {
238                    bundle: previous_bundle
239                }
240            );
241            self.added_bundles.delete_front();
242            tracing::trace!("Skipping previously received bundle {:?}", previous_bundle);
243        }
244        // Reconcile the bundle with the next added bundle, or mark it as removed.
245        let already_known = match self.added_bundles.front().await? {
246            Some(previous_bundle) => {
247                // Rationale: If the two cursors are equal, then the bundles should match.
248                // Otherwise, at this point we know that `self.next_cursor_to_add >
249                // previous_bundle.cursor() > cursor`. Notably, `bundle` will never be
250                // added in the future. Therefore, we should fail instead of adding
251                // it to `self.removed_bundles`.
252                ensure!(
253                    bundle == &previous_bundle,
254                    InboxError::UnexpectedBundle {
255                        previous_bundle,
256                        bundle: bundle.clone(),
257                    }
258                );
259                self.added_bundles.delete_front();
260                tracing::trace!("Consuming bundle {:?}", bundle);
261                true
262            }
263            None => {
264                tracing::trace!("Marking bundle as expected: {:?}", bundle);
265                self.removed_bundles.push_back(bundle.clone());
266                #[cfg(with_metrics)]
267                metrics::REMOVED_BUNDLES
268                    .with_label_values(&[])
269                    .observe(self.removed_bundles.count() as f64);
270                false
271            }
272        };
273        self.next_cursor_to_remove.set(cursor.try_add_one()?);
274        Ok(already_known)
275    }
276
277    /// Pushes a bundle to the inbox. The verifications should not fail in production unless
278    /// many validators are faulty.
279    ///
280    /// Returns `true` if the bundle was new, `false` if it was already in `removed_bundles`.
281    pub(crate) async fn add_bundle(&mut self, bundle: MessageBundle) -> Result<bool, InboxError> {
282        // Record the latest cursor.
283        let cursor = bundle.cursor();
284        if cursor < *self.restored_cursor.get() {
285            // The sender is re-delivering a bundle whose effects are already baked
286            // into our restored execution state. Silently drop it.
287            return Ok(false);
288        }
289        ensure!(
290            cursor >= *self.next_cursor_to_add.get(),
291            InboxError::IncorrectOrder {
292                bundle: bundle.clone(),
293                next_cursor: *self.next_cursor_to_add.get(),
294            }
295        );
296        // Find if the bundle was removed ahead of time.
297        let newly_added = match self.removed_bundles.front().await? {
298            Some(previous_bundle) => {
299                if previous_bundle.cursor() == cursor {
300                    // We already executed this bundle by anticipation. Remove it from
301                    // the queue.
302                    ensure!(
303                        bundle == previous_bundle,
304                        InboxError::UnexpectedBundle {
305                            previous_bundle,
306                            bundle,
307                        }
308                    );
309                    self.removed_bundles.delete_front();
310                    #[cfg(with_metrics)]
311                    metrics::REMOVED_BUNDLES
312                        .with_label_values(&[])
313                        .observe(self.removed_bundles.count() as f64);
314                } else {
315                    // The receiver has already executed a later bundle from the same
316                    // sender ahead of time so we should skip this one.
317                    ensure!(
318                        cursor < previous_bundle.cursor() && bundle.is_skippable(),
319                        InboxError::UnexpectedBundle {
320                            previous_bundle,
321                            bundle,
322                        }
323                    );
324                }
325                false
326            }
327            None => {
328                // Otherwise, schedule the messages for execution.
329                self.added_bundles.push_back(bundle);
330                true
331            }
332        };
333        self.next_cursor_to_add.set(cursor.try_add_one()?);
334        Ok(newly_added)
335    }
336}
337
338#[cfg(with_testing)]
339impl InboxStateView<MemoryContext<()>>
340where
341    MemoryContext<()>: Context + Clone + Send + Sync + 'static,
342{
343    pub async fn new() -> Self {
344        let context = MemoryContext::new_for_testing(());
345        Self::load(context)
346            .await
347            .expect("Loading from memory should work")
348    }
349}