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::{ChainIdSet, ChainStateView, ChainTipState, StreamCounts};
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        "Cannot vote for block proposal of chain {chain_id} because a message \
57         from chain {origin} at height {height} has not been received yet"
58    )]
59    MissingCrossChainUpdate {
60        chain_id: ChainId,
61        origin: ChainId,
62        height: BlockHeight,
63    },
64    #[error(
65        "Message in block proposed to {chain_id} does not match the previously received messages from \
66        origin {origin:?}: was {bundle:?} instead of {previous_bundle:?}"
67    )]
68    UnexpectedMessage {
69        chain_id: ChainId,
70        origin: ChainId,
71        bundle: Box<MessageBundle>,
72        previous_bundle: Box<MessageBundle>,
73    },
74    #[error(
75        "Message in block proposed to {chain_id} is out of order compared to previous messages \
76         from origin {origin:?}: {bundle:?}. Block and height should be at least: \
77         {next_height}, {next_index}"
78    )]
79    IncorrectMessageOrder {
80        chain_id: ChainId,
81        origin: ChainId,
82        bundle: Box<MessageBundle>,
83        next_height: BlockHeight,
84        next_index: u32,
85    },
86    #[error(
87        "Block proposed to {chain_id} is attempting to reject protected message \
88        {posted_message:?}"
89    )]
90    CannotRejectMessage {
91        chain_id: ChainId,
92        origin: ChainId,
93        posted_message: Box<PostedMessage>,
94    },
95    #[error(
96        "Block proposed to {chain_id} is attempting to skip a message bundle \
97         that cannot be skipped: {bundle:?}"
98    )]
99    CannotSkipMessage {
100        chain_id: ChainId,
101        origin: ChainId,
102        bundle: Box<MessageBundle>,
103    },
104    #[error(
105        "Incoming message bundle in block proposed to {chain_id} has timestamp \
106        {bundle_timestamp:}, which is later than the block timestamp {block_timestamp:}."
107    )]
108    IncorrectBundleTimestamp {
109        chain_id: ChainId,
110        bundle_timestamp: Timestamp,
111        block_timestamp: Timestamp,
112    },
113    #[error("The signature was not created by a valid entity")]
114    InvalidSigner,
115    #[error(
116        "Chain is expecting a next block at height {expected_block_height} but the given block \
117        is at height {found_block_height} instead"
118    )]
119    UnexpectedBlockHeight {
120        expected_block_height: BlockHeight,
121        found_block_height: BlockHeight,
122    },
123    #[error("The previous block hash of a new block should match the last block of the chain")]
124    UnexpectedPreviousBlockHash,
125    #[error("Sequence numbers above the maximal value are not usable for blocks")]
126    BlockHeightOverflow,
127    #[error(
128        "Block timestamp {new} must not be earlier than the parent block's timestamp {parent}"
129    )]
130    InvalidBlockTimestamp { parent: Timestamp, new: Timestamp },
131    #[error("Round number should be at least {0:?}")]
132    InsufficientRound(Round),
133    #[error("Round number should be greater than {0:?}")]
134    InsufficientRoundStrict(Round),
135    #[error("Round number should be {0:?}")]
136    WrongRound(Round),
137    #[error("Already voted to confirm a different block for height {0:?} at round number {1:?}")]
138    HasIncompatibleConfirmedVote(BlockHeight, Round),
139    #[error("Proposal for height {0:?} is not newer than locking block in round {1:?}")]
140    MustBeNewerThanLockingBlock(BlockHeight, Round),
141    #[error("Cannot confirm a block before its predecessors: {current_block_height:?}")]
142    MissingEarlierBlocks { current_block_height: BlockHeight },
143    #[error("Signatures in a certificate must be from different validators")]
144    CertificateValidatorReuse,
145    #[error("Signatures in a certificate must form a quorum")]
146    CertificateRequiresQuorum,
147    #[error(
148        "Inbox gap on chain {chain_id} from origin {origin}: \
149        expected height {expected_height}, got {actual_height}"
150    )]
151    InboxGapDetected {
152        chain_id: ChainId,
153        origin: ChainId,
154        expected_height: BlockHeight,
155        actual_height: BlockHeight,
156    },
157    #[error("Internal error {0}")]
158    InternalError(String),
159    #[error("Corrupted chain state: {0}")]
160    CorruptedChainState(String),
161    #[error("Block proposal has size {0} which is too large")]
162    BlockProposalTooLarge(usize),
163    #[error(transparent)]
164    BcsError(#[from] bcs::Error),
165    #[error("Closed chains cannot have operations, accepted messages or empty blocks")]
166    ClosedChain,
167    #[error("Empty blocks are not allowed")]
168    EmptyBlock,
169    #[error("All operations on this chain must be from one of the following applications: {0:?}")]
170    AuthorizedApplications(Vec<ApplicationId>),
171    #[error("Missing operations or messages from mandatory applications: {0:?}")]
172    MissingMandatoryApplications(Vec<ApplicationId>),
173    #[error("Executed block contains fewer oracle responses than requests")]
174    MissingOracleResponseList,
175    #[error("Not signing timeout certificate; current round does not time out")]
176    RoundDoesNotTimeOut,
177    #[error("Not signing timeout certificate; current round times out at time {0}")]
178    NotTimedOutYet(Timestamp),
179    #[error("Checkpoint precondition failed: {0}")]
180    CheckpointPreconditionFailed(&'static str),
181}
182
183impl ChainError {
184    /// Returns whether this error is caused by an issue in the local node.
185    ///
186    /// Returns `false` whenever the error could be caused by a bad message from a peer.
187    pub fn is_local(&self) -> bool {
188        match self {
189            ChainError::CryptoError(_)
190            | ChainError::ArithmeticError(_)
191            | ChainError::ViewError(ViewError::NotFound(_))
192            | ChainError::InactiveChain(_)
193            | ChainError::IncorrectMessageOrder { .. }
194            | ChainError::CannotRejectMessage { .. }
195            | ChainError::CannotSkipMessage { .. }
196            | ChainError::IncorrectBundleTimestamp { .. }
197            | ChainError::InvalidSigner
198            | ChainError::UnexpectedBlockHeight { .. }
199            | ChainError::UnexpectedPreviousBlockHash
200            | ChainError::BlockHeightOverflow
201            | ChainError::InvalidBlockTimestamp { .. }
202            | ChainError::InsufficientRound(_)
203            | ChainError::InsufficientRoundStrict(_)
204            | ChainError::WrongRound(_)
205            | ChainError::HasIncompatibleConfirmedVote(..)
206            | ChainError::MustBeNewerThanLockingBlock(..)
207            | ChainError::MissingEarlierBlocks { .. }
208            | ChainError::CertificateValidatorReuse
209            | ChainError::CertificateRequiresQuorum
210            | ChainError::BlockProposalTooLarge(_)
211            | ChainError::ClosedChain
212            | ChainError::EmptyBlock
213            | ChainError::AuthorizedApplications(_)
214            | ChainError::MissingMandatoryApplications(_)
215            | ChainError::MissingOracleResponseList
216            | ChainError::RoundDoesNotTimeOut
217            | ChainError::NotTimedOutYet(_)
218            | ChainError::CheckpointPreconditionFailed(_)
219            | ChainError::MissingCrossChainUpdate { .. } => false,
220            ChainError::ViewError(_)
221            | ChainError::UnexpectedMessage { .. }
222            | ChainError::InboxGapDetected { .. }
223            | ChainError::InternalError(_)
224            | ChainError::CorruptedChainState(_)
225            | ChainError::BcsError(_) => true,
226            ChainError::ExecutionError(execution_error, _) => execution_error.is_local(),
227        }
228    }
229
230    /// Returns the qualified error variant name for the `error_type` metric label,
231    /// e.g. `"ChainError::UnexpectedBlockHeight"`.
232    ///
233    /// For `ExecutionError` variants, delegates to `ExecutionError::error_type()`
234    /// to surface the underlying error name rather than just `"ExecutionError"`.
235    pub fn error_type(&self) -> String {
236        match self {
237            ChainError::ExecutionError(execution_error, _) => execution_error.error_type(),
238            other => {
239                let variant: &'static str = other.into();
240                format!("ChainError::{variant}")
241            }
242        }
243    }
244}
245
246/// The phase of block execution during which an error occurred.
247#[derive(Copy, Clone, Debug)]
248#[cfg_attr(with_testing, derive(Eq, PartialEq))]
249#[allow(missing_docs)]
250pub enum ChainExecutionContext {
251    Query,
252    DescribeApplication,
253    IncomingBundle(u32),
254    Operation(u32),
255    Block,
256}
257
258/// Extension trait for attaching a [`ChainExecutionContext`] to an execution error.
259pub trait ExecutionResultExt<T> {
260    /// Converts the error into a [`ChainError`], tagging it with the given execution context.
261    fn with_execution_context(self, context: ChainExecutionContext) -> Result<T, ChainError>;
262}
263
264impl<T, E> ExecutionResultExt<T> for Result<T, E>
265where
266    E: Into<ExecutionError>,
267{
268    fn with_execution_context(self, context: ChainExecutionContext) -> Result<T, ChainError> {
269        self.map_err(|error| ChainError::ExecutionError(Box::new(error.into()), context))
270    }
271}