Skip to main content

zebra_state/
error.rs

1//! Error types for Zebra's state.
2
3use std::{path::PathBuf, sync::Arc};
4
5use chrono::{DateTime, Utc};
6use derive_new::new;
7use thiserror::Error;
8
9use zebra_chain::{
10    amount::{self, NegativeAllowed, NonNegative},
11    block,
12    history_tree::HistoryTreeError,
13    ironwood, orchard, sapling, sprout, transaction, transparent,
14    value_balance::{ValueBalance, ValueBalanceError},
15    work::difficulty::CompactDifficulty,
16};
17
18use crate::{constants::MIN_TRANSPARENT_COINBASE_MATURITY, HashOrHeight, KnownBlock};
19
20/// A wrapper for type erased errors that is itself clonable and implements the
21/// Error trait
22#[derive(Debug, Error, Clone)]
23#[error(transparent)]
24pub struct CloneError {
25    source: Arc<dyn std::error::Error + Send + Sync + 'static>,
26}
27
28impl From<CommitSemanticallyVerifiedError> for CloneError {
29    fn from(source: CommitSemanticallyVerifiedError) -> Self {
30        let source = Arc::new(source);
31        Self { source }
32    }
33}
34
35impl From<BoxError> for CloneError {
36    fn from(source: BoxError) -> Self {
37        let source = Arc::from(source);
38        Self { source }
39    }
40}
41
42/// A boxed [`std::error::Error`].
43pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
44
45/// An error describing why opening the finalized state database failed.
46///
47/// These errors are recoverable open-time failures that the caller can report,
48/// as opposed to invariant violations that indicate a bug.
49#[derive(Debug, Error)]
50#[non_exhaustive]
51pub enum StateInitError {
52    /// A read-only state was requested, but the configured cache directory is
53    /// missing or unreadable.
54    ///
55    /// A read-only secondary instance must never create the primary's cache
56    /// directory, so a missing or unreadable directory is a fatal configuration
57    /// error rather than something to be created.
58    #[error(
59        "cannot open read-only state: cache directory {path:?} is missing or unreadable. \
60         Hint: a read-only state requires an existing Zebra cache directory; check that the \
61         state cache_dir in the Zebra config points at a running Zebra node's cache directory"
62    )]
63    ReadOnlyCacheDirUnreadable {
64        /// The configured cache directory that could not be read.
65        path: PathBuf,
66        /// The underlying I/O error returned while reading the directory.
67        source: std::io::Error,
68    },
69
70    /// A read-only state was requested, but no database exists at the expected
71    /// path.
72    ///
73    /// A read-only secondary instance cannot create a database, so the absence
74    /// of an existing database is a fatal configuration error.
75    #[error(
76        "cannot open read-only state: no database found at {path:?}. \
77         Hint: a read-only state requires an existing finalized database created by a running \
78         Zebra node; check that the state cache_dir in the Zebra config points at that node's \
79         cache directory"
80    )]
81    ReadOnlyDatabaseNotFound {
82        /// The database path at which no database was found.
83        path: PathBuf,
84    },
85
86    /// A read-only state was requested together with an ephemeral database.
87    ///
88    /// A read-only secondary follows another process's primary database and must
89    /// never delete it, whereas an ephemeral database deletes its files on drop. The
90    /// two are mutually exclusive, so requesting both is a fatal configuration error.
91    #[error(
92        "cannot open read-only state: an ephemeral database was also requested. \
93         Hint: a read-only state follows an existing Zebra node's database and must not \
94         delete it; set `ephemeral = false`, or do not request a read-only state"
95    )]
96    ReadOnlyEphemeralConflict,
97}
98
99/// An error describing why a block could not be queued to be committed to the state.
100#[derive(Debug, Error, Clone, PartialEq, Eq, new)]
101pub enum CommitBlockError {
102    #[error("block hash is a duplicate: already in {location}")]
103    /// The block is a duplicate: it is already queued or committed in the state.
104    Duplicate {
105        /// Hash or height of the duplicated block.
106        hash_or_height: Option<HashOrHeight>,
107        /// Location in the state where the block can be found.
108        location: KnownBlock,
109    },
110
111    /// Contextual validation failed.
112    #[error("could not contextually validate semantically verified block")]
113    ValidateContextError(#[from] Box<ValidateContextError>),
114
115    /// The write task exited (likely during shutdown).
116    #[error("block commit task exited. Is Zebra shutting down?")]
117    #[non_exhaustive]
118    WriteTaskExited,
119}
120
121impl CommitBlockError {
122    /// Returns `true` if this is definitely a duplicate commit request.
123    /// Some duplicate requests might not be detected, and therefore return `false`.
124    pub fn is_duplicate_request(&self) -> bool {
125        matches!(self, CommitBlockError::Duplicate { .. })
126    }
127
128    /// Returns a suggested misbehaviour score increment for a certain error.
129    pub fn misbehavior_score(&self) -> u32 {
130        0
131    }
132}
133
134/// An error describing why a `CommitSemanticallyVerified` request failed.
135#[derive(Debug, Error, Clone, PartialEq, Eq)]
136#[error("could not commit semantically-verified block")]
137pub struct CommitSemanticallyVerifiedError(#[from] CommitBlockError);
138
139impl CommitSemanticallyVerifiedError {
140    /// Returns the [`CommitBlockError`] describing why the commit failed.
141    pub fn inner(&self) -> &CommitBlockError {
142        &self.0
143    }
144}
145
146impl From<ValidateContextError> for CommitSemanticallyVerifiedError {
147    fn from(value: ValidateContextError) -> Self {
148        Self(CommitBlockError::ValidateContextError(Box::new(value)))
149    }
150}
151
152#[derive(Debug, Error)]
153pub enum LayeredStateError<E: std::error::Error + std::fmt::Display> {
154    #[error("{0}")]
155    State(E),
156    #[error("{0}")]
157    Layer(BoxError),
158}
159
160impl<E: std::error::Error + 'static> From<BoxError> for LayeredStateError<E> {
161    fn from(err: BoxError) -> Self {
162        match err.downcast::<E>() {
163            Ok(state_err) => Self::State(*state_err),
164            Err(layer_error) => Self::Layer(layer_error),
165        }
166    }
167}
168
169/// An error describing why a `CommitCheckpointVerifiedBlock` request failed.
170#[derive(Debug, Error, Clone)]
171#[error("could not commit checkpoint-verified block")]
172pub struct CommitCheckpointVerifiedError(#[from] CommitBlockError);
173
174impl CommitCheckpointVerifiedError {
175    /// Returns the [`CommitBlockError`] describing why the commit failed.
176    pub fn inner(&self) -> &CommitBlockError {
177        &self.0
178    }
179}
180
181impl From<ValidateContextError> for CommitCheckpointVerifiedError {
182    fn from(value: ValidateContextError) -> Self {
183        Self(CommitBlockError::ValidateContextError(Box::new(value)))
184    }
185}
186
187/// An error describing why a `InvalidateBlock` request failed.
188#[derive(Debug, Error)]
189#[non_exhaustive]
190pub enum InvalidateError {
191    /// The state is currently checkpointing blocks and cannot accept invalidation requests.
192    #[error("cannot invalidate blocks while still committing checkpointed blocks")]
193    ProcessingCheckpointedBlocks,
194
195    /// Sending the invalidate request to the block write task failed.
196    #[error("failed to send invalidate block request to block write task")]
197    SendInvalidateRequestFailed,
198
199    /// The invalidate request was dropped before processing.
200    #[error("invalidate block request was unexpectedly dropped")]
201    InvalidateRequestDropped,
202
203    /// The block hash was not found in any non-finalized chain.
204    #[error("block hash {0} not found in any non-finalized chain")]
205    BlockNotFound(block::Hash),
206}
207
208/// An error describing why a `ReconsiderBlock` request failed.
209#[derive(Debug, Error)]
210#[non_exhaustive]
211pub enum ReconsiderError {
212    /// The block is not found in the list of invalidated blocks.
213    #[error("Block with hash {0} was not previously invalidated")]
214    MissingInvalidatedBlock(block::Hash),
215
216    /// The block's parent is missing from the non-finalized state.
217    #[error("Parent chain not found for block {0}")]
218    ParentChainNotFound(block::Hash),
219
220    /// There were no invalidated blocks when at least one was expected.
221    #[error("Invalidated blocks list is empty when it should contain at least one block")]
222    InvalidatedBlocksEmpty,
223
224    /// The state is currently checkpointing blocks and cannot accept reconsider requests.
225    #[error("cannot reconsider blocks while still committing checkpointed blocks")]
226    CheckpointCommitInProgress,
227
228    /// Sending the reconsider request to the block write task failed.
229    #[error("failed to send reconsider block request to block write task")]
230    ReconsiderSendFailed,
231
232    /// The reconsider request was dropped before processing.
233    #[error("reconsider block request was unexpectedly dropped")]
234    ReconsiderResponseDropped,
235
236    /// Replaying an invalidated block into the restored chain failed contextual
237    /// validation.
238    #[error("replaying a previously invalidated block failed contextual validation: {0}")]
239    ReplayFailed(#[source] ValidateContextError),
240}
241
242/// An error describing why a block failed contextual validation.
243#[derive(Debug, Error, Clone, PartialEq, Eq)]
244#[non_exhaustive]
245#[allow(missing_docs)]
246pub enum ValidateContextError {
247    #[error("block hash {block_hash} was previously invalidated")]
248    #[non_exhaustive]
249    BlockPreviouslyInvalidated { block_hash: block::Hash },
250
251    #[error("block parent not found in any chain, or not enough blocks in chain")]
252    #[non_exhaustive]
253    NotReadyToBeCommitted,
254
255    #[error("block height {candidate_height:?} is lower than the current finalized height {finalized_tip_height:?}")]
256    #[non_exhaustive]
257    OrphanedBlock {
258        candidate_height: block::Height,
259        finalized_tip_height: block::Height,
260    },
261
262    #[error("block height {candidate_height:?} is not one greater than its parent block's height {parent_height:?}")]
263    #[non_exhaustive]
264    NonSequentialBlock {
265        candidate_height: block::Height,
266        parent_height: block::Height,
267    },
268
269    #[error("block time {candidate_time:?} is less than or equal to the median-time-past for the block {median_time_past:?}")]
270    #[non_exhaustive]
271    TimeTooEarly {
272        candidate_time: DateTime<Utc>,
273        median_time_past: DateTime<Utc>,
274    },
275
276    #[error("block time {candidate_time:?} is greater than the median-time-past for the block plus 90 minutes {block_time_max:?}")]
277    #[non_exhaustive]
278    TimeTooLate {
279        candidate_time: DateTime<Utc>,
280        block_time_max: DateTime<Utc>,
281    },
282
283    #[error("block difficulty threshold {difficulty_threshold:?} is not equal to the expected difficulty for the block {expected_difficulty:?}")]
284    #[non_exhaustive]
285    InvalidDifficultyThreshold {
286        difficulty_threshold: CompactDifficulty,
287        expected_difficulty: CompactDifficulty,
288    },
289
290    #[error("transparent double-spend: {outpoint:?} is spent twice in {location:?}")]
291    #[non_exhaustive]
292    DuplicateTransparentSpend {
293        outpoint: transparent::OutPoint,
294        location: &'static str,
295    },
296
297    #[error("missing transparent output: possible double-spend of {outpoint:?} in {location:?}")]
298    #[non_exhaustive]
299    MissingTransparentOutput {
300        outpoint: transparent::OutPoint,
301        location: &'static str,
302    },
303
304    #[error("out-of-order transparent spend: {outpoint:?} is created by a later transaction in the same block")]
305    #[non_exhaustive]
306    EarlyTransparentSpend { outpoint: transparent::OutPoint },
307
308    #[error(
309        "unshielded transparent coinbase spend: {outpoint:?} \
310         must be spent in a transaction which only has shielded outputs"
311    )]
312    #[non_exhaustive]
313    UnshieldedTransparentCoinbaseSpend { outpoint: transparent::OutPoint },
314
315    #[error(
316        "immature transparent coinbase spend: \
317        attempt to spend {outpoint:?} at {spend_height:?}, \
318        but spends are invalid before {min_spend_height:?}, \
319        which is {MIN_TRANSPARENT_COINBASE_MATURITY:?} blocks \
320        after it was created at {created_height:?}"
321    )]
322    #[non_exhaustive]
323    ImmatureTransparentCoinbaseSpend {
324        outpoint: transparent::OutPoint,
325        spend_height: block::Height,
326        min_spend_height: block::Height,
327        created_height: block::Height,
328    },
329
330    #[error("sprout double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
331    #[non_exhaustive]
332    DuplicateSproutNullifier {
333        nullifier: sprout::Nullifier,
334        in_finalized_state: bool,
335    },
336
337    #[error("sapling double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
338    #[non_exhaustive]
339    DuplicateSaplingNullifier {
340        nullifier: sapling::Nullifier,
341        in_finalized_state: bool,
342    },
343
344    #[error("orchard double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
345    #[non_exhaustive]
346    DuplicateOrchardNullifier {
347        nullifier: orchard::Nullifier,
348        in_finalized_state: bool,
349    },
350
351    #[error("ironwood double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
352    #[non_exhaustive]
353    DuplicateIronwoodNullifier {
354        nullifier: ironwood::Nullifier,
355        in_finalized_state: bool,
356    },
357
358    #[error(
359        "the remaining value in the transparent transaction value pool MUST be nonnegative:\n\
360         {amount_error:?},\n\
361         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
362    )]
363    #[non_exhaustive]
364    NegativeRemainingTransactionValue {
365        amount_error: amount::Error,
366        height: block::Height,
367        tx_index_in_block: usize,
368        transaction_hash: transaction::Hash,
369    },
370
371    #[error(
372        "error calculating the remaining value in the transaction value pool:\n\
373         {amount_error:?},\n\
374         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
375    )]
376    #[non_exhaustive]
377    CalculateRemainingTransactionValue {
378        amount_error: amount::Error,
379        height: block::Height,
380        tx_index_in_block: usize,
381        transaction_hash: transaction::Hash,
382    },
383
384    #[error(
385        "error calculating value balances for the remaining value in the transaction value pool:\n\
386         {value_balance_error:?},\n\
387         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
388    )]
389    #[non_exhaustive]
390    CalculateTransactionValueBalances {
391        value_balance_error: ValueBalanceError,
392        height: block::Height,
393        tx_index_in_block: usize,
394        transaction_hash: transaction::Hash,
395    },
396
397    #[error(
398        "error calculating the block chain value pool change:\n\
399         {value_balance_error:?},\n\
400         {height:?}, {block_hash:?},\n\
401         transactions: {transaction_count:?}, spent UTXOs: {spent_utxo_count:?}"
402    )]
403    #[non_exhaustive]
404    CalculateBlockChainValueChange {
405        value_balance_error: ValueBalanceError,
406        height: block::Height,
407        block_hash: block::Hash,
408        transaction_count: usize,
409        spent_utxo_count: usize,
410    },
411
412    #[error(
413        "error adding value balances to the chain value pool:\n\
414         {value_balance_error:?},\n\
415         {chain_value_pools:?},\n\
416         {block_value_pool_change:?},\n\
417         {height:?}"
418    )]
419    #[non_exhaustive]
420    AddValuePool {
421        value_balance_error: ValueBalanceError,
422        chain_value_pools: Box<ValueBalance<NonNegative>>,
423        block_value_pool_change: Box<ValueBalance<NegativeAllowed>>,
424        height: Option<block::Height>,
425    },
426
427    #[error("error updating a note commitment tree: {0}")]
428    NoteCommitmentTreeError(#[from] zebra_chain::parallel::tree::NoteCommitmentTreeError),
429
430    #[error("error building the history tree: {0}")]
431    HistoryTreeError(#[from] Arc<HistoryTreeError>),
432
433    #[error("block contains an invalid commitment: {0}")]
434    InvalidBlockCommitment(#[from] block::CommitmentError),
435
436    #[error(
437        "unknown Sprout anchor: {anchor:?},\n\
438         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
439    )]
440    #[non_exhaustive]
441    UnknownSproutAnchor {
442        anchor: sprout::tree::Root,
443        height: Option<block::Height>,
444        tx_index_in_block: Option<usize>,
445        transaction_hash: transaction::Hash,
446    },
447
448    #[error(
449        "unknown Sapling anchor: {anchor:?},\n\
450         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
451    )]
452    #[non_exhaustive]
453    UnknownSaplingAnchor {
454        anchor: sapling::tree::Root,
455        height: Option<block::Height>,
456        tx_index_in_block: Option<usize>,
457        transaction_hash: transaction::Hash,
458    },
459
460    #[error(
461        "unknown Orchard anchor: {anchor:?},\n\
462         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
463    )]
464    #[non_exhaustive]
465    UnknownOrchardAnchor {
466        anchor: orchard::tree::Root,
467        height: Option<block::Height>,
468        tx_index_in_block: Option<usize>,
469        transaction_hash: transaction::Hash,
470    },
471
472    #[error(
473        "unknown Ironwood anchor: {anchor:?},\n\
474         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
475    )]
476    #[non_exhaustive]
477    UnknownIronwoodAnchor {
478        // Ironwood reuses the Orchard tree root type.
479        anchor: orchard::tree::Root,
480        height: Option<block::Height>,
481        tx_index_in_block: Option<usize>,
482        transaction_hash: transaction::Hash,
483    },
484}
485
486impl From<sprout::tree::NoteCommitmentTreeError> for ValidateContextError {
487    fn from(value: sprout::tree::NoteCommitmentTreeError) -> Self {
488        ValidateContextError::NoteCommitmentTreeError(value.into())
489    }
490}
491
492/// Trait for creating the corresponding duplicate nullifier error from a nullifier.
493pub trait DuplicateNullifierError {
494    /// Returns the corresponding duplicate nullifier error for `self`.
495    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError;
496}
497
498impl DuplicateNullifierError for sprout::Nullifier {
499    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
500        ValidateContextError::DuplicateSproutNullifier {
501            nullifier: *self,
502            in_finalized_state,
503        }
504    }
505}
506
507impl DuplicateNullifierError for sapling::Nullifier {
508    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
509        ValidateContextError::DuplicateSaplingNullifier {
510            nullifier: *self,
511            in_finalized_state,
512        }
513    }
514}
515
516impl DuplicateNullifierError for orchard::Nullifier {
517    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
518        ValidateContextError::DuplicateOrchardNullifier {
519            nullifier: *self,
520            in_finalized_state,
521        }
522    }
523}
524
525impl DuplicateNullifierError for ironwood::Nullifier {
526    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
527        ValidateContextError::DuplicateIronwoodNullifier {
528            nullifier: *self,
529            in_finalized_state,
530        }
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537    use zebra_chain::block::Height;
538
539    #[test]
540    fn commit_block_error_misbehavior_scores() {
541        let context_err = CommitBlockError::ValidateContextError(Box::new(
542            ValidateContextError::NonSequentialBlock {
543                candidate_height: Height(5),
544                parent_height: Height(3),
545            },
546        ));
547        assert_eq!(context_err.misbehavior_score(), 0);
548
549        let dup_err = CommitBlockError::Duplicate {
550            hash_or_height: None,
551            location: KnownBlock::BestChain,
552        };
553        assert_eq!(dup_err.misbehavior_score(), 0);
554    }
555}