use std::{path::PathBuf, sync::Arc};
use chrono::{DateTime, Utc};
use derive_new::new;
use thiserror::Error;
use zakura_chain::{
amount::{self, NegativeAllowed, NonNegative},
block,
history_tree::HistoryTreeError,
ironwood, orchard, sapling, sprout,
subtree::NoteCommitmentSubtreeIndex,
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(Clone, Debug, Error, Eq, PartialEq)]
#[error("missing Sprout note commitment tree at finalized tip {tip:?}")]
pub struct MissingSproutTipTree {
pub tip: block::Height,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error(
"historical note commitment tree at {hash_or_height} is unavailable: this node was fast-synced \
with verified commitment trees, so per-height trees below the last checkpoint \
{last_checkpoint:?} \
were never written"
)]
pub struct HistoricalTreeUnavailable {
pub hash_or_height: HashOrHeight,
pub last_checkpoint: block::Height,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error(
"historical {pool} note commitment subtree {index:?} is unavailable at last checkpoint \
{last_checkpoint:?}: {reason}"
)]
pub struct HistoricalSubtreeUnavailable {
pub pool: &'static str,
pub index: NoteCommitmentSubtreeIndex,
pub last_checkpoint: block::Height,
pub reason: HistoricalSubtreeUnavailableReason,
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum HistoricalSubtreeUnavailableReason {
#[error(
"this node has not reached the last checkpoint, so it cannot yet tell whether the \
subtree was skipped; subtrees completed below the last checkpoint are never recorded"
)]
Indeterminate,
#[error(
"this node did not record the subtree and continuing synchronization will not restore it; \
use another node"
)]
NotStored,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StateInitError {
#[error(
"the historical frontier artifact configured at {path:?} could not be loaded: {source}. \
Hint: remove state.historical_frontier_artifact to serve without it"
)]
HistoricalFrontierArtifact {
path: PathBuf,
source: BoxError,
},
#[error(
"historical frontier artifact at {path:?} has a {blocks}-block gap, more than \
MAX_HISTORICAL_TREE_REPLAY_BLOCKS = {limit}; a cold request would replay that range"
)]
HistoricalFrontierArtifactTooSparse {
path: PathBuf,
blocks: u64,
limit: u64,
},
#[error(
"cannot read state database format version at {path:?}. Hint: check the cache directory permissions and version file contents"
)]
DatabaseFormatVersion {
path: PathBuf,
source: BoxError,
},
#[error(
"cannot open state database at {path:?}. Hint: check whether another process holds the database lock and whether cache_dir is readable and writable"
)]
DatabaseOpen {
path: PathBuf,
source: rocksdb::Error,
},
#[error(
"cannot upgrade state database format at {path:?}. The database version remains unchanged, so Zakura can retry the migration: {source}"
)]
DatabaseFormatUpgrade {
path: PathBuf,
source: BoxError,
},
#[error(
"cannot open read-only state: cache directory {path:?} is missing or unreadable. \
Hint: a read-only state requires an existing Zakura cache directory; check that the \
state cache_dir in the Zakura config points at a running Zakura 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 \
Zakura node; check that the state cache_dir in the Zakura 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 Zakura node's database and must not \
delete it; set `ephemeral = false`, or do not request a read-only state"
)]
ReadOnlyEphemeralConflict,
#[error(
"cannot open state: this database was written by an early verified-commitment-trees fast \
sync and is missing historical Sprout anchors, so it cannot verify JoinSplits that spend \
a historical Sprout anchor. \
Hint: discard the database and resync, or restore a snapshot taken with a current release"
)]
VctSproutHistoryUnrepairable,
}
#[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("could not commit matching header-chain transition: {error}")]
HeaderChainError {
error: String,
},
#[error("mined block parent is unavailable")]
MissingMinedParent,
#[error("too many blocks are waiting for contextual verification")]
QueueFull,
#[error("block commit task exited. Is Zakura shutting down?")]
#[non_exhaustive]
WriteTaskExited,
}
impl CommitBlockError {
pub fn is_duplicate_request(&self) -> bool {
matches!(self, CommitBlockError::Duplicate { .. })
}
pub fn duplicate_location(&self) -> Option<&KnownBlock> {
match self {
CommitBlockError::Duplicate { location, .. } => Some(location),
_ => None,
}
}
pub fn vct_supplied_root_unavailable_height(&self) -> Option<block::Height> {
match self {
CommitBlockError::ValidateContextError(error) => {
error.vct_supplied_root_unavailable_height()
}
_ => None,
}
}
pub fn vct_retryable_height(&self) -> Option<block::Height> {
match self {
CommitBlockError::ValidateContextError(error) => error.vct_retryable_height(),
_ => None,
}
}
pub fn misbehavior_score(&self) -> u32 {
match self {
CommitBlockError::ValidateContextError(error) => error.misbehavior_score(),
_ => 0,
}
}
pub fn body_verification_class(&self) -> zakura_header_chain::BodyVerificationClass {
use zakura_header_chain::{BodyVerificationClass, TransientBodyFailureKind};
match self {
Self::Duplicate { .. } => BodyVerificationClass::Duplicate,
Self::ValidateContextError(error) => error.body_verification_class(),
Self::HeaderChainError { .. } => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::Storage)
}
Self::MissingMinedParent => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::MissingContext)
}
Self::QueueFull => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::VerifierUnavailable)
}
Self::WriteTaskExited => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::VerifierUnavailable)
}
}
}
}
#[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
}
pub fn duplicate_location(&self) -> Option<&KnownBlock> {
self.0.duplicate_location()
}
}
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 {
#[source]
inner: CommitBlockError,
vct_failure: Option<VctCommitFailure>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum VctCommitFailure {
CurrentRoots,
SuccessorBoundary,
}
impl CommitCheckpointVerifiedError {
pub fn inner(&self) -> &CommitBlockError {
&self.inner
}
pub fn duplicate_location(&self) -> Option<&KnownBlock> {
self.inner.duplicate_location()
}
pub fn vct_supplied_root_unavailable_height(&self) -> Option<block::Height> {
self.inner.vct_supplied_root_unavailable_height()
}
pub fn vct_retryable_height(&self) -> Option<block::Height> {
self.inner.vct_retryable_height()
}
pub(crate) fn with_vct_failure(mut self, failure: VctCommitFailure) -> Self {
self.vct_failure = Some(failure);
self
}
pub(crate) fn vct_failure(&self) -> Option<VctCommitFailure> {
self.vct_failure
}
}
impl From<CommitBlockError> for CommitCheckpointVerifiedError {
fn from(inner: CommitBlockError) -> Self {
Self {
inner,
vct_failure: None,
}
}
}
impl From<ValidateContextError> for CommitCheckpointVerifiedError {
fn from(value: ValidateContextError) -> Self {
CommitBlockError::ValidateContextError(Box::new(value)).into()
}
}
#[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),
#[error("could not commit matching header-chain invalidation: {error}")]
HeaderChain {
error: String,
},
}
#[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),
#[error(transparent)]
MissingSproutTipTree(#[from] MissingSproutTipTree),
#[error("could not commit matching header-chain reconsideration: {error}")]
HeaderChain {
error: String,
},
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum ValidateContextError {
#[error(transparent)]
MissingSproutTipTree(#[from] MissingSproutTipTree),
#[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 descends from invalid ancestor {0}")]
InvalidAncestorBlock(block::Hash),
#[error(
"verified-commitment-trees fast path has no valid supplied root for height \
{height:?}: the note-commitment frontier is frozen, so this block cannot be \
committed until a verifiable root is fetched from a peer (retryable)"
)]
#[non_exhaustive]
VctSuppliedRootUnavailable { height: block::Height },
#[error(
"verified-commitment-trees fast path cannot yet verify the supplied root for height \
{height:?}: no successor header is stored to confirm it against the header chain, and \
committing it unverified would persist a root that is only checked one block later \
(irreversibly, once on disk). Commit is deferred until the successor header arrives (retryable)"
)]
#[non_exhaustive]
VctSuppliedRootAwaitingSuccessor { height: block::Height },
#[error(
"checkpoint block at {height:?} has authorizing-data root {actual:?}, but its cached \
header prevalidation requires {expected:?}"
)]
#[non_exhaustive]
VctBlockAuthDataRootMismatch {
height: block::Height,
expected: block::merkle::AuthDataRoot,
actual: block::merkle::AuthDataRoot,
},
#[error(
"locally reconstructed Sprout root at the VCT handoff height {height:?} is \
{actual:?}, but the embedded handoff frontier requires {expected:?}"
)]
#[non_exhaustive]
VctSproutHandoffRootMismatch {
height: block::Height,
expected: sprout::tree::Root,
actual: sprout::tree::Root,
},
#[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("cumulative chain work overflows at block {block_hash} ({height:?})")]
#[non_exhaustive]
CumulativeWorkOverflow {
height: block::Height,
block_hash: block::Hash,
},
#[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("error updating a note commitment tree: {0}")]
NoteCommitmentTreeError(#[from] zakura_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: ironwood::tree::Root,
height: Option<block::Height>,
tx_index_in_block: Option<usize>,
transaction_hash: transaction::Hash,
},
}
impl ValidateContextError {
pub fn body_verification_class(&self) -> zakura_header_chain::BodyVerificationClass {
use zakura_chain::block::CommitmentError;
use zakura_header_chain::{
BodyCommitmentKind, BodyRuleId, BodyVerificationClass, TransientBodyFailureKind,
};
let consensus = |rule| BodyVerificationClass::ConsensusInvalid(BodyRuleId::new(rule));
match self {
Self::MissingSproutTipTree(_) | Self::NotReadyToBeCommitted => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::MissingContext)
}
Self::BlockPreviouslyInvalidated { .. }
| Self::InvalidAncestorBlock(_)
| Self::OrphanedBlock { .. } => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::Canceled)
}
Self::VctSuppliedRootUnavailable { .. }
| Self::VctSuppliedRootAwaitingSuccessor { .. } => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::MissingContext)
}
Self::VctBlockAuthDataRootMismatch { .. } => {
BodyVerificationClass::PayloadMismatch(BodyCommitmentKind::AuthDataRoot)
}
Self::VctSproutHandoffRootMismatch { .. }
| Self::CumulativeWorkOverflow { .. }
| Self::NoteCommitmentTreeError(_)
| Self::HistoryTreeError(_) => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::Storage)
}
Self::InvalidBlockCommitment(error) => {
let kind = match error {
CommitmentError::InvalidAuthDataRoot { .. } => BodyCommitmentKind::AuthDataRoot,
CommitmentError::InvalidFinalSaplingRoot { .. } => {
BodyCommitmentKind::Other("final_sapling_root")
}
CommitmentError::InvalidChainHistoryActivationReserved { .. } => {
BodyCommitmentKind::Other("chain_history_activation_reserved")
}
CommitmentError::InvalidChainHistoryRoot { .. } => {
BodyCommitmentKind::Other("chain_history_root")
}
CommitmentError::InvalidChainHistoryBlockTxAuthCommitment { .. } => {
BodyCommitmentKind::Other("chain_history_block_tx_auth_commitment")
}
CommitmentError::InvalidPreNu5OrchardRoot { .. } => {
BodyCommitmentKind::Other("pre_nu5_orchard_root")
}
CommitmentError::InvalidPreNu5OrchardTxCount { .. } => {
BodyCommitmentKind::Other("pre_nu5_orchard_tx_count")
}
CommitmentError::InvalidPreSaplingSaplingTxCount { .. } => {
BodyCommitmentKind::Other("pre_sapling_sapling_tx_count")
}
CommitmentError::InvalidPreNu6_3IronwoodRoot { .. } => {
BodyCommitmentKind::Other("pre_nu6_3_ironwood_root")
}
CommitmentError::InvalidPreNu6_3IronwoodTxCount { .. } => {
BodyCommitmentKind::Other("pre_nu6_3_ironwood_tx_count")
}
CommitmentError::MissingBlockHeight { .. } => {
BodyCommitmentKind::Other("missing_block_height")
}
CommitmentError::InvalidSapingRootBytes => {
BodyCommitmentKind::Other("invalid_sapling_root_bytes")
}
};
BodyVerificationClass::PayloadMismatch(kind)
}
Self::NonSequentialBlock { .. } => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::MissingContext)
}
Self::TimeTooEarly { .. }
| Self::TimeTooLate { .. }
| Self::InvalidDifficultyThreshold { .. } => {
BodyVerificationClass::Retryable(TransientBodyFailureKind::VerifierUnavailable)
}
Self::DuplicateTransparentSpend { .. } => {
consensus("context.duplicate_transparent_spend")
}
Self::MissingTransparentOutput { .. } => {
consensus("context.missing_transparent_output")
}
Self::EarlyTransparentSpend { .. } => consensus("context.early_transparent_spend"),
Self::UnshieldedTransparentCoinbaseSpend { .. } => {
consensus("context.unshielded_transparent_coinbase_spend")
}
Self::ImmatureTransparentCoinbaseSpend { .. } => {
consensus("context.immature_transparent_coinbase_spend")
}
Self::DuplicateSproutNullifier { .. } => {
consensus("context.duplicate_sprout_nullifier")
}
Self::DuplicateSaplingNullifier { .. } => {
consensus("context.duplicate_sapling_nullifier")
}
Self::DuplicateOrchardNullifier { .. } => {
consensus("context.duplicate_orchard_nullifier")
}
Self::DuplicateIronwoodNullifier { .. } => {
consensus("context.duplicate_ironwood_nullifier")
}
Self::NegativeRemainingTransactionValue { .. } => {
consensus("context.negative_remaining_transaction_value")
}
Self::CalculateRemainingTransactionValue { .. } => {
consensus("context.calculate_remaining_transaction_value")
}
Self::CalculateTransactionValueBalances { .. } => {
consensus("context.calculate_transaction_value_balances")
}
Self::CalculateBlockChainValueChange { .. } => {
consensus("context.calculate_block_chain_value_change")
}
Self::AddValuePool { .. } => consensus("context.add_value_pool"),
Self::UnknownSproutAnchor { .. } => consensus("context.unknown_sprout_anchor"),
Self::UnknownSaplingAnchor { .. } => consensus("context.unknown_sapling_anchor"),
Self::UnknownOrchardAnchor { .. } => consensus("context.unknown_orchard_anchor"),
Self::UnknownIronwoodAnchor { .. } => consensus("context.unknown_ironwood_anchor"),
}
}
fn misbehavior_score(&self) -> u32 {
match self {
ValidateContextError::NonSequentialBlock { .. }
| ValidateContextError::TimeTooEarly { .. }
| ValidateContextError::TimeTooLate { .. }
| ValidateContextError::InvalidDifficultyThreshold { .. }
| ValidateContextError::DuplicateTransparentSpend { .. }
| ValidateContextError::MissingTransparentOutput { .. }
| ValidateContextError::EarlyTransparentSpend { .. }
| ValidateContextError::UnshieldedTransparentCoinbaseSpend { .. }
| ValidateContextError::ImmatureTransparentCoinbaseSpend { .. }
| ValidateContextError::DuplicateSproutNullifier { .. }
| ValidateContextError::DuplicateSaplingNullifier { .. }
| ValidateContextError::DuplicateOrchardNullifier { .. }
| ValidateContextError::DuplicateIronwoodNullifier { .. }
| ValidateContextError::NegativeRemainingTransactionValue { .. }
| ValidateContextError::AddValuePool { .. }
| ValidateContextError::InvalidBlockCommitment(_)
| ValidateContextError::UnknownSproutAnchor { .. }
| ValidateContextError::UnknownSaplingAnchor { .. }
| ValidateContextError::UnknownOrchardAnchor { .. }
| ValidateContextError::UnknownIronwoodAnchor { .. } => 100,
ValidateContextError::CalculateRemainingTransactionValue { .. }
| ValidateContextError::CalculateTransactionValueBalances { .. }
| ValidateContextError::CalculateBlockChainValueChange { .. }
| ValidateContextError::MissingSproutTipTree(_)
| ValidateContextError::BlockPreviouslyInvalidated { .. }
| ValidateContextError::NotReadyToBeCommitted
| ValidateContextError::InvalidAncestorBlock(_)
| ValidateContextError::VctSuppliedRootUnavailable { .. }
| ValidateContextError::VctSuppliedRootAwaitingSuccessor { .. }
| ValidateContextError::VctBlockAuthDataRootMismatch { .. }
| ValidateContextError::VctSproutHandoffRootMismatch { .. }
| ValidateContextError::CumulativeWorkOverflow { .. }
| ValidateContextError::OrphanedBlock { .. }
| ValidateContextError::NoteCommitmentTreeError(_)
| ValidateContextError::HistoryTreeError(_) => 0,
}
}
pub fn vct_supplied_root_unavailable_height(&self) -> Option<block::Height> {
match self {
ValidateContextError::VctSuppliedRootUnavailable { height } => Some(*height),
_ => None,
}
}
pub fn vct_retryable_height(&self) -> Option<block::Height> {
match self {
ValidateContextError::VctSuppliedRootUnavailable { height }
| ValidateContextError::VctSuppliedRootAwaitingSuccessor { height } => Some(*height),
_ => None,
}
}
}
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,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use zakura_header_chain::{
BodyCommitmentKind, BodyVerificationClass, TransientBodyFailureKind,
};
#[test]
fn body_verification_classes_preserve_attribution_boundaries() {
assert_eq!(
ValidateContextError::VctSuppliedRootUnavailable { height: Height(7) }
.body_verification_class(),
BodyVerificationClass::Retryable(TransientBodyFailureKind::MissingContext)
);
assert_eq!(
ValidateContextError::InvalidAncestorBlock(block::Hash([9; 32]))
.body_verification_class(),
BodyVerificationClass::Retryable(TransientBodyFailureKind::Canceled)
);
assert_eq!(
ValidateContextError::VctBlockAuthDataRootMismatch {
height: Height(7),
expected: block::merkle::AuthDataRoot::from([1; 32]),
actual: block::merkle::AuthDataRoot::from([2; 32]),
}
.body_verification_class(),
BodyVerificationClass::PayloadMismatch(BodyCommitmentKind::AuthDataRoot)
);
assert_eq!(
ValidateContextError::DuplicateTransparentSpend {
outpoint: transparent::OutPoint {
hash: [3; 32].into(),
index: 0,
},
location: "test chain",
}
.body_verification_class(),
BodyVerificationClass::ConsensusInvalid(zakura_header_chain::BodyRuleId::new(
"context.duplicate_transparent_spend"
))
);
assert_eq!(
CommitBlockError::HeaderChainError {
error: "local transition failure".to_owned(),
}
.body_verification_class(),
BodyVerificationClass::Retryable(TransientBodyFailureKind::Storage)
);
assert_eq!(
CommitBlockError::Duplicate {
hash_or_height: None,
location: KnownBlock::BestChain,
}
.body_verification_class(),
BodyVerificationClass::Duplicate
);
}
#[test]
fn contextual_body_failure_classes_match_header_engine_contract() {
use zakura_chain::value_balance::ValueBalanceError;
use zakura_header_chain::{BodyRuleId, BodyVerificationClass};
let outpoint = transparent::OutPoint {
hash: [3; 32].into(),
index: 0,
};
let transaction_hash = transaction::Hash::from([4; 32]);
let now = Utc::now();
let cases = [
(
ValidateContextError::DuplicateTransparentSpend {
outpoint,
location: "test chain",
}
.body_verification_class(),
BodyVerificationClass::ConsensusInvalid(BodyRuleId::new(
"context.duplicate_transparent_spend",
)),
),
(
ValidateContextError::DuplicateSproutNullifier {
nullifier: sprout::Nullifier::from([6; 32]),
in_finalized_state: false,
}
.body_verification_class(),
BodyVerificationClass::ConsensusInvalid(BodyRuleId::new(
"context.duplicate_sprout_nullifier",
)),
),
(
ValidateContextError::UnknownSproutAnchor {
anchor: sprout::tree::Root::default(),
height: Some(Height(7)),
tx_index_in_block: Some(0),
transaction_hash,
}
.body_verification_class(),
BodyVerificationClass::ConsensusInvalid(BodyRuleId::new(
"context.unknown_sprout_anchor",
)),
),
(
ValidateContextError::CalculateBlockChainValueChange {
value_balance_error: ValueBalanceError::Unparsable,
height: Height(7),
block_hash: block::Hash([5; 32]),
transaction_count: 1,
spent_utxo_count: 1,
}
.body_verification_class(),
BodyVerificationClass::ConsensusInvalid(BodyRuleId::new(
"context.calculate_block_chain_value_change",
)),
),
(
ValidateContextError::TimeTooLate {
candidate_time: now,
block_time_max: now - chrono::Duration::seconds(1),
}
.body_verification_class(),
BodyVerificationClass::Retryable(TransientBodyFailureKind::VerifierUnavailable),
),
(
CommitBlockError::HeaderChainError {
error: "local transition failure".to_owned(),
}
.body_verification_class(),
BodyVerificationClass::Retryable(TransientBodyFailureKind::Storage),
),
];
for (actual, expected) in cases {
assert_eq!(actual, expected);
}
}
use zakura_chain::{
block::{CommitmentError, Height},
parameters::Network,
work::difficulty::{ParameterDifficulty, INVALID_COMPACT_DIFFICULTY},
};
#[test]
fn commit_block_error_misbehavior_scores() {
let block_time = DateTime::from_timestamp(1_000_000, 0)
.expect("test timestamp is in the supported range");
let height = Height(5);
let transaction_hash = transaction::Hash([2; 32]);
let outpoint = transparent::OutPoint {
hash: transaction_hash,
index: 0,
};
let amount_error = amount::Error::Constraint {
value: -1,
range: 0..=1,
};
let value_balance_error = ValueBalanceError::Transparent(amount_error.clone());
let orchard_nullifier = orchard::Nullifier::try_from([0; 32])
.expect("zero is a canonical Orchard nullifier encoding");
let arithmetic_faults = [
ValidateContextError::CalculateRemainingTransactionValue {
amount_error: amount_error.clone(),
height,
tx_index_in_block: 1,
transaction_hash,
},
ValidateContextError::CalculateTransactionValueBalances {
value_balance_error: value_balance_error.clone(),
height,
tx_index_in_block: 1,
transaction_hash,
},
ValidateContextError::CalculateBlockChainValueChange {
value_balance_error: value_balance_error.clone(),
height,
block_hash: block::Hash([3; 32]),
transaction_count: 2,
spent_utxo_count: 1,
},
];
let peer_faults = [
ValidateContextError::NonSequentialBlock {
candidate_height: height,
parent_height: Height(3),
},
ValidateContextError::TimeTooEarly {
candidate_time: block_time,
median_time_past: block_time,
},
ValidateContextError::TimeTooLate {
candidate_time: block_time,
block_time_max: block_time,
},
ValidateContextError::InvalidDifficultyThreshold {
difficulty_threshold: INVALID_COMPACT_DIFFICULTY,
expected_difficulty: Network::Mainnet.target_difficulty_limit().to_compact(),
},
ValidateContextError::DuplicateTransparentSpend {
outpoint,
location: "test chain",
},
ValidateContextError::MissingTransparentOutput {
outpoint,
location: "test chain",
},
ValidateContextError::EarlyTransparentSpend { outpoint },
ValidateContextError::UnshieldedTransparentCoinbaseSpend { outpoint },
ValidateContextError::ImmatureTransparentCoinbaseSpend {
outpoint,
spend_height: height,
min_spend_height: Height(100),
created_height: Height(1),
},
ValidateContextError::DuplicateSproutNullifier {
nullifier: sprout::Nullifier::from([0; 32]),
in_finalized_state: false,
},
ValidateContextError::DuplicateSaplingNullifier {
nullifier: sapling::Nullifier::from([0; 32]),
in_finalized_state: false,
},
ValidateContextError::DuplicateOrchardNullifier {
nullifier: orchard_nullifier,
in_finalized_state: false,
},
ValidateContextError::DuplicateIronwoodNullifier {
nullifier: orchard_nullifier,
in_finalized_state: false,
},
ValidateContextError::NegativeRemainingTransactionValue {
amount_error: amount_error.clone(),
height,
tx_index_in_block: 1,
transaction_hash,
},
ValidateContextError::AddValuePool {
value_balance_error,
chain_value_pools: Box::new(ValueBalance::<NonNegative>::zero()),
block_value_pool_change: Box::new(ValueBalance::<NegativeAllowed>::zero()),
height: Some(height),
},
ValidateContextError::InvalidBlockCommitment(
CommitmentError::InvalidChainHistoryActivationReserved { actual: [1; 32] },
),
ValidateContextError::UnknownSproutAnchor {
anchor: sprout::tree::Root::default(),
height: Some(height),
tx_index_in_block: Some(1),
transaction_hash,
},
ValidateContextError::UnknownSaplingAnchor {
anchor: sapling::tree::Root::default(),
height: Some(height),
tx_index_in_block: Some(1),
transaction_hash,
},
ValidateContextError::UnknownOrchardAnchor {
anchor: orchard::tree::Root::default(),
height: Some(height),
tx_index_in_block: Some(1),
transaction_hash,
},
ValidateContextError::UnknownIronwoodAnchor {
anchor: ironwood::tree::Root::default(),
height: Some(height),
tx_index_in_block: Some(1),
transaction_hash,
},
];
for error in peer_faults {
let commit_error = CommitBlockError::ValidateContextError(Box::new(error));
assert_eq!(
commit_error.misbehavior_score(),
100,
"direct contextual consensus failure must be scored: {commit_error:?}"
);
}
for error in arithmetic_faults {
let commit_error = CommitBlockError::ValidateContextError(Box::new(error));
assert_eq!(
commit_error.misbehavior_score(),
0,
"value-summation arithmetic failure must not be attributed to a peer: \
{commit_error:?}"
);
}
let transient_context_error = CommitBlockError::ValidateContextError(Box::new(
ValidateContextError::NotReadyToBeCommitted,
));
assert_eq!(transient_context_error.misbehavior_score(), 0);
let invalid_ancestor_error = CommitBlockError::ValidateContextError(Box::new(
ValidateContextError::InvalidAncestorBlock(block::Hash([1; 32])),
));
assert_eq!(invalid_ancestor_error.misbehavior_score(), 0);
let stale_fork_error =
CommitBlockError::ValidateContextError(Box::new(ValidateContextError::OrphanedBlock {
candidate_height: Height(3),
finalized_tip_height: height,
}));
assert_eq!(stale_fork_error.misbehavior_score(), 0);
let dup_err = CommitBlockError::Duplicate {
hash_or_height: None,
location: KnownBlock::BestChain,
};
assert_eq!(dup_err.misbehavior_score(), 0);
}
#[test]
fn checkpoint_error_exposes_retryable_vct_root_height() {
let height = Height(42);
let retryable =
CommitCheckpointVerifiedError::from(ValidateContextError::VctSuppliedRootUnavailable {
height,
})
.with_vct_failure(VctCommitFailure::SuccessorBoundary);
assert_eq!(
retryable.vct_supplied_root_unavailable_height(),
Some(height),
"checkpoint commit errors expose retryable VCT root misses"
);
assert_eq!(
retryable.vct_failure(),
Some(VctCommitFailure::SuccessorBoundary),
"checkpoint errors preserve the exact VCT verifier stage"
);
let non_retryable: CommitCheckpointVerifiedError =
ValidateContextError::NonSequentialBlock {
candidate_height: Height(5),
parent_height: Height(3),
}
.into();
assert_eq!(
non_retryable.vct_supplied_root_unavailable_height(),
None,
"unrelated validation errors are not treated as VCT root misses"
);
assert_eq!(
non_retryable.vct_retryable_height(),
None,
"unrelated validation errors are not retryable VCT stalls"
);
}
#[test]
fn await_successor_is_retryable_but_not_root_unavailable() {
let height = Height(7);
let awaiting: CommitCheckpointVerifiedError =
ValidateContextError::VctSuppliedRootAwaitingSuccessor { height }.into();
assert_eq!(
awaiting.vct_retryable_height(),
Some(height),
"an await-successor stall is retryable",
);
assert_eq!(
awaiting.vct_supplied_root_unavailable_height(),
None,
"an await-successor stall is not a missing root (the root is present)",
);
let unavailable: CommitCheckpointVerifiedError =
ValidateContextError::VctSuppliedRootUnavailable { height }.into();
assert_eq!(unavailable.vct_retryable_height(), Some(height));
assert_eq!(
unavailable.vct_supplied_root_unavailable_height(),
Some(height)
);
}
}