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