Skip to main content

zebra_state/
error.rs

1//! Error types for Zebra's state.
2
3use std::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    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 a block could not be queued to be committed to the state.
46#[derive(Debug, Error, Clone, PartialEq, Eq, new)]
47pub enum CommitBlockError {
48    #[error("block hash is a duplicate: already in {location}")]
49    /// The block is a duplicate: it is already queued or committed in the state.
50    Duplicate {
51        /// Hash or height of the duplicated block.
52        hash_or_height: Option<HashOrHeight>,
53        /// Location in the state where the block can be found.
54        location: KnownBlock,
55    },
56
57    /// Contextual validation failed.
58    #[error("could not contextually validate semantically verified block")]
59    ValidateContextError(#[from] Box<ValidateContextError>),
60
61    /// The write task exited (likely during shutdown).
62    #[error("block commit task exited. Is Zebra shutting down?")]
63    #[non_exhaustive]
64    WriteTaskExited,
65}
66
67impl CommitBlockError {
68    /// Returns `true` if this is definitely a duplicate commit request.
69    /// Some duplicate requests might not be detected, and therefore return `false`.
70    pub fn is_duplicate_request(&self) -> bool {
71        matches!(self, CommitBlockError::Duplicate { .. })
72    }
73
74    /// Returns a suggested misbehaviour score increment for a certain error.
75    pub fn misbehavior_score(&self) -> u32 {
76        0
77    }
78}
79
80/// An error describing why a `CommitSemanticallyVerified` request failed.
81#[derive(Debug, Error, Clone, PartialEq, Eq)]
82#[error("could not commit semantically-verified block")]
83pub struct CommitSemanticallyVerifiedError(#[from] CommitBlockError);
84
85impl From<ValidateContextError> for CommitSemanticallyVerifiedError {
86    fn from(value: ValidateContextError) -> Self {
87        Self(CommitBlockError::ValidateContextError(Box::new(value)))
88    }
89}
90
91#[derive(Debug, Error)]
92pub enum LayeredStateError<E: std::error::Error + std::fmt::Display> {
93    #[error("{0}")]
94    State(E),
95    #[error("{0}")]
96    Layer(BoxError),
97}
98
99impl<E: std::error::Error + 'static> From<BoxError> for LayeredStateError<E> {
100    fn from(err: BoxError) -> Self {
101        match err.downcast::<E>() {
102            Ok(state_err) => Self::State(*state_err),
103            Err(layer_error) => Self::Layer(layer_error),
104        }
105    }
106}
107
108/// An error describing why a `CommitCheckpointVerifiedBlock` request failed.
109#[derive(Debug, Error, Clone)]
110#[error("could not commit checkpoint-verified block")]
111pub struct CommitCheckpointVerifiedError(#[from] CommitBlockError);
112
113impl From<ValidateContextError> for CommitCheckpointVerifiedError {
114    fn from(value: ValidateContextError) -> Self {
115        Self(CommitBlockError::ValidateContextError(Box::new(value)))
116    }
117}
118
119/// An error describing why a `InvalidateBlock` request failed.
120#[derive(Debug, Error)]
121#[non_exhaustive]
122pub enum InvalidateError {
123    /// The state is currently checkpointing blocks and cannot accept invalidation requests.
124    #[error("cannot invalidate blocks while still committing checkpointed blocks")]
125    ProcessingCheckpointedBlocks,
126
127    /// Sending the invalidate request to the block write task failed.
128    #[error("failed to send invalidate block request to block write task")]
129    SendInvalidateRequestFailed,
130
131    /// The invalidate request was dropped before processing.
132    #[error("invalidate block request was unexpectedly dropped")]
133    InvalidateRequestDropped,
134
135    /// The block hash was not found in any non-finalized chain.
136    #[error("block hash {0} not found in any non-finalized chain")]
137    BlockNotFound(block::Hash),
138}
139
140/// An error describing why a `ReconsiderBlock` request failed.
141#[derive(Debug, Error)]
142#[non_exhaustive]
143pub enum ReconsiderError {
144    /// The block is not found in the list of invalidated blocks.
145    #[error("Block with hash {0} was not previously invalidated")]
146    MissingInvalidatedBlock(block::Hash),
147
148    /// The block's parent is missing from the non-finalized state.
149    #[error("Parent chain not found for block {0}")]
150    ParentChainNotFound(block::Hash),
151
152    /// There were no invalidated blocks when at least one was expected.
153    #[error("Invalidated blocks list is empty when it should contain at least one block")]
154    InvalidatedBlocksEmpty,
155
156    /// The state is currently checkpointing blocks and cannot accept reconsider requests.
157    #[error("cannot reconsider blocks while still committing checkpointed blocks")]
158    CheckpointCommitInProgress,
159
160    /// Sending the reconsider request to the block write task failed.
161    #[error("failed to send reconsider block request to block write task")]
162    ReconsiderSendFailed,
163
164    /// The reconsider request was dropped before processing.
165    #[error("reconsider block request was unexpectedly dropped")]
166    ReconsiderResponseDropped,
167}
168
169/// An error describing why a block failed contextual validation.
170#[derive(Debug, Error, Clone, PartialEq, Eq)]
171#[non_exhaustive]
172#[allow(missing_docs)]
173pub enum ValidateContextError {
174    #[error("block hash {block_hash} was previously invalidated")]
175    #[non_exhaustive]
176    BlockPreviouslyInvalidated { block_hash: block::Hash },
177
178    #[error("block parent not found in any chain, or not enough blocks in chain")]
179    #[non_exhaustive]
180    NotReadyToBeCommitted,
181
182    #[error("block height {candidate_height:?} is lower than the current finalized height {finalized_tip_height:?}")]
183    #[non_exhaustive]
184    OrphanedBlock {
185        candidate_height: block::Height,
186        finalized_tip_height: block::Height,
187    },
188
189    #[error("block height {candidate_height:?} is not one greater than its parent block's height {parent_height:?}")]
190    #[non_exhaustive]
191    NonSequentialBlock {
192        candidate_height: block::Height,
193        parent_height: block::Height,
194    },
195
196    #[error("block time {candidate_time:?} is less than or equal to the median-time-past for the block {median_time_past:?}")]
197    #[non_exhaustive]
198    TimeTooEarly {
199        candidate_time: DateTime<Utc>,
200        median_time_past: DateTime<Utc>,
201    },
202
203    #[error("block time {candidate_time:?} is greater than the median-time-past for the block plus 90 minutes {block_time_max:?}")]
204    #[non_exhaustive]
205    TimeTooLate {
206        candidate_time: DateTime<Utc>,
207        block_time_max: DateTime<Utc>,
208    },
209
210    #[error("block difficulty threshold {difficulty_threshold:?} is not equal to the expected difficulty for the block {expected_difficulty:?}")]
211    #[non_exhaustive]
212    InvalidDifficultyThreshold {
213        difficulty_threshold: CompactDifficulty,
214        expected_difficulty: CompactDifficulty,
215    },
216
217    #[error("transparent double-spend: {outpoint:?} is spent twice in {location:?}")]
218    #[non_exhaustive]
219    DuplicateTransparentSpend {
220        outpoint: transparent::OutPoint,
221        location: &'static str,
222    },
223
224    #[error("missing transparent output: possible double-spend of {outpoint:?} in {location:?}")]
225    #[non_exhaustive]
226    MissingTransparentOutput {
227        outpoint: transparent::OutPoint,
228        location: &'static str,
229    },
230
231    #[error("out-of-order transparent spend: {outpoint:?} is created by a later transaction in the same block")]
232    #[non_exhaustive]
233    EarlyTransparentSpend { outpoint: transparent::OutPoint },
234
235    #[error(
236        "unshielded transparent coinbase spend: {outpoint:?} \
237         must be spent in a transaction which only has shielded outputs"
238    )]
239    #[non_exhaustive]
240    UnshieldedTransparentCoinbaseSpend { outpoint: transparent::OutPoint },
241
242    #[error(
243        "immature transparent coinbase spend: \
244        attempt to spend {outpoint:?} at {spend_height:?}, \
245        but spends are invalid before {min_spend_height:?}, \
246        which is {MIN_TRANSPARENT_COINBASE_MATURITY:?} blocks \
247        after it was created at {created_height:?}"
248    )]
249    #[non_exhaustive]
250    ImmatureTransparentCoinbaseSpend {
251        outpoint: transparent::OutPoint,
252        spend_height: block::Height,
253        min_spend_height: block::Height,
254        created_height: block::Height,
255    },
256
257    #[error("sprout double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
258    #[non_exhaustive]
259    DuplicateSproutNullifier {
260        nullifier: sprout::Nullifier,
261        in_finalized_state: bool,
262    },
263
264    #[error("sapling double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
265    #[non_exhaustive]
266    DuplicateSaplingNullifier {
267        nullifier: sapling::Nullifier,
268        in_finalized_state: bool,
269    },
270
271    #[error("orchard double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
272    #[non_exhaustive]
273    DuplicateOrchardNullifier {
274        nullifier: orchard::Nullifier,
275        in_finalized_state: bool,
276    },
277
278    #[error(
279        "the remaining value in the transparent transaction value pool MUST be nonnegative:\n\
280         {amount_error:?},\n\
281         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
282    )]
283    #[non_exhaustive]
284    NegativeRemainingTransactionValue {
285        amount_error: amount::Error,
286        height: block::Height,
287        tx_index_in_block: usize,
288        transaction_hash: transaction::Hash,
289    },
290
291    #[error(
292        "error calculating the remaining value in the transaction value pool:\n\
293         {amount_error:?},\n\
294         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
295    )]
296    #[non_exhaustive]
297    CalculateRemainingTransactionValue {
298        amount_error: amount::Error,
299        height: block::Height,
300        tx_index_in_block: usize,
301        transaction_hash: transaction::Hash,
302    },
303
304    #[error(
305        "error calculating value balances for the remaining value in the transaction value pool:\n\
306         {value_balance_error:?},\n\
307         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
308    )]
309    #[non_exhaustive]
310    CalculateTransactionValueBalances {
311        value_balance_error: ValueBalanceError,
312        height: block::Height,
313        tx_index_in_block: usize,
314        transaction_hash: transaction::Hash,
315    },
316
317    #[error(
318        "error calculating the block chain value pool change:\n\
319         {value_balance_error:?},\n\
320         {height:?}, {block_hash:?},\n\
321         transactions: {transaction_count:?}, spent UTXOs: {spent_utxo_count:?}"
322    )]
323    #[non_exhaustive]
324    CalculateBlockChainValueChange {
325        value_balance_error: ValueBalanceError,
326        height: block::Height,
327        block_hash: block::Hash,
328        transaction_count: usize,
329        spent_utxo_count: usize,
330    },
331
332    #[error(
333        "error adding value balances to the chain value pool:\n\
334         {value_balance_error:?},\n\
335         {chain_value_pools:?},\n\
336         {block_value_pool_change:?},\n\
337         {height:?}"
338    )]
339    #[non_exhaustive]
340    AddValuePool {
341        value_balance_error: ValueBalanceError,
342        chain_value_pools: Box<ValueBalance<NonNegative>>,
343        block_value_pool_change: Box<ValueBalance<NegativeAllowed>>,
344        height: Option<block::Height>,
345    },
346
347    #[error("error updating a note commitment tree: {0}")]
348    NoteCommitmentTreeError(#[from] zebra_chain::parallel::tree::NoteCommitmentTreeError),
349
350    #[error("error building the history tree: {0}")]
351    HistoryTreeError(#[from] Arc<HistoryTreeError>),
352
353    #[error("block contains an invalid commitment: {0}")]
354    InvalidBlockCommitment(#[from] block::CommitmentError),
355
356    #[error(
357        "unknown Sprout anchor: {anchor:?},\n\
358         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
359    )]
360    #[non_exhaustive]
361    UnknownSproutAnchor {
362        anchor: sprout::tree::Root,
363        height: Option<block::Height>,
364        tx_index_in_block: Option<usize>,
365        transaction_hash: transaction::Hash,
366    },
367
368    #[error(
369        "unknown Sapling anchor: {anchor:?},\n\
370         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
371    )]
372    #[non_exhaustive]
373    UnknownSaplingAnchor {
374        anchor: sapling::tree::Root,
375        height: Option<block::Height>,
376        tx_index_in_block: Option<usize>,
377        transaction_hash: transaction::Hash,
378    },
379
380    #[error(
381        "unknown Orchard anchor: {anchor:?},\n\
382         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
383    )]
384    #[non_exhaustive]
385    UnknownOrchardAnchor {
386        anchor: orchard::tree::Root,
387        height: Option<block::Height>,
388        tx_index_in_block: Option<usize>,
389        transaction_hash: transaction::Hash,
390    },
391}
392
393impl From<sprout::tree::NoteCommitmentTreeError> for ValidateContextError {
394    fn from(value: sprout::tree::NoteCommitmentTreeError) -> Self {
395        ValidateContextError::NoteCommitmentTreeError(value.into())
396    }
397}
398
399/// Trait for creating the corresponding duplicate nullifier error from a nullifier.
400pub trait DuplicateNullifierError {
401    /// Returns the corresponding duplicate nullifier error for `self`.
402    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError;
403}
404
405impl DuplicateNullifierError for sprout::Nullifier {
406    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
407        ValidateContextError::DuplicateSproutNullifier {
408            nullifier: *self,
409            in_finalized_state,
410        }
411    }
412}
413
414impl DuplicateNullifierError for sapling::Nullifier {
415    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
416        ValidateContextError::DuplicateSaplingNullifier {
417            nullifier: *self,
418            in_finalized_state,
419        }
420    }
421}
422
423impl DuplicateNullifierError for orchard::Nullifier {
424    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
425        ValidateContextError::DuplicateOrchardNullifier {
426            nullifier: *self,
427            in_finalized_state,
428        }
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use zebra_chain::block::Height;
436
437    #[test]
438    fn commit_block_error_misbehavior_scores() {
439        let context_err = CommitBlockError::ValidateContextError(Box::new(
440            ValidateContextError::NonSequentialBlock {
441                candidate_height: Height(5),
442                parent_height: Height(3),
443            },
444        ));
445        assert_eq!(context_err.misbehavior_score(), 0);
446
447        let dup_err = CommitBlockError::Duplicate {
448            hash_or_height: None,
449            location: KnownBlock::BestChain,
450        };
451        assert_eq!(dup_err.misbehavior_score(), 0);
452    }
453}