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