use std::{path::PathBuf, sync::Arc};
use chrono::{DateTime, Utc};
use derive_new::new;
use thiserror::Error;
use tokio::sync::broadcast::error::RecvError;
use zebra_chain::{
amount::{self, NegativeAllowed, NonNegative},
block,
history_tree::HistoryTreeError,
ironwood, orchard, sapling, sprout, transaction, transparent,
value_balance::{ValueBalance, ValueBalanceError},
work::difficulty::CompactDifficulty,
};
use crate::{constants::MIN_TRANSPARENT_COINBASE_MATURITY, HashOrHeight, KnownBlock};
#[derive(Debug, Error, Clone)]
#[error(transparent)]
pub struct CloneError {
source: Arc<dyn std::error::Error + Send + Sync + 'static>,
}
impl From<CommitSemanticallyVerifiedError> for CloneError {
fn from(source: CommitSemanticallyVerifiedError) -> Self {
let source = Arc::new(source);
Self { source }
}
}
impl From<BoxError> for CloneError {
fn from(source: BoxError) -> Self {
let source = Arc::from(source);
Self { source }
}
}
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StateInitError {
#[error(
"cannot open read-only state: cache directory {path:?} is missing or unreadable. \
Hint: a read-only state requires an existing Zebra cache directory; check that the \
state cache_dir in the Zebra config points at a running Zebra node's cache directory"
)]
ReadOnlyCacheDirUnreadable {
path: PathBuf,
source: std::io::Error,
},
#[error(
"cannot open read-only state: no database found at {path:?}. \
Hint: a read-only state requires an existing finalized database created by a running \
Zebra node; check that the state cache_dir in the Zebra config points at that node's \
cache directory"
)]
ReadOnlyDatabaseNotFound {
path: PathBuf,
},
#[error(
"cannot open read-only state: an ephemeral database was also requested. \
Hint: a read-only state follows an existing Zebra node's database and must not \
delete it; set `ephemeral = false`, or do not request a read-only state"
)]
ReadOnlyEphemeralConflict,
}
#[derive(Debug, Error, Clone, PartialEq, Eq, new)]
pub enum CommitBlockError {
#[error("block hash is a duplicate: already in {location}")]
Duplicate {
hash_or_height: Option<HashOrHeight>,
location: KnownBlock,
},
#[error("could not contextually validate semantically verified block")]
ValidateContextError(#[from] Box<ValidateContextError>),
#[error("block commit task exited. Is Zebra shutting down?")]
#[non_exhaustive]
WriteTaskExited,
}
impl CommitBlockError {
pub fn is_duplicate_request(&self) -> bool {
matches!(self, CommitBlockError::Duplicate { .. })
}
pub fn is_auth_commitment_mismatch(&self) -> bool {
match self {
CommitBlockError::ValidateContextError(err) => err.is_auth_commitment_mismatch(),
CommitBlockError::Duplicate { .. } | CommitBlockError::WriteTaskExited => false,
}
}
pub fn is_descendant_of_auth_commitment_mismatch(&self) -> bool {
match self {
CommitBlockError::ValidateContextError(err) => {
err.is_descendant_of_auth_commitment_mismatch()
}
CommitBlockError::Duplicate { .. } | CommitBlockError::WriteTaskExited => false,
}
}
pub fn misbehavior_score(&self) -> u32 {
match self {
CommitBlockError::ValidateContextError(err) => err.misbehavior_score(),
CommitBlockError::Duplicate { .. } | CommitBlockError::WriteTaskExited => 0,
}
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("could not commit semantically-verified block")]
pub struct CommitSemanticallyVerifiedError(#[from] CommitBlockError);
impl CommitSemanticallyVerifiedError {
pub fn inner(&self) -> &CommitBlockError {
&self.0
}
}
impl From<ValidateContextError> for CommitSemanticallyVerifiedError {
fn from(value: ValidateContextError) -> Self {
Self(CommitBlockError::ValidateContextError(Box::new(value)))
}
}
#[derive(Debug, Error)]
pub enum LayeredStateError<E: std::error::Error + std::fmt::Display> {
#[error("{0}")]
State(E),
#[error("{0}")]
Layer(BoxError),
}
impl<E: std::error::Error + 'static> From<BoxError> for LayeredStateError<E> {
fn from(err: BoxError) -> Self {
match err.downcast::<E>() {
Ok(state_err) => Self::State(*state_err),
Err(layer_error) => Self::Layer(layer_error),
}
}
}
#[derive(Debug, Error, Clone)]
#[error("could not commit checkpoint-verified block")]
pub struct CommitCheckpointVerifiedError(#[from] CommitBlockError);
impl CommitCheckpointVerifiedError {
pub fn inner(&self) -> &CommitBlockError {
&self.0
}
}
impl From<ValidateContextError> for CommitCheckpointVerifiedError {
fn from(value: ValidateContextError) -> Self {
Self(CommitBlockError::ValidateContextError(Box::new(value)))
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum InvalidateError {
#[error("cannot invalidate blocks while still committing checkpointed blocks")]
ProcessingCheckpointedBlocks,
#[error("failed to send invalidate block request to block write task")]
SendInvalidateRequestFailed,
#[error("invalidate block request was unexpectedly dropped")]
InvalidateRequestDropped,
#[error("block hash {0} not found in any non-finalized chain")]
BlockNotFound(block::Hash),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ReconsiderError {
#[error("Block with hash {0} was not previously invalidated")]
MissingInvalidatedBlock(block::Hash),
#[error("Parent chain not found for block {0}")]
ParentChainNotFound(block::Hash),
#[error("Invalidated blocks list is empty when it should contain at least one block")]
InvalidatedBlocksEmpty,
#[error("cannot reconsider blocks while still committing checkpointed blocks")]
CheckpointCommitInProgress,
#[error("failed to send reconsider block request to block write task")]
ReconsiderSendFailed,
#[error("reconsider block request was unexpectedly dropped")]
ReconsiderResponseDropped,
#[error("replaying a previously invalidated block failed contextual validation: {0}")]
ReplayFailed(#[source] ValidateContextError),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AwaitUtxoError {
#[error("the state stopped responding before the UTXO arrived")]
Cancelled,
#[error("internal channel error while waiting for the UTXO: receiver lagged by {0}")]
Lagged(u64),
#[error("the read state service returned an error: {0}")]
ReadStateFailed(#[source] BoxError),
}
impl From<RecvError> for AwaitUtxoError {
fn from(err: RecvError) -> Self {
match err {
RecvError::Closed => AwaitUtxoError::Cancelled,
RecvError::Lagged(skipped) => AwaitUtxoError::Lagged(skipped),
}
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum ValidateContextError {
#[error("block hash {block_hash} was previously invalidated")]
#[non_exhaustive]
BlockPreviouslyInvalidated { block_hash: block::Hash },
#[error("block parent not found in any chain, or not enough blocks in chain")]
#[non_exhaustive]
NotReadyToBeCommitted,
#[error("block height {candidate_height:?} is lower than the current finalized height {finalized_tip_height:?}")]
#[non_exhaustive]
OrphanedBlock {
candidate_height: block::Height,
finalized_tip_height: block::Height,
},
#[error("block height {candidate_height:?} is not one greater than its parent block's height {parent_height:?}")]
#[non_exhaustive]
NonSequentialBlock {
candidate_height: block::Height,
parent_height: block::Height,
},
#[error("block time {candidate_time:?} is less than or equal to the median-time-past for the block {median_time_past:?}")]
#[non_exhaustive]
TimeTooEarly {
candidate_time: DateTime<Utc>,
median_time_past: DateTime<Utc>,
},
#[error("block time {candidate_time:?} is greater than the median-time-past for the block plus 90 minutes {block_time_max:?}")]
#[non_exhaustive]
TimeTooLate {
candidate_time: DateTime<Utc>,
block_time_max: DateTime<Utc>,
},
#[error("block difficulty threshold {difficulty_threshold:?} is not equal to the expected difficulty for the block {expected_difficulty:?}")]
#[non_exhaustive]
InvalidDifficultyThreshold {
difficulty_threshold: CompactDifficulty,
expected_difficulty: CompactDifficulty,
},
#[error("transparent double-spend: {outpoint:?} is spent twice in {location:?}")]
#[non_exhaustive]
DuplicateTransparentSpend {
outpoint: transparent::OutPoint,
location: &'static str,
},
#[error("missing transparent output: possible double-spend of {outpoint:?} in {location:?}")]
#[non_exhaustive]
MissingTransparentOutput {
outpoint: transparent::OutPoint,
location: &'static str,
},
#[error("out-of-order transparent spend: {outpoint:?} is created by a later transaction in the same block")]
#[non_exhaustive]
EarlyTransparentSpend { outpoint: transparent::OutPoint },
#[error(
"unshielded transparent coinbase spend: {outpoint:?} \
must be spent in a transaction which only has shielded outputs"
)]
#[non_exhaustive]
UnshieldedTransparentCoinbaseSpend { outpoint: transparent::OutPoint },
#[error(
"immature transparent coinbase spend: \
attempt to spend {outpoint:?} at {spend_height:?}, \
but spends are invalid before {min_spend_height:?}, \
which is {MIN_TRANSPARENT_COINBASE_MATURITY:?} blocks \
after it was created at {created_height:?}"
)]
#[non_exhaustive]
ImmatureTransparentCoinbaseSpend {
outpoint: transparent::OutPoint,
spend_height: block::Height,
min_spend_height: block::Height,
created_height: block::Height,
},
#[error("sprout double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
#[non_exhaustive]
DuplicateSproutNullifier {
nullifier: sprout::Nullifier,
in_finalized_state: bool,
},
#[error("sapling double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
#[non_exhaustive]
DuplicateSaplingNullifier {
nullifier: sapling::Nullifier,
in_finalized_state: bool,
},
#[error("orchard double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
#[non_exhaustive]
DuplicateOrchardNullifier {
nullifier: orchard::Nullifier,
in_finalized_state: bool,
},
#[error("ironwood double-spend: duplicate nullifier: {nullifier:?}, in finalized state: {in_finalized_state:?}")]
#[non_exhaustive]
DuplicateIronwoodNullifier {
nullifier: ironwood::Nullifier,
in_finalized_state: bool,
},
#[error(
"the remaining value in the transparent transaction value pool MUST be nonnegative:\n\
{amount_error:?},\n\
{height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
)]
#[non_exhaustive]
NegativeRemainingTransactionValue {
amount_error: amount::Error,
height: block::Height,
tx_index_in_block: usize,
transaction_hash: transaction::Hash,
},
#[error(
"error calculating the remaining value in the transaction value pool:\n\
{amount_error:?},\n\
{height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
)]
#[non_exhaustive]
CalculateRemainingTransactionValue {
amount_error: amount::Error,
height: block::Height,
tx_index_in_block: usize,
transaction_hash: transaction::Hash,
},
#[error(
"error calculating value balances for the remaining value in the transaction value pool:\n\
{value_balance_error:?},\n\
{height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
)]
#[non_exhaustive]
CalculateTransactionValueBalances {
value_balance_error: ValueBalanceError,
height: block::Height,
tx_index_in_block: usize,
transaction_hash: transaction::Hash,
},
#[error(
"error calculating the block chain value pool change:\n\
{value_balance_error:?},\n\
{height:?}, {block_hash:?},\n\
transactions: {transaction_count:?}, spent UTXOs: {spent_utxo_count:?}"
)]
#[non_exhaustive]
CalculateBlockChainValueChange {
value_balance_error: ValueBalanceError,
height: block::Height,
block_hash: block::Hash,
transaction_count: usize,
spent_utxo_count: usize,
},
#[error(
"error adding value balances to the chain value pool:\n\
{value_balance_error:?},\n\
{chain_value_pools:?},\n\
{block_value_pool_change:?},\n\
{height:?}"
)]
#[non_exhaustive]
AddValuePool {
value_balance_error: ValueBalanceError,
chain_value_pools: Box<ValueBalance<NonNegative>>,
block_value_pool_change: Box<ValueBalance<NegativeAllowed>>,
height: Option<block::Height>,
},
#[error(
"invalid block subsidy, funding stream, or miner fee payment in block {block_hash:?} at \
{height:?}: {subsidy_error}"
)]
#[non_exhaustive]
InvalidSubsidy {
subsidy_error: zebra_chain::parameters::subsidy::CoinbaseTransactionError,
height: block::Height,
block_hash: block::Hash,
},
#[error("error updating a note commitment tree: {0}")]
NoteCommitmentTreeError(#[from] zebra_chain::parallel::tree::NoteCommitmentTreeError),
#[error("error building the history tree: {0}")]
HistoryTreeError(#[from] Arc<HistoryTreeError>),
#[error("block contains an invalid commitment: {0}")]
InvalidBlockCommitment(#[from] block::CommitmentError),
#[error(
"unknown Sprout anchor: {anchor:?},\n\
{height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
)]
#[non_exhaustive]
UnknownSproutAnchor {
anchor: sprout::tree::Root,
height: Option<block::Height>,
tx_index_in_block: Option<usize>,
transaction_hash: transaction::Hash,
},
#[error(
"unknown Sapling anchor: {anchor:?},\n\
{height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
)]
#[non_exhaustive]
UnknownSaplingAnchor {
anchor: sapling::tree::Root,
height: Option<block::Height>,
tx_index_in_block: Option<usize>,
transaction_hash: transaction::Hash,
},
#[error(
"unknown Orchard anchor: {anchor:?},\n\
{height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
)]
#[non_exhaustive]
UnknownOrchardAnchor {
anchor: orchard::tree::Root,
height: Option<block::Height>,
tx_index_in_block: Option<usize>,
transaction_hash: transaction::Hash,
},
#[error(
"unknown Ironwood anchor: {anchor:?},\n\
{height:?}, index in block: {tx_index_in_block:?}, {transaction_hash:?}"
)]
#[non_exhaustive]
UnknownIronwoodAnchor {
anchor: orchard::tree::Root,
height: Option<block::Height>,
tx_index_in_block: Option<usize>,
transaction_hash: transaction::Hash,
},
#[error("ancestor block {ancestor_hash} failed contextual validation")]
#[non_exhaustive]
AncestorRejected {
ancestor_hash: block::Hash,
source: Box<ValidateContextError>,
},
}
impl ValidateContextError {
pub fn is_auth_commitment_mismatch(&self) -> bool {
matches!(
self,
ValidateContextError::InvalidBlockCommitment(
block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment { .. },
)
)
}
pub fn is_descendant_of_auth_commitment_mismatch(&self) -> bool {
matches!(
self,
ValidateContextError::AncestorRejected { source, .. }
if source.is_auth_commitment_mismatch()
)
}
pub fn for_descendant(&self, ancestor_hash: block::Hash) -> Self {
match self {
ValidateContextError::AncestorRejected { .. } => self.clone(),
_ => ValidateContextError::AncestorRejected {
ancestor_hash,
source: Box::new(self.clone()),
},
}
}
pub fn misbehavior_score(&self) -> u32 {
match self {
ValidateContextError::InvalidBlockCommitment(
block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment { .. },
) => 100,
ValidateContextError::InvalidSubsidy { .. } => 100,
_other => 0,
}
}
}
impl From<sprout::tree::NoteCommitmentTreeError> for ValidateContextError {
fn from(value: sprout::tree::NoteCommitmentTreeError) -> Self {
ValidateContextError::NoteCommitmentTreeError(value.into())
}
}
pub trait DuplicateNullifierError {
fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError;
}
impl DuplicateNullifierError for sprout::Nullifier {
fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
ValidateContextError::DuplicateSproutNullifier {
nullifier: *self,
in_finalized_state,
}
}
}
impl DuplicateNullifierError for sapling::Nullifier {
fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
ValidateContextError::DuplicateSaplingNullifier {
nullifier: *self,
in_finalized_state,
}
}
}
impl DuplicateNullifierError for orchard::Nullifier {
fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
ValidateContextError::DuplicateOrchardNullifier {
nullifier: *self,
in_finalized_state,
}
}
}
impl DuplicateNullifierError for ironwood::Nullifier {
fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError {
ValidateContextError::DuplicateIronwoodNullifier {
nullifier: *self,
in_finalized_state,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use zebra_chain::block::Height;
#[test]
fn commit_block_error_misbehavior_scores() {
let context_err = CommitBlockError::ValidateContextError(Box::new(
ValidateContextError::NonSequentialBlock {
candidate_height: Height(5),
parent_height: Height(3),
},
));
assert_eq!(context_err.misbehavior_score(), 0);
let dup_err = CommitBlockError::Duplicate {
hash_or_height: None,
location: KnownBlock::BestChain,
};
assert_eq!(dup_err.misbehavior_score(), 0);
let auth_commitment_err = CommitBlockError::ValidateContextError(Box::new(
ValidateContextError::InvalidBlockCommitment(
block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
expected: [1; 32],
actual: [2; 32],
},
),
));
assert_eq!(auth_commitment_err.misbehavior_score(), 100);
}
#[test]
fn descendant_errors_are_not_scored() {
let auth_commitment_err = ValidateContextError::InvalidBlockCommitment(
block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
expected: [1; 32],
actual: [2; 32],
},
);
let forged_hash = block::Hash([1; 32]);
let child_err = auth_commitment_err.for_descendant(forged_hash);
assert_eq!(child_err.misbehavior_score(), 0);
assert!(!child_err.is_auth_commitment_mismatch());
assert!(child_err.is_descendant_of_auth_commitment_mismatch());
let grandchild_err = child_err.for_descendant(block::Hash([2; 32]));
assert_eq!(grandchild_err, child_err);
let commit_err = CommitBlockError::ValidateContextError(Box::new(child_err));
assert_eq!(commit_err.misbehavior_score(), 0);
assert!(!commit_err.is_auth_commitment_mismatch());
assert!(commit_err.is_descendant_of_auth_commitment_mismatch());
let other_child_err = ValidateContextError::NonSequentialBlock {
candidate_height: Height(5),
parent_height: Height(3),
}
.for_descendant(forged_hash);
assert_eq!(other_child_err.misbehavior_score(), 0);
assert!(!other_child_err.is_descendant_of_auth_commitment_mismatch());
}
#[test]
fn invalid_subsidy_commit_error_misbehavior_score() {
use zebra_chain::parameters::subsidy::{CoinbaseTransactionError, SubsidyError};
let invalid_subsidy = ValidateContextError::InvalidSubsidy {
subsidy_error: CoinbaseTransactionError::Subsidy(SubsidyError::InvalidMinerFees),
height: Height(5),
block_hash: zebra_chain::block::Hash([0; 32]),
};
let descendant = invalid_subsidy.for_descendant(block::Hash([1; 32]));
assert_eq!(descendant.misbehavior_score(), 0);
assert!(!descendant.is_descendant_of_auth_commitment_mismatch());
let subsidy_err = CommitBlockError::ValidateContextError(Box::new(invalid_subsidy));
assert_eq!(subsidy_err.misbehavior_score(), 100);
}
}