1use 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#[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
43pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
45
46#[derive(Debug, Error)]
51#[non_exhaustive]
52pub enum StateInitError {
53 #[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 path: PathBuf,
67 source: std::io::Error,
69 },
70
71 #[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 path: PathBuf,
85 },
86
87 #[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#[derive(Debug, Error, Clone, PartialEq, Eq, new)]
102pub enum CommitBlockError {
103 #[error("block hash is a duplicate: already in {location}")]
104 Duplicate {
106 hash_or_height: Option<HashOrHeight>,
108 location: KnownBlock,
110 },
111
112 #[error("could not contextually validate semantically verified block")]
114 ValidateContextError(#[from] Box<ValidateContextError>),
115
116 #[error("block commit task exited. Is Zebra shutting down?")]
118 #[non_exhaustive]
119 WriteTaskExited,
120}
121
122impl CommitBlockError {
123 pub fn is_duplicate_request(&self) -> bool {
126 matches!(self, CommitBlockError::Duplicate { .. })
127 }
128
129 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 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 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#[derive(Debug, Error, Clone, PartialEq, Eq)]
170#[error("could not commit semantically-verified block")]
171pub struct CommitSemanticallyVerifiedError(#[from] CommitBlockError);
172
173impl CommitSemanticallyVerifiedError {
174 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#[derive(Debug, Error, Clone)]
205#[error("could not commit checkpoint-verified block")]
206pub struct CommitCheckpointVerifiedError(#[from] CommitBlockError);
207
208impl CommitCheckpointVerifiedError {
209 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#[derive(Debug, Error)]
223#[non_exhaustive]
224pub enum InvalidateError {
225 #[error("cannot invalidate blocks while still committing checkpointed blocks")]
227 ProcessingCheckpointedBlocks,
228
229 #[error("failed to send invalidate block request to block write task")]
231 SendInvalidateRequestFailed,
232
233 #[error("invalidate block request was unexpectedly dropped")]
235 InvalidateRequestDropped,
236
237 #[error("block hash {0} not found in any non-finalized chain")]
239 BlockNotFound(block::Hash),
240
241 #[error("cannot invalidate block {0}: it is the root of the only non-finalized chain")]
243 OnlyChainRoot(block::Hash),
244}
245
246#[derive(Debug, Error)]
248#[non_exhaustive]
249pub enum ReconsiderError {
250 #[error("Block with hash {0} was not previously invalidated")]
252 MissingInvalidatedBlock(block::Hash),
253
254 #[error("Parent chain not found for block {0}")]
256 ParentChainNotFound(block::Hash),
257
258 #[error("Invalidated blocks list is empty when it should contain at least one block")]
260 InvalidatedBlocksEmpty,
261
262 #[error("cannot reconsider blocks while still committing checkpointed blocks")]
264 CheckpointCommitInProgress,
265
266 #[error("failed to send reconsider block request to block write task")]
268 ReconsiderSendFailed,
269
270 #[error("reconsider block request was unexpectedly dropped")]
272 ReconsiderResponseDropped,
273
274 #[error("replaying a previously invalidated block failed contextual validation: {0}")]
277 ReplayFailed(#[source] ValidateContextError),
278}
279
280#[derive(Debug, Error)]
282#[non_exhaustive]
283pub enum AwaitUtxoError {
284 #[error("the state stopped responding before the UTXO arrived")]
287 Cancelled,
288
289 #[error("internal channel error while waiting for the UTXO: receiver lagged by {0}")]
291 Lagged(u64),
292
293 #[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#[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 anchor: orchard::tree::Root,
556 height: Option<block::Height>,
557 tx_index_in_block: Option<usize>,
558 transaction_hash: transaction::Hash,
559 },
560
561 #[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 pub fn is_auth_commitment_mismatch(&self) -> bool {
585 matches!(
586 self,
587 ValidateContextError::InvalidBlockCommitment(
588 block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment { .. },
589 )
590 )
591 }
592
593 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 pub fn for_descendant(&self, ancestor_hash: block::Hash) -> Self {
611 match self {
612 ValidateContextError::AncestorRejected { .. } => self.clone(),
614 _ => ValidateContextError::AncestorRejected {
615 ancestor_hash,
616 source: Box::new(self.clone()),
617 },
618 }
619 }
620
621 pub fn misbehavior_score(&self) -> u32 {
623 match self {
624 ValidateContextError::InvalidBlockCommitment(
627 block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment { .. },
628 ) => 100,
629 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
643pub trait DuplicateNullifierError {
645 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 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 #[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 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 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}