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 justification;
23pub mod manager;
24mod outbox;
25mod pending_blobs;
26#[cfg(with_testing)]
27pub mod test;
28
29pub use chain::{BlockExecutionPhase, ChainIdSet, ChainStateView, ChainTipState, StreamCounts};
30use data_types::{MessageBundle, PostedMessage};
31use linera_base::{
32    bcs,
33    crypto::CryptoError,
34    data_types::{ArithmeticError, BlockHeight, Epoch, Round, Timestamp},
35    identifiers::{ApplicationId, ChainId},
36};
37use linera_execution::ExecutionError;
38use linera_views::ViewError;
39use thiserror::Error;
40
41/// An error that occurred while validating or executing a block on a chain.
42#[derive(Error, Debug, strum::IntoStaticStr)]
43#[allow(missing_docs)]
44pub enum ChainError {
45    #[error("Cryptographic error: {0}")]
46    CryptoError(#[from] CryptoError),
47    #[error(transparent)]
48    ArithmeticError(#[from] ArithmeticError),
49    #[error(transparent)]
50    ViewError(#[from] ViewError),
51    #[error("Execution error: {0} during {1:?}")]
52    ExecutionError(Box<ExecutionError>, ChainExecutionContext),
53
54    #[error("The chain being queried is not active {0}")]
55    InactiveChain(ChainId),
56    #[error(
57        "Cannot vote for block proposal of chain {chain_id} because a message \
58         from chain {origin} at height {height} has not been received yet"
59    )]
60    MissingCrossChainUpdate {
61        chain_id: ChainId,
62        origin: ChainId,
63        height: BlockHeight,
64    },
65    #[error(
66        "Message in block proposed to {chain_id} does not match the previously received messages from \
67        origin {origin:?}: was {bundle:?} instead of {previous_bundle:?}"
68    )]
69    UnexpectedMessage {
70        chain_id: ChainId,
71        origin: ChainId,
72        bundle: Box<MessageBundle>,
73        previous_bundle: Box<MessageBundle>,
74    },
75    #[error(
76        "Message in block proposed to {chain_id} is out of order compared to previous messages \
77         from origin {origin:?}: {bundle:?}. Block and height should be at least: \
78         {next_height}, {next_index}"
79    )]
80    IncorrectMessageOrder {
81        chain_id: ChainId,
82        origin: ChainId,
83        bundle: Box<MessageBundle>,
84        next_height: BlockHeight,
85        next_index: u32,
86    },
87    #[error(
88        "Block proposed to {chain_id} is attempting to reject protected message \
89        {posted_message:?}"
90    )]
91    CannotRejectMessage {
92        chain_id: ChainId,
93        origin: ChainId,
94        posted_message: Box<PostedMessage>,
95    },
96    #[error(
97        "Block proposed to {chain_id} is attempting to skip a message bundle \
98         that cannot be skipped: {bundle:?}"
99    )]
100    CannotSkipMessage {
101        chain_id: ChainId,
102        origin: ChainId,
103        bundle: Box<MessageBundle>,
104    },
105    #[error(
106        "Incoming message bundle in block proposed to {chain_id} has timestamp \
107        {bundle_timestamp:}, which is later than the block timestamp {block_timestamp:}."
108    )]
109    IncorrectBundleTimestamp {
110        chain_id: ChainId,
111        bundle_timestamp: Timestamp,
112        block_timestamp: Timestamp,
113    },
114    #[error("The signature was not created by a valid entity")]
115    InvalidSigner,
116    #[error(
117        "Chain is expecting a next block at height {expected_block_height} but the given block \
118        is at height {found_block_height} instead"
119    )]
120    UnexpectedBlockHeight {
121        expected_block_height: BlockHeight,
122        found_block_height: BlockHeight,
123    },
124    #[error("The previous block hash of a new block should match the last block of the chain")]
125    UnexpectedPreviousBlockHash,
126    #[error("Sequence numbers above the maximal value are not usable for blocks")]
127    BlockHeightOverflow,
128    #[error(
129        "Block timestamp {new} must not be earlier than the parent block's timestamp {parent}"
130    )]
131    InvalidBlockTimestamp { parent: Timestamp, new: Timestamp },
132    #[error("Round number should be at least {0:?}")]
133    InsufficientRound(Round),
134    #[error("Round number should be greater than {0:?}")]
135    InsufficientRoundStrict(Round),
136    #[error("Round number should be {0:?}")]
137    WrongRound(Round),
138    #[error("Already voted to confirm a different block for height {0:?} at round number {1:?}")]
139    HasIncompatibleConfirmedVote(BlockHeight, Round),
140    #[error("Proposal for height {0:?} is not newer than locking block in round {1:?}")]
141    MustBeNewerThanLockingBlock(BlockHeight, Round),
142    #[error("Cannot confirm a block before its predecessors: {current_block_height:?}")]
143    MissingEarlierBlocks { current_block_height: BlockHeight },
144    #[error("Signatures in a certificate must be from different validators")]
145    CertificateValidatorReuse,
146    #[error("Signatures in a certificate must form a quorum")]
147    CertificateRequiresQuorum,
148    #[error("Justification chain rounds must be strictly increasing")]
149    JustificationRoundsNotIncreasing,
150    #[error("Certificate unlocking round does not match the top of its justification chain")]
151    JustificationUnlockingRoundMismatch,
152    #[error("Certificate justification commitment does not match its justification chain")]
153    JustificationCommitmentMismatch,
154    #[error("Justification chain must lie in rounds strictly below the certificate's round")]
155    JustificationChainNotBelowCertificate,
156    #[error("Certificate carries the first-round attestation but was not confirmed in the chain's first round")]
157    FalseFirstRoundAttestation,
158    #[error("Equivocation proof must reference two different blocks")]
159    EquivocationProofSameBlock,
160    #[error("Equivocation proof references blocks on different chains or at different heights")]
161    EquivocationProofDifferentChainOrHeight,
162    #[error("Equivocation proof does not violate the lock claim")]
163    EquivocationProofNoLockViolation,
164    #[error("Equivocation proof's earlier vote is not below the attested first round")]
165    EquivocationProofNoFirstRoundViolation,
166    #[error("Equivocation proof's opened justification is a valid quorum")]
167    EquivocationProofValidJustification,
168    #[error(
169        "Inbox gap on chain {chain_id} from origin {origin}: \
170        expected height {expected_height}, got {actual_height}"
171    )]
172    InboxGapDetected {
173        chain_id: ChainId,
174        origin: ChainId,
175        expected_height: BlockHeight,
176        actual_height: BlockHeight,
177    },
178    #[error("Internal error {0}")]
179    InternalError(String),
180    #[error("Corrupted chain state: {0}")]
181    CorruptedChainState(String),
182    #[error("Block proposal has size {0} which is too large")]
183    BlockProposalTooLarge(usize),
184    #[error(transparent)]
185    BcsError(#[from] bcs::Error),
186    #[error(
187        "Block advances the chain's epoch from {start_epoch} to {end_epoch}; \
188         a block may advance the epoch at most once"
189    )]
190    MultipleEpochAdvances {
191        start_epoch: Epoch,
192        end_epoch: Epoch,
193    },
194    #[error("Closed chains cannot have operations, accepted messages or empty blocks")]
195    ClosedChain,
196    #[error("Empty blocks are not allowed")]
197    EmptyBlock,
198    #[error("All operations on this chain must be from one of the following applications: {0:?}")]
199    AuthorizedApplications(Vec<ApplicationId>),
200    #[error("Missing operations or messages from mandatory applications: {0:?}")]
201    MissingMandatoryApplications(Vec<ApplicationId>),
202    #[error("Executed block contains fewer oracle responses than requests")]
203    MissingOracleResponseList,
204    #[error("Not signing timeout certificate; current round does not time out")]
205    RoundDoesNotTimeOut,
206    #[error("Not signing timeout certificate; current round times out at time {0}")]
207    NotTimedOutYet(Timestamp),
208    #[error("Checkpoint precondition failed: {0}")]
209    CheckpointPreconditionFailed(&'static str),
210}
211
212impl ChainError {
213    /// Returns whether this error is caused by an issue in the local node.
214    ///
215    /// Returns `false` whenever the error could be caused by a bad message from a peer.
216    pub fn is_local(&self) -> bool {
217        match self {
218            ChainError::CryptoError(_)
219            | ChainError::ArithmeticError(_)
220            | ChainError::ViewError(ViewError::NotFound(_))
221            | ChainError::InactiveChain(_)
222            | ChainError::IncorrectMessageOrder { .. }
223            | ChainError::CannotRejectMessage { .. }
224            | ChainError::CannotSkipMessage { .. }
225            | ChainError::IncorrectBundleTimestamp { .. }
226            | ChainError::InvalidSigner
227            | ChainError::UnexpectedBlockHeight { .. }
228            | ChainError::UnexpectedPreviousBlockHash
229            | ChainError::BlockHeightOverflow
230            | ChainError::InvalidBlockTimestamp { .. }
231            | ChainError::InsufficientRound(_)
232            | ChainError::InsufficientRoundStrict(_)
233            | ChainError::WrongRound(_)
234            | ChainError::HasIncompatibleConfirmedVote(..)
235            | ChainError::MustBeNewerThanLockingBlock(..)
236            | ChainError::MissingEarlierBlocks { .. }
237            | ChainError::CertificateValidatorReuse
238            | ChainError::CertificateRequiresQuorum
239            | ChainError::JustificationRoundsNotIncreasing
240            | ChainError::JustificationUnlockingRoundMismatch
241            | ChainError::JustificationCommitmentMismatch
242            | ChainError::JustificationChainNotBelowCertificate
243            | ChainError::FalseFirstRoundAttestation
244            | ChainError::EquivocationProofSameBlock
245            | ChainError::EquivocationProofDifferentChainOrHeight
246            | ChainError::EquivocationProofNoLockViolation
247            | ChainError::EquivocationProofNoFirstRoundViolation
248            | ChainError::EquivocationProofValidJustification
249            | ChainError::BlockProposalTooLarge(_)
250            | ChainError::MultipleEpochAdvances { .. }
251            | ChainError::ClosedChain
252            | ChainError::EmptyBlock
253            | ChainError::AuthorizedApplications(_)
254            | ChainError::MissingMandatoryApplications(_)
255            | ChainError::MissingOracleResponseList
256            | ChainError::RoundDoesNotTimeOut
257            | ChainError::NotTimedOutYet(_)
258            | ChainError::CheckpointPreconditionFailed(_)
259            | ChainError::MissingCrossChainUpdate { .. } => false,
260            ChainError::ViewError(_)
261            | ChainError::UnexpectedMessage { .. }
262            | ChainError::InboxGapDetected { .. }
263            | ChainError::InternalError(_)
264            | ChainError::CorruptedChainState(_)
265            | ChainError::BcsError(_) => true,
266            ChainError::ExecutionError(execution_error, _) => execution_error.is_local(),
267        }
268    }
269
270    /// Returns the qualified error variant name for the `error_type` metric label,
271    /// e.g. `"ChainError::UnexpectedBlockHeight"`.
272    ///
273    /// For `ExecutionError` variants, delegates to `ExecutionError::error_type()`
274    /// to surface the underlying error name rather than just `"ExecutionError"`.
275    pub fn error_type(&self) -> String {
276        match self {
277            ChainError::ExecutionError(execution_error, _) => execution_error.error_type(),
278            other => {
279                let variant: &'static str = other.into();
280                format!("ChainError::{variant}")
281            }
282        }
283    }
284}
285
286/// The phase of block execution during which an error occurred.
287#[derive(Copy, Clone, Debug)]
288#[cfg_attr(with_testing, derive(Eq, PartialEq))]
289#[allow(missing_docs)]
290pub enum ChainExecutionContext {
291    Query,
292    DescribeApplication,
293    IncomingBundle(u32),
294    Operation(u32),
295    Block,
296}
297
298/// Extension trait for attaching a [`ChainExecutionContext`] to an execution error.
299pub trait ExecutionResultExt<T> {
300    /// Converts the error into a [`ChainError`], tagging it with the given execution context.
301    fn with_execution_context(self, context: ChainExecutionContext) -> Result<T, ChainError>;
302}
303
304impl<T, E> ExecutionResultExt<T> for Result<T, E>
305where
306    E: Into<ExecutionError>,
307{
308    fn with_execution_context(self, context: ChainExecutionContext) -> Result<T, ChainError> {
309        self.map_err(|error| ChainError::ExecutionError(Box::new(error.into()), context))
310    }
311}