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 tokio::sync::broadcast::error::RecvError;
10use zebra_chain::{
11    amount::{self, NegativeAllowed, NonNegative},
12    block,
13    history_tree::HistoryTreeError,
14    ironwood, orchard, sapling, sprout, transaction, transparent,
15    value_balance::{ValueBalance, ValueBalanceError},
16    work::difficulty::CompactDifficulty,
17};
18
19use crate::{constants::MIN_TRANSPARENT_COINBASE_MATURITY, HashOrHeight, KnownBlock};
20
21/// A wrapper for type erased errors that is itself clonable and implements the
22/// Error trait
23#[derive(Debug, Error, Clone)]
24#[error(transparent)]
25pub struct CloneError {
26    source: Arc<dyn std::error::Error + Send + Sync + 'static>,
27}
28
29impl From<CommitSemanticallyVerifiedError> for CloneError {
30    fn from(source: CommitSemanticallyVerifiedError) -> Self {
31        let source = Arc::new(source);
32        Self { source }
33    }
34}
35
36impl From<BoxError> for CloneError {
37    fn from(source: BoxError) -> Self {
38        let source = Arc::from(source);
39        Self { source }
40    }
41}
42
43/// A boxed [`std::error::Error`].
44pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
45
46/// An error describing why opening the finalized state database failed.
47///
48/// These errors are recoverable open-time failures that the caller can report,
49/// as opposed to invariant violations that indicate a bug.
50#[derive(Debug, Error)]
51#[non_exhaustive]
52pub enum StateInitError {
53    /// A read-only state was requested, but the configured cache directory is
54    /// missing or unreadable.
55    ///
56    /// A read-only secondary instance must never create the primary's cache
57    /// directory, so a missing or unreadable directory is a fatal configuration
58    /// error rather than something to be created.
59    #[error(
60        "cannot open read-only state: cache directory {path:?} is missing or unreadable. \
61         Hint: a read-only state requires an existing Zebra cache directory; check that the \
62         state cache_dir in the Zebra config points at a running Zebra node's cache directory"
63    )]
64    ReadOnlyCacheDirUnreadable {
65        /// The configured cache directory that could not be read.
66        path: PathBuf,
67        /// The underlying I/O error returned while reading the directory.
68        source: std::io::Error,
69    },
70
71    /// A read-only state was requested, but no database exists at the expected
72    /// path.
73    ///
74    /// A read-only secondary instance cannot create a database, so the absence
75    /// of an existing database is a fatal configuration error.
76    #[error(
77        "cannot open read-only state: no database found at {path:?}. \
78         Hint: a read-only state requires an existing finalized database created by a running \
79         Zebra node; check that the state cache_dir in the Zebra config points at that node's \
80         cache directory"
81    )]
82    ReadOnlyDatabaseNotFound {
83        /// The database path at which no database was found.
84        path: PathBuf,
85    },
86
87    /// A read-only state was requested together with an ephemeral database.
88    ///
89    /// A read-only secondary follows another process's primary database and must
90    /// never delete it, whereas an ephemeral database deletes its files on drop. The
91    /// two are mutually exclusive, so requesting both is a fatal configuration error.
92    #[error(
93        "cannot open read-only state: an ephemeral database was also requested. \
94         Hint: a read-only state follows an existing Zebra node's database and must not \
95         delete it; set `ephemeral = false`, or do not request a read-only state"
96    )]
97    ReadOnlyEphemeralConflict,
98}
99
100/// An error describing why a block could not be queued to be committed to the state.
101#[derive(Debug, Error, Clone, PartialEq, Eq, new)]
102pub enum CommitBlockError {
103    #[error("block hash is a duplicate: already in {location}")]
104    /// The block is a duplicate: it is already queued or committed in the state.
105    Duplicate {
106        /// Hash or height of the duplicated block.
107        hash_or_height: Option<HashOrHeight>,
108        /// Location in the state where the block can be found.
109        location: KnownBlock,
110    },
111
112    /// Contextual validation failed.
113    #[error("could not contextually validate semantically verified block")]
114    ValidateContextError(#[from] Box<ValidateContextError>),
115
116    /// The write task exited (likely during shutdown).
117    #[error("block commit task exited. Is Zebra shutting down?")]
118    #[non_exhaustive]
119    WriteTaskExited,
120}
121
122impl CommitBlockError {
123    /// Returns `true` if this is definitely a duplicate commit request.
124    /// Some duplicate requests might not be detected, and therefore return `false`.
125    pub fn is_duplicate_request(&self) -> bool {
126        matches!(self, CommitBlockError::Duplicate { .. })
127    }
128
129    /// Returns `true` if the block's authorizing data doesn't match the commitment in
130    /// its header.
131    ///
132    /// See [`ValidateContextError::is_auth_commitment_mismatch()`] for why this is
133    /// tracked separately from the misbehaviour score: the served body is invalid, but
134    /// the block hash is still valid and still wanted, so the syncer re-requests it
135    /// instead of restarting the sync round.
136    pub fn is_auth_commitment_mismatch(&self) -> bool {
137        match self {
138            CommitBlockError::ValidateContextError(err) => err.is_auth_commitment_mismatch(),
139            CommitBlockError::Duplicate { .. } | CommitBlockError::WriteTaskExited => false,
140        }
141    }
142
143    /// Returns `true` if the block was rejected only because an ancestor's authorizing
144    /// data didn't match the commitment in its header.
145    ///
146    /// See [`ValidateContextError::is_descendant_of_auth_commitment_mismatch()`].
147    pub fn is_descendant_of_auth_commitment_mismatch(&self) -> bool {
148        match self {
149            CommitBlockError::ValidateContextError(err) => {
150                err.is_descendant_of_auth_commitment_mismatch()
151            }
152            CommitBlockError::Duplicate { .. } | CommitBlockError::WriteTaskExited => false,
153        }
154    }
155
156    /// Returns a suggested misbehaviour score increment for a certain error.
157    ///
158    /// Consensus-invalid subsidies and authorizing-data commitments score 100.
159    /// Duplicate requests, shutdowns, and rejected-ancestor descendants score 0.
160    pub fn misbehavior_score(&self) -> u32 {
161        match self {
162            CommitBlockError::ValidateContextError(err) => err.misbehavior_score(),
163            CommitBlockError::Duplicate { .. } | CommitBlockError::WriteTaskExited => 0,
164        }
165    }
166}
167
168/// An error describing why a `CommitSemanticallyVerified` request failed.
169#[derive(Debug, Error, Clone, PartialEq, Eq)]
170#[error("could not commit semantically-verified block")]
171pub struct CommitSemanticallyVerifiedError(#[from] CommitBlockError);
172
173impl CommitSemanticallyVerifiedError {
174    /// Returns the [`CommitBlockError`] describing why the commit failed.
175    pub fn inner(&self) -> &CommitBlockError {
176        &self.0
177    }
178}
179
180impl From<ValidateContextError> for CommitSemanticallyVerifiedError {
181    fn from(value: ValidateContextError) -> Self {
182        Self(CommitBlockError::ValidateContextError(Box::new(value)))
183    }
184}
185
186#[derive(Debug, Error)]
187pub enum LayeredStateError<E: std::error::Error + std::fmt::Display> {
188    #[error("{0}")]
189    State(E),
190    #[error("{0}")]
191    Layer(BoxError),
192}
193
194impl<E: std::error::Error + 'static> From<BoxError> for LayeredStateError<E> {
195    fn from(err: BoxError) -> Self {
196        match err.downcast::<E>() {
197            Ok(state_err) => Self::State(*state_err),
198            Err(layer_error) => Self::Layer(layer_error),
199        }
200    }
201}
202
203/// An error describing why a `CommitCheckpointVerifiedBlock` request failed.
204#[derive(Debug, Error, Clone)]
205#[error("could not commit checkpoint-verified block")]
206pub struct CommitCheckpointVerifiedError(#[from] CommitBlockError);
207
208impl CommitCheckpointVerifiedError {
209    /// Returns the [`CommitBlockError`] describing why the commit failed.
210    pub fn inner(&self) -> &CommitBlockError {
211        &self.0
212    }
213}
214
215impl From<ValidateContextError> for CommitCheckpointVerifiedError {
216    fn from(value: ValidateContextError) -> Self {
217        Self(CommitBlockError::ValidateContextError(Box::new(value)))
218    }
219}
220
221/// An error describing why a `InvalidateBlock` request failed.
222#[derive(Debug, Error)]
223#[non_exhaustive]
224pub enum InvalidateError {
225    /// The state is currently checkpointing blocks and cannot accept invalidation requests.
226    #[error("cannot invalidate blocks while still committing checkpointed blocks")]
227    ProcessingCheckpointedBlocks,
228
229    /// Sending the invalidate request to the block write task failed.
230    #[error("failed to send invalidate block request to block write task")]
231    SendInvalidateRequestFailed,
232
233    /// The invalidate request was dropped before processing.
234    #[error("invalidate block request was unexpectedly dropped")]
235    InvalidateRequestDropped,
236
237    /// The block hash was not found in any non-finalized chain.
238    #[error("block hash {0} not found in any non-finalized chain")]
239    BlockNotFound(block::Hash),
240
241    /// Invalidating this root would remove the only non-finalized chain.
242    #[error("cannot invalidate block {0}: it is the root of the only non-finalized chain")]
243    OnlyChainRoot(block::Hash),
244}
245
246/// An error describing why a `ReconsiderBlock` request failed.
247#[derive(Debug, Error)]
248#[non_exhaustive]
249pub enum ReconsiderError {
250    /// The block is not found in the list of invalidated blocks.
251    #[error("Block with hash {0} was not previously invalidated")]
252    MissingInvalidatedBlock(block::Hash),
253
254    /// The block's parent is missing from the non-finalized state.
255    #[error("Parent chain not found for block {0}")]
256    ParentChainNotFound(block::Hash),
257
258    /// There were no invalidated blocks when at least one was expected.
259    #[error("Invalidated blocks list is empty when it should contain at least one block")]
260    InvalidatedBlocksEmpty,
261
262    /// The state is currently checkpointing blocks and cannot accept reconsider requests.
263    #[error("cannot reconsider blocks while still committing checkpointed blocks")]
264    CheckpointCommitInProgress,
265
266    /// Sending the reconsider request to the block write task failed.
267    #[error("failed to send reconsider block request to block write task")]
268    ReconsiderSendFailed,
269
270    /// The reconsider request was dropped before processing.
271    #[error("reconsider block request was unexpectedly dropped")]
272    ReconsiderResponseDropped,
273
274    /// Replaying an invalidated block into the restored chain failed contextual
275    /// validation.
276    #[error("replaying a previously invalidated block failed contextual validation: {0}")]
277    ReplayFailed(#[source] ValidateContextError),
278}
279
280/// An error describing why an `AwaitUtxo` request failed.
281#[derive(Debug, Error)]
282#[non_exhaustive]
283pub enum AwaitUtxoError {
284    /// The state stopped responding, for example because it was dropped
285    /// during shutdown, before the UTXO could be found.
286    #[error("the state stopped responding before the UTXO arrived")]
287    Cancelled,
288
289    /// An internal channel error occurred while waiting for the UTXO.
290    #[error("internal channel error while waiting for the UTXO: receiver lagged by {0}")]
291    Lagged(u64),
292
293    /// The `ReadStateService` returned an error while looking up the UTXO.
294    #[error("the read state service returned an error: {0}")]
295    ReadStateFailed(#[source] BoxError),
296}
297
298impl From<RecvError> for AwaitUtxoError {
299    fn from(err: RecvError) -> Self {
300        match err {
301            RecvError::Closed => AwaitUtxoError::Cancelled,
302            RecvError::Lagged(skipped) => AwaitUtxoError::Lagged(skipped),
303        }
304    }
305}
306
307/// An error describing why a block failed contextual validation.
308#[derive(Debug, Error, Clone, PartialEq, Eq)]
309#[non_exhaustive]
310#[allow(missing_docs)]
311pub enum ValidateContextError {
312    #[error("block hash {block_hash} was previously invalidated")]
313    #[non_exhaustive]
314    BlockPreviouslyInvalidated { block_hash: block::Hash },
315
316    #[error("block parent not found in any chain, or not enough blocks in chain")]
317    #[non_exhaustive]
318    NotReadyToBeCommitted,
319
320    #[error("block height {candidate_height:?} is lower than the current finalized height {finalized_tip_height:?}")]
321    #[non_exhaustive]
322    OrphanedBlock {
323        candidate_height: block::Height,
324        finalized_tip_height: block::Height,
325    },
326
327    #[error("block height {candidate_height:?} is not one greater than its parent block's height {parent_height:?}")]
328    #[non_exhaustive]
329    NonSequentialBlock {
330        candidate_height: block::Height,
331        parent_height: block::Height,
332    },
333
334    #[error("block time {candidate_time:?} is less than or equal to the median-time-past for the block {median_time_past:?}")]
335    #[non_exhaustive]
336    TimeTooEarly {
337        candidate_time: DateTime<Utc>,
338        median_time_past: DateTime<Utc>,
339    },
340
341    #[error("block time {candidate_time:?} is greater than the median-time-past for the block plus 90 minutes {block_time_max:?}")]
342    #[non_exhaustive]
343    TimeTooLate {
344        candidate_time: DateTime<Utc>,
345        block_time_max: DateTime<Utc>,
346    },
347
348    #[error("block difficulty threshold {difficulty_threshold:?} is not equal to the expected difficulty for the block {expected_difficulty:?}")]
349    #[non_exhaustive]
350    InvalidDifficultyThreshold {
351        difficulty_threshold: CompactDifficulty,
352        expected_difficulty: CompactDifficulty,
353    },
354
355    #[error("transparent double-spend: {outpoint:?} is spent twice in {location:?}")]
356    #[non_exhaustive]
357    DuplicateTransparentSpend {
358        outpoint: transparent::OutPoint,
359        location: &'static str,
360    },
361
362    #[error("missing transparent output: possible double-spend of {outpoint:?} in {location:?}")]
363    #[non_exhaustive]
364    MissingTransparentOutput {
365        outpoint: transparent::OutPoint,
366        location: &'static str,
367    },
368
369    #[error("out-of-order transparent spend: {outpoint:?} is created by a later transaction in the same block")]
370    #[non_exhaustive]
371    EarlyTransparentSpend { outpoint: transparent::OutPoint },
372
373    #[error(
374        "unshielded transparent coinbase spend: {outpoint:?} \
375         must be spent in a transaction which only has shielded outputs"
376    )]
377    #[non_exhaustive]
378    UnshieldedTransparentCoinbaseSpend { outpoint: transparent::OutPoint },
379
380    #[error(
381        "immature transparent coinbase spend: \
382        attempt to spend {outpoint:?} at {spend_height:?}, \
383        but spends are invalid before {min_spend_height:?}, \
384        which is {MIN_TRANSPARENT_COINBASE_MATURITY:?} blocks \
385        after it was created at {created_height:?}"
386    )]
387    #[non_exhaustive]
388    ImmatureTransparentCoinbaseSpend {
389        outpoint: transparent::OutPoint,
390        spend_height: block::Height,
391        min_spend_height: block::Height,
392        created_height: block::Height,
393    },
394
395    #[error("sprout double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
396    #[non_exhaustive]
397    DuplicateSproutNullifier {
398        nullifier: sprout::Nullifier,
399        in_finalized_state: bool,
400    },
401
402    #[error("sapling double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
403    #[non_exhaustive]
404    DuplicateSaplingNullifier {
405        nullifier: sapling::Nullifier,
406        in_finalized_state: bool,
407    },
408
409    #[error("orchard double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
410    #[non_exhaustive]
411    DuplicateOrchardNullifier {
412        nullifier: orchard::Nullifier,
413        in_finalized_state: bool,
414    },
415
416    #[error("ironwood double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
417    #[non_exhaustive]
418    DuplicateIronwoodNullifier {
419        nullifier: ironwood::Nullifier,
420        in_finalized_state: bool,
421    },
422
423    #[error(
424        "the remaining value in the transparent transaction value pool MUST be nonnegative:\n\
425         {amount_error:?},\n\
426         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
427    )]
428    #[non_exhaustive]
429    NegativeRemainingTransactionValue {
430        amount_error: amount::Error,
431        height: block::Height,
432        tx_index_in_block: usize,
433        transaction_hash: transaction::Hash,
434    },
435
436    #[error(
437        "error calculating the remaining value in the transaction value pool:\n\
438         {amount_error:?},\n\
439         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
440    )]
441    #[non_exhaustive]
442    CalculateRemainingTransactionValue {
443        amount_error: amount::Error,
444        height: block::Height,
445        tx_index_in_block: usize,
446        transaction_hash: transaction::Hash,
447    },
448
449    #[error(
450        "error calculating value balances for the remaining value in the transaction value pool:\n\
451         {value_balance_error:?},\n\
452         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
453    )]
454    #[non_exhaustive]
455    CalculateTransactionValueBalances {
456        value_balance_error: ValueBalanceError,
457        height: block::Height,
458        tx_index_in_block: usize,
459        transaction_hash: transaction::Hash,
460    },
461
462    #[error(
463        "error calculating the block chain value pool change:\n\
464         {value_balance_error:?},\n\
465         {height:?}, {block_hash:?},\n\
466         transactions: {transaction_count:?}, spent UTXOs: {spent_utxo_count:?}"
467    )]
468    #[non_exhaustive]
469    CalculateBlockChainValueChange {
470        value_balance_error: ValueBalanceError,
471        height: block::Height,
472        block_hash: block::Hash,
473        transaction_count: usize,
474        spent_utxo_count: usize,
475    },
476
477    #[error(
478        "error adding value balances to the chain value pool:\n\
479         {value_balance_error:?},\n\
480         {chain_value_pools:?},\n\
481         {block_value_pool_change:?},\n\
482         {height:?}"
483    )]
484    #[non_exhaustive]
485    AddValuePool {
486        value_balance_error: ValueBalanceError,
487        chain_value_pools: Box<ValueBalance<NonNegative>>,
488        block_value_pool_change: Box<ValueBalance<NegativeAllowed>>,
489        height: Option<block::Height>,
490    },
491
492    #[error(
493        "invalid block subsidy, funding stream, or miner fee payment in block {block_hash:?} at \
494         {height:?}: {subsidy_error}"
495    )]
496    #[non_exhaustive]
497    InvalidSubsidy {
498        subsidy_error: zebra_chain::parameters::subsidy::CoinbaseTransactionError,
499        height: block::Height,
500        block_hash: block::Hash,
501    },
502
503    #[error("error updating a note commitment tree: {0}")]
504    NoteCommitmentTreeError(#[from] zebra_chain::parallel::tree::NoteCommitmentTreeError),
505
506    #[error("error building the history tree: {0}")]
507    HistoryTreeError(#[from] Arc<HistoryTreeError>),
508
509    #[error("block contains an invalid commitment: {0}")]
510    InvalidBlockCommitment(#[from] block::CommitmentError),
511
512    #[error(
513        "unknown Sprout anchor: {anchor:?},\n\
514         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
515    )]
516    #[non_exhaustive]
517    UnknownSproutAnchor {
518        anchor: sprout::tree::Root,
519        height: Option<block::Height>,
520        tx_index_in_block: Option<usize>,
521        transaction_hash: transaction::Hash,
522    },
523
524    #[error(
525        "unknown Sapling anchor: {anchor:?},\n\
526         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
527    )]
528    #[non_exhaustive]
529    UnknownSaplingAnchor {
530        anchor: sapling::tree::Root,
531        height: Option<block::Height>,
532        tx_index_in_block: Option<usize>,
533        transaction_hash: transaction::Hash,
534    },
535
536    #[error(
537        "unknown Orchard anchor: {anchor:?},\n\
538         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
539    )]
540    #[non_exhaustive]
541    UnknownOrchardAnchor {
542        anchor: orchard::tree::Root,
543        height: Option<block::Height>,
544        tx_index_in_block: Option<usize>,
545        transaction_hash: transaction::Hash,
546    },
547
548    #[error(
549        "unknown Ironwood anchor: {anchor:?},\n\
550         {height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
551    )]
552    #[non_exhaustive]
553    UnknownIronwoodAnchor {
554        // Ironwood reuses the Orchard tree root type.
555        anchor: orchard::tree::Root,
556        height: Option<block::Height>,
557        tx_index_in_block: Option<usize>,
558        transaction_hash: transaction::Hash,
559    },
560
561    /// The block was not validated because an ancestor in the same queued chain
562    /// failed contextual validation.
563    ///
564    /// This is a distinct variant, rather than a copy of the ancestor's error, so the
565    /// peer that served this block isn't scored or classified for the ancestor's
566    /// failure.
567    #[error("ancestor block {ancestor_hash} failed contextual validation")]
568    #[non_exhaustive]
569    AncestorRejected {
570        ancestor_hash: block::Hash,
571        source: Box<ValidateContextError>,
572    },
573}
574
575impl ValidateContextError {
576    /// Returns `true` if the block's authorizing data doesn't match the commitment in
577    /// its header.
578    ///
579    /// From NU5 onward, the block header hash and merkle root don't commit to the
580    /// authorizing data, so a peer can serve a forged body for a canonical header
581    /// without changing the block hash. A mismatched authorizing data commitment proves
582    /// the served body doesn't belong to its header, and an honest peer never serves
583    /// such a body, so the serving peer is misbehaving and the hash is still wanted.
584    pub fn is_auth_commitment_mismatch(&self) -> bool {
585        matches!(
586            self,
587            ValidateContextError::InvalidBlockCommitment(
588                block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment { .. },
589            )
590        )
591    }
592
593    /// Returns `true` if the block was rejected only because an ancestor's authorizing
594    /// data didn't match the commitment in its header.
595    ///
596    /// The ancestor's served body was forged, but its hash is still wanted, so this
597    /// block's hash is still wanted too. The peer that served this block isn't the one
598    /// that forged the ancestor, so it must not be scored.
599    pub fn is_descendant_of_auth_commitment_mismatch(&self) -> bool {
600        matches!(
601            self,
602            ValidateContextError::AncestorRejected { source, .. }
603                if source.is_auth_commitment_mismatch()
604        )
605    }
606
607    /// Wraps `self`, the error of a rejected ancestor block, into the error for one of
608    /// its descendants, so the descendant carries neither the ancestor's misbehaviour
609    /// score nor its classification.
610    pub fn for_descendant(&self, ancestor_hash: block::Hash) -> Self {
611        match self {
612            // Keep the original failing ancestor, so deep descendants don't nest errors.
613            ValidateContextError::AncestorRejected { .. } => self.clone(),
614            _ => ValidateContextError::AncestorRejected {
615                ancestor_hash,
616                source: Box::new(self.clone()),
617            },
618        }
619    }
620
621    /// Returns a suggested misbehaviour score increment for a certain error.
622    pub fn misbehavior_score(&self) -> u32 {
623        match self {
624            // A forged body proves the serving peer misbehaved, see
625            // `is_auth_commitment_mismatch()`.
626            ValidateContextError::InvalidBlockCommitment(
627                block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment { .. },
628            ) => 100,
629            // A wrong block subsidy, funding stream, or miner fee payment is invalid given only
630            // the block and its parent, so the peer that sent it is misbehaving.
631            ValidateContextError::InvalidSubsidy { .. } => 100,
632            _other => 0,
633        }
634    }
635}
636
637impl From<sprout::tree::NoteCommitmentTreeError> for ValidateContextError {
638    fn from(value: sprout::tree::NoteCommitmentTreeError) -> Self {
639        ValidateContextError::NoteCommitmentTreeError(value.into())
640    }
641}
642
643/// Trait for creating the corresponding duplicate nullifier error from a nullifier.
644pub trait DuplicateNullifierError {
645    /// Returns the corresponding duplicate nullifier error for `self`.
646    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError;
647}
648
649impl DuplicateNullifierError for sprout::Nullifier {
650    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
651        ValidateContextError::DuplicateSproutNullifier {
652            nullifier: *self,
653            in_finalized_state,
654        }
655    }
656}
657
658impl DuplicateNullifierError for sapling::Nullifier {
659    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
660        ValidateContextError::DuplicateSaplingNullifier {
661            nullifier: *self,
662            in_finalized_state,
663        }
664    }
665}
666
667impl DuplicateNullifierError for orchard::Nullifier {
668    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
669        ValidateContextError::DuplicateOrchardNullifier {
670            nullifier: *self,
671            in_finalized_state,
672        }
673    }
674}
675
676impl DuplicateNullifierError for ironwood::Nullifier {
677    fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
678        ValidateContextError::DuplicateIronwoodNullifier {
679            nullifier: *self,
680            in_finalized_state,
681        }
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688    use zebra_chain::block::Height;
689
690    #[test]
691    fn commit_block_error_misbehavior_scores() {
692        let context_err = CommitBlockError::ValidateContextError(Box::new(
693            ValidateContextError::NonSequentialBlock {
694                candidate_height: Height(5),
695                parent_height: Height(3),
696            },
697        ));
698        assert_eq!(context_err.misbehavior_score(), 0);
699
700        let dup_err = CommitBlockError::Duplicate {
701            hash_or_height: None,
702            location: KnownBlock::BestChain,
703        };
704        assert_eq!(dup_err.misbehavior_score(), 0);
705
706        // A mismatched authorizing data commitment means the served body doesn't
707        // belong to its header, so the serving peer must be scored.
708        let auth_commitment_err = CommitBlockError::ValidateContextError(Box::new(
709            ValidateContextError::InvalidBlockCommitment(
710                block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
711                    expected: [1; 32],
712                    actual: [2; 32],
713                },
714            ),
715        ));
716        assert_eq!(auth_commitment_err.misbehavior_score(), 100);
717    }
718
719    /// The state rejects queued descendants along with a failed block, but the peers that
720    /// served them didn't serve the failed block, so they must not inherit its score or
721    /// classification.
722    #[test]
723    fn descendant_errors_are_not_scored() {
724        let auth_commitment_err = ValidateContextError::InvalidBlockCommitment(
725            block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
726                expected: [1; 32],
727                actual: [2; 32],
728            },
729        );
730        let forged_hash = block::Hash([1; 32]);
731
732        let child_err = auth_commitment_err.for_descendant(forged_hash);
733        assert_eq!(child_err.misbehavior_score(), 0);
734        assert!(!child_err.is_auth_commitment_mismatch());
735        assert!(child_err.is_descendant_of_auth_commitment_mismatch());
736
737        // Deeper descendants keep pointing at the block that actually failed.
738        let grandchild_err = child_err.for_descendant(block::Hash([2; 32]));
739        assert_eq!(grandchild_err, child_err);
740
741        let commit_err = CommitBlockError::ValidateContextError(Box::new(child_err));
742        assert_eq!(commit_err.misbehavior_score(), 0);
743        assert!(!commit_err.is_auth_commitment_mismatch());
744        assert!(commit_err.is_descendant_of_auth_commitment_mismatch());
745
746        // Descendants of other contextual failures aren't re-requested.
747        let other_child_err = ValidateContextError::NonSequentialBlock {
748            candidate_height: Height(5),
749            parent_height: Height(3),
750        }
751        .for_descendant(forged_hash);
752        assert_eq!(other_child_err.misbehavior_score(), 0);
753        assert!(!other_child_err.is_descendant_of_auth_commitment_mismatch());
754    }
755
756    #[test]
757    fn invalid_subsidy_commit_error_misbehavior_score() {
758        use zebra_chain::parameters::subsidy::{CoinbaseTransactionError, SubsidyError};
759
760        let invalid_subsidy = ValidateContextError::InvalidSubsidy {
761            subsidy_error: CoinbaseTransactionError::Subsidy(SubsidyError::InvalidMinerFees),
762            height: Height(5),
763            block_hash: zebra_chain::block::Hash([0; 32]),
764        };
765        let descendant = invalid_subsidy.for_descendant(block::Hash([1; 32]));
766        assert_eq!(descendant.misbehavior_score(), 0);
767        assert!(!descendant.is_descendant_of_auth_commitment_mismatch());
768        let subsidy_err = CommitBlockError::ValidateContextError(Box::new(invalid_subsidy));
769        assert_eq!(subsidy_err.misbehavior_score(), 100);
770    }
771}