Skip to main content

linera_chain/
lib.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! This module manages the state of a Linera chain, including cross-chain communication.
5
6#![deny(missing_docs)]
7
8/// Block types and the wrappers that pair them with their execution outcomes.
9pub mod block;
10mod certificate;
11
12/// Convenience re-exports of the public block and certificate types.
13pub mod types {
14    pub use super::{block::*, certificate::*};
15}
16
17mod block_tracker;
18mod chain;
19/// Data types exchanged while proposing, voting on, and confirming blocks.
20pub mod data_types;
21mod inbox;
22pub mod manager;
23mod outbox;
24mod pending_blobs;
25#[cfg(with_testing)]
26pub mod test;
27
28pub use chain::{BlockExecution, BlockExecutionPhase, ChainIdSet, ChainStateView, ChainTipState};
29use data_types::{MessageBundle, PostedMessage};
30use linera_base::{
31    bcs,
32    crypto::CryptoError,
33    data_types::{ArithmeticError, BlockHeight, Round, Timestamp},
34    identifiers::{ApplicationId, ChainId},
35};
36use linera_execution::ExecutionError;
37use linera_views::ViewError;
38use thiserror::Error;
39
40/// An error that occurred while validating or executing a block on a chain.
41#[derive(Error, Debug, strum::IntoStaticStr)]
42#[allow(missing_docs)]
43pub enum ChainError {
44    #[error("Cryptographic error: {0}")]
45    CryptoError(#[from] CryptoError),
46    #[error(transparent)]
47    ArithmeticError(#[from] ArithmeticError),
48    #[error(transparent)]
49    ViewError(#[from] ViewError),
50    #[error("Execution error: {0} during {1:?}")]
51    ExecutionError(Box<ExecutionError>, ChainExecutionContext),
52
53    #[error("The chain being queried is not active {0}")]
54    InactiveChain(ChainId),
55    #[error(
56        "Message in block proposed to {chain_id} does not match the previously received messages from \
57        origin {origin:?}: was {bundle:?} instead of {previous_bundle:?}"
58    )]
59    UnexpectedMessage {
60        chain_id: ChainId,
61        origin: ChainId,
62        bundle: Box<MessageBundle>,
63        previous_bundle: Box<MessageBundle>,
64    },
65    #[error(
66        "Message in block proposed to {chain_id} is out of order compared to previous messages \
67         from origin {origin:?}: {bundle:?}. Block and height should be at least: \
68         {next_height}, {next_index}"
69    )]
70    IncorrectMessageOrder {
71        chain_id: ChainId,
72        origin: ChainId,
73        bundle: Box<MessageBundle>,
74        next_height: BlockHeight,
75        next_index: u32,
76    },
77    #[error(
78        "Block proposed to {chain_id} is attempting to reject protected message \
79        {posted_message:?}"
80    )]
81    CannotRejectMessage {
82        chain_id: ChainId,
83        origin: ChainId,
84        posted_message: Box<PostedMessage>,
85    },
86    #[error(
87        "Block proposed to {chain_id} is attempting to skip a message bundle \
88         that cannot be skipped: {bundle:?}"
89    )]
90    CannotSkipMessage {
91        chain_id: ChainId,
92        origin: ChainId,
93        bundle: Box<MessageBundle>,
94    },
95    #[error(
96        "Incoming message bundle in block proposed to {chain_id} has timestamp \
97        {bundle_timestamp:}, which is later than the block timestamp {block_timestamp:}."
98    )]
99    IncorrectBundleTimestamp {
100        chain_id: ChainId,
101        bundle_timestamp: Timestamp,
102        block_timestamp: Timestamp,
103    },
104    #[error("The signature was not created by a valid entity")]
105    InvalidSigner,
106    #[error(
107        "Chain is expecting a next block at height {expected_block_height} but the given block \
108        is at height {found_block_height} instead"
109    )]
110    UnexpectedBlockHeight {
111        expected_block_height: BlockHeight,
112        found_block_height: BlockHeight,
113    },
114    #[error("The previous block hash of a new block should match the last block of the chain")]
115    UnexpectedPreviousBlockHash,
116    #[error("Sequence numbers above the maximal value are not usable for blocks")]
117    BlockHeightOverflow,
118    #[error(
119        "Block timestamp {new} must not be earlier than the parent block's timestamp {parent}"
120    )]
121    InvalidBlockTimestamp { parent: Timestamp, new: Timestamp },
122    #[error("Round number should be at least {0:?}")]
123    InsufficientRound(Round),
124    #[error("Round number should be greater than {0:?}")]
125    InsufficientRoundStrict(Round),
126    #[error("Round number should be {0:?}")]
127    WrongRound(Round),
128    #[error("Already voted to confirm a different block for height {0:?} at round number {1:?}")]
129    HasIncompatibleConfirmedVote(BlockHeight, Round),
130    #[error("Proposal for height {0:?} is not newer than locking block in round {1:?}")]
131    MustBeNewerThanLockingBlock(BlockHeight, Round),
132    #[error("Cannot confirm a block before its predecessors: {current_block_height:?}")]
133    MissingEarlierBlocks { current_block_height: BlockHeight },
134    #[error("Signatures in a certificate must be from different validators")]
135    CertificateValidatorReuse,
136    #[error("Signatures in a certificate must form a quorum")]
137    CertificateRequiresQuorum,
138    #[error(
139        "Inbox gap on chain {chain_id} from origin {origin}: \
140        expected height {expected_height}, got {actual_height}"
141    )]
142    InboxGapDetected {
143        chain_id: ChainId,
144        origin: ChainId,
145        expected_height: BlockHeight,
146        actual_height: BlockHeight,
147    },
148    #[error("Internal error {0}")]
149    InternalError(String),
150    #[error("Corrupted chain state: {0}")]
151    CorruptedChainState(String),
152    #[error("Block proposal has size {0} which is too large")]
153    BlockProposalTooLarge(usize),
154    #[error(transparent)]
155    BcsError(#[from] bcs::Error),
156    #[error("Closed chains cannot have operations, accepted messages or empty blocks")]
157    ClosedChain,
158    #[error("Empty blocks are not allowed")]
159    EmptyBlock,
160    #[error("All operations on this chain must be from one of the following applications: {0:?}")]
161    AuthorizedApplications(Vec<ApplicationId>),
162    #[error("Missing operations or messages from mandatory applications: {0:?}")]
163    MissingMandatoryApplications(Vec<ApplicationId>),
164    #[error("Executed block contains fewer oracle responses than requests")]
165    MissingOracleResponseList,
166    #[error("Not signing timeout certificate; current round does not time out")]
167    RoundDoesNotTimeOut,
168    #[error("Not signing timeout certificate; current round times out at time {0}")]
169    NotTimedOutYet(Timestamp),
170    #[error(
171        "Cannot vote for block proposal of chain {chain_id} because {} cross-chain message \
172         bundle(s) have not been received yet",
173        bundles.len()
174    )]
175    MissingCrossChainUpdates {
176        chain_id: ChainId,
177        /// The missing incoming message bundles, as `(origin chain, height)` pairs that must
178        /// all be received before this block can be validated. The validator reports every
179        /// missing bundle at once so the client can fetch them in a single round, instead of
180        /// the legacy one-rejection-per-missing-sender behavior.
181        bundles: Vec<(ChainId, BlockHeight)>,
182    },
183}
184
185impl ChainError {
186    /// Returns whether this error is caused by an issue in the local node.
187    ///
188    /// Returns `false` whenever the error could be caused by a bad message from a peer.
189    pub fn is_local(&self) -> bool {
190        match self {
191            ChainError::CryptoError(_)
192            | ChainError::ArithmeticError(_)
193            | ChainError::ViewError(ViewError::NotFound(_))
194            | ChainError::InactiveChain(_)
195            | ChainError::IncorrectMessageOrder { .. }
196            | ChainError::CannotRejectMessage { .. }
197            | ChainError::CannotSkipMessage { .. }
198            | ChainError::IncorrectBundleTimestamp { .. }
199            | ChainError::InvalidSigner
200            | ChainError::UnexpectedBlockHeight { .. }
201            | ChainError::UnexpectedPreviousBlockHash
202            | ChainError::BlockHeightOverflow
203            | ChainError::InvalidBlockTimestamp { .. }
204            | ChainError::InsufficientRound(_)
205            | ChainError::InsufficientRoundStrict(_)
206            | ChainError::WrongRound(_)
207            | ChainError::HasIncompatibleConfirmedVote(..)
208            | ChainError::MustBeNewerThanLockingBlock(..)
209            | ChainError::MissingEarlierBlocks { .. }
210            | ChainError::CertificateValidatorReuse
211            | ChainError::CertificateRequiresQuorum
212            | ChainError::BlockProposalTooLarge(_)
213            | ChainError::ClosedChain
214            | ChainError::EmptyBlock
215            | ChainError::AuthorizedApplications(_)
216            | ChainError::MissingMandatoryApplications(_)
217            | ChainError::MissingOracleResponseList
218            | ChainError::RoundDoesNotTimeOut
219            | ChainError::NotTimedOutYet(_)
220            | ChainError::MissingCrossChainUpdates { .. } => false,
221            ChainError::ViewError(_)
222            | ChainError::UnexpectedMessage { .. }
223            | ChainError::InboxGapDetected { .. }
224            | ChainError::InternalError(_)
225            | ChainError::CorruptedChainState(_)
226            | ChainError::BcsError(_) => true,
227            ChainError::ExecutionError(execution_error, _) => execution_error.is_local(),
228        }
229    }
230
231    /// Returns the qualified error variant name for the `error_type` metric label,
232    /// e.g. `"ChainError::UnexpectedBlockHeight"`.
233    ///
234    /// For `ExecutionError` variants, delegates to `ExecutionError::error_type()`
235    /// to surface the underlying error name rather than just `"ExecutionError"`.
236    pub fn error_type(&self) -> String {
237        match self {
238            ChainError::ExecutionError(execution_error, _) => execution_error.error_type(),
239            other => {
240                let variant: &'static str = other.into();
241                format!("ChainError::{variant}")
242            }
243        }
244    }
245}
246
247/// The phase of block execution during which an error occurred.
248#[derive(Copy, Clone, Debug)]
249#[cfg_attr(with_testing, derive(Eq, PartialEq))]
250#[allow(missing_docs)]
251pub enum ChainExecutionContext {
252    Query,
253    DescribeApplication,
254    IncomingBundle(u32),
255    Operation(u32),
256    Block,
257}
258
259/// Extension trait for attaching a [`ChainExecutionContext`] to an execution error.
260pub trait ExecutionResultExt<T> {
261    /// Converts the error into a [`ChainError`], tagging it with the given execution context.
262    fn with_execution_context(self, context: ChainExecutionContext) -> Result<T, ChainError>;
263}
264
265impl<T, E> ExecutionResultExt<T> for Result<T, E>
266where
267    E: Into<ExecutionError>,
268{
269    fn with_execution_context(self, context: ChainExecutionContext) -> Result<T, ChainError> {
270        self.map_err(|error| ChainError::ExecutionError(Box::new(error.into()), context))
271    }
272}