Skip to main content

zebra_consensus/
error.rs

1//! Errors that can occur when checking consensus rules.
2//!
3//! Each error variant corresponds to a consensus rule, so enumerating
4//! all possible verification failures enumerates the consensus rules we
5//! implement, and ensures that we don't reject blocks or transactions
6//! for a non-enumerated reason.
7
8use std::{array::TryFromSliceError, convert::Infallible};
9
10use chrono::{DateTime, Utc};
11use thiserror::Error;
12
13use zcash_protocol::value::BalanceError;
14use zebra_chain::{
15    amount, block, orchard,
16    parameters::subsidy::SubsidyError,
17    sapling, sprout,
18    transparent::{self, MIN_TRANSPARENT_COINBASE_MATURITY},
19};
20use zebra_state::ValidateContextError;
21
22use crate::{block::MAX_BLOCK_SIGOPS, BoxError};
23
24#[cfg(any(test, feature = "proptest-impl"))]
25use proptest_derive::Arbitrary;
26
27/// Workaround for format string identifier rules.
28const MAX_EXPIRY_HEIGHT: block::Height = block::Height::MAX_EXPIRY_HEIGHT;
29
30/// Errors for semantic transaction validation.
31#[derive(Error, Clone, Debug, PartialEq, Eq)]
32#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
33#[allow(missing_docs)]
34pub enum TransactionError {
35    #[error("first transaction must be coinbase")]
36    CoinbasePosition,
37
38    #[error("coinbase input found in non-coinbase transaction")]
39    CoinbaseAfterFirst,
40
41    #[error("coinbase transaction MUST NOT have any JoinSplit descriptions")]
42    CoinbaseHasJoinSplit,
43
44    #[error("coinbase transaction MUST NOT have any Spend descriptions")]
45    CoinbaseHasSpend,
46
47    #[error("coinbase transaction MUST NOT have any Output descriptions pre-Heartwood")]
48    CoinbaseHasOutputPreHeartwood,
49
50    #[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")]
51    CoinbaseHasEnableSpendsOrchard,
52
53    #[error("coinbase transaction Sapling or Orchard outputs MUST be decryptable with an all-zero outgoing viewing key")]
54    CoinbaseOutputsNotDecryptable,
55
56    #[error("coinbase inputs MUST NOT exist in mempool")]
57    CoinbaseInMempool,
58
59    #[error("non-coinbase transactions MUST NOT have coinbase inputs")]
60    NonCoinbaseHasCoinbaseInput,
61
62    #[error("the tx is not coinbase, but it should be")]
63    NotCoinbase,
64
65    #[error("transaction is locked until after block height {}", _0.0)]
66    LockedUntilAfterBlockHeight(block::Height),
67
68    #[error("transaction is locked until after block time {0}")]
69    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
70    LockedUntilAfterBlockTime(DateTime<Utc>),
71
72    #[error(
73        "coinbase expiry {expiry_height:?} must be the same as the block {block_height:?} \
74         after NU5 activation, failing transaction: {transaction_hash:?}"
75    )]
76    CoinbaseExpiryBlockHeight {
77        expiry_height: Option<zebra_chain::block::Height>,
78        block_height: zebra_chain::block::Height,
79        transaction_hash: zebra_chain::transaction::Hash,
80    },
81
82    #[error("could not construct coinbase tx: {0}")]
83    CoinbaseConstruction(String),
84
85    #[error(
86        "expiry {expiry_height:?} must be less than the maximum {MAX_EXPIRY_HEIGHT:?} \
87         coinbase: {is_coinbase}, block: {block_height:?}, failing transaction: {transaction_hash:?}"
88    )]
89    MaximumExpiryHeight {
90        expiry_height: zebra_chain::block::Height,
91        is_coinbase: bool,
92        block_height: zebra_chain::block::Height,
93        transaction_hash: zebra_chain::transaction::Hash,
94    },
95
96    #[error(
97        "transaction must not be mined at a block {block_height:?} \
98         greater than its expiry {expiry_height:?}, failing transaction {transaction_hash:?}"
99    )]
100    ExpiredTransaction {
101        expiry_height: zebra_chain::block::Height,
102        block_height: zebra_chain::block::Height,
103        transaction_hash: zebra_chain::transaction::Hash,
104    },
105
106    #[error("coinbase transaction failed subsidy validation: {0}")]
107    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
108    Subsidy(#[from] SubsidyError),
109
110    #[error("transaction version number MUST be >= 4")]
111    WrongVersion,
112
113    #[error("transaction version {0} not supported by the network upgrade {1:?}")]
114    UnsupportedByNetworkUpgrade(u32, zebra_chain::parameters::NetworkUpgrade),
115
116    #[error("must have at least one input: transparent, shielded spend, or joinsplit")]
117    NoInputs,
118
119    #[error("must have at least one output: transparent, shielded output, or joinsplit")]
120    NoOutputs,
121
122    #[error("if there are no Spends or Outputs, the value balance MUST be 0.")]
123    BadBalance,
124
125    #[error("could not verify a transparent script: {0}")]
126    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
127    Script(#[from] zebra_script::Error),
128
129    #[error("spend description cv and rk MUST NOT be of small order")]
130    SmallOrder,
131
132    // TODO: the underlying error is bellman::VerificationError, but it does not implement
133    // Arbitrary as required here.
134    #[error("spend proof MUST be valid given a primary input formed from the other fields except spendAuthSig: {0}")]
135    Groth16(String),
136
137    // TODO: the underlying error is io::Error, but it does not implement Clone as required here.
138    #[error("Groth16 proof is malformed: {0}")]
139    MalformedGroth16(String),
140
141    #[error(
142        "Sprout joinSplitSig MUST represent a valid signature under joinSplitPubKey of dataToBeSigned: {0}"
143    )]
144    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
145    Ed25519(#[from] zebra_chain::primitives::ed25519::Error),
146
147    #[error("Sapling bindingSig MUST represent a valid signature under the transaction binding validating key bvk of SigHash: {0}")]
148    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
149    RedJubjub(zebra_chain::primitives::redjubjub::Error),
150
151    #[error("Orchard bindingSig MUST represent a valid signature under the transaction binding validating key bvk of SigHash: {0}")]
152    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
153    RedPallas(zebra_chain::primitives::reddsa::Error),
154
155    // temporary error type until #1186 is fixed
156    #[error("Downcast from BoxError to redjubjub::Error failed: {0}")]
157    InternalDowncastError(String),
158
159    #[error("either vpub_old or vpub_new must be zero")]
160    BothVPubsNonZero,
161
162    #[error("adding to the sprout pool is disabled after Canopy")]
163    DisabledAddToSproutPool,
164
165    #[error("could not calculate the transaction fee")]
166    IncorrectFee,
167
168    #[error("transparent double-spend: {_0:?} is spent twice")]
169    DuplicateTransparentSpend(transparent::OutPoint),
170
171    #[error("sprout double-spend: duplicate nullifier: {_0:?}")]
172    DuplicateSproutNullifier(sprout::Nullifier),
173
174    #[error("sapling double-spend: duplicate nullifier: {_0:?}")]
175    DuplicateSaplingNullifier(sapling::Nullifier),
176
177    #[error("orchard double-spend: duplicate nullifier: {_0:?}")]
178    DuplicateOrchardNullifier(orchard::Nullifier),
179
180    #[error("must have at least one active orchard flag")]
181    NotEnoughFlags,
182
183    #[error("could not find transparent input UTXO in the best chain or mempool")]
184    TransparentInputNotFound,
185
186    #[error("could not contextually validate transaction on best chain: {0}")]
187    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
188    // This error variant is at least 128 bytes
189    ValidateContextError(Box<ValidateContextError>),
190
191    #[error("could not validate mempool transaction lock time on best chain: {0}")]
192    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
193    // TODO: turn this into a typed error
194    ValidateMempoolLockTimeError(String),
195
196    #[error(
197        "immature transparent coinbase spend: \
198        attempt to spend {outpoint:?} at {spend_height:?}, \
199        but spends are invalid before {min_spend_height:?}, \
200        which is {MIN_TRANSPARENT_COINBASE_MATURITY:?} blocks \
201        after it was created at {created_height:?}"
202    )]
203    #[non_exhaustive]
204    ImmatureTransparentCoinbaseSpend {
205        outpoint: transparent::OutPoint,
206        spend_height: block::Height,
207        min_spend_height: block::Height,
208        created_height: block::Height,
209    },
210
211    #[error(
212        "unshielded transparent coinbase spend: {outpoint:?} \
213         must be spent in a transaction which only has shielded outputs"
214    )]
215    #[non_exhaustive]
216    UnshieldedTransparentCoinbaseSpend {
217        outpoint: transparent::OutPoint,
218        min_spend_height: block::Height,
219    },
220
221    #[error(
222        "failed to verify ZIP-317 transaction rules, transaction was not inserted to mempool: {0}"
223    )]
224    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
225    Zip317(#[from] zebra_chain::transaction::zip317::Error),
226
227    #[error("transaction uses an incorrect consensus branch id")]
228    WrongConsensusBranchId,
229
230    #[error("wrong tx format: tx version is ≥ 5, but `nConsensusBranchId` is missing")]
231    MissingConsensusBranchId,
232
233    #[error("input/output error")]
234    Io(String),
235
236    #[error("failed to convert a slice")]
237    TryFromSlice(String),
238
239    #[error("invalid amount")]
240    Amount(String),
241
242    #[error("invalid balance")]
243    Balance(String),
244
245    #[error("Orchard proof has a non-canonical size")]
246    OrchardProofSize,
247
248    #[error("unexpected error")]
249    Other(String),
250}
251
252impl From<ValidateContextError> for TransactionError {
253    fn from(err: ValidateContextError) -> Self {
254        TransactionError::ValidateContextError(Box::new(err))
255    }
256}
257
258impl From<zcash_transparent::builder::Error> for TransactionError {
259    fn from(err: zcash_transparent::builder::Error) -> Self {
260        TransactionError::CoinbaseConstruction(err.to_string())
261    }
262}
263
264impl From<zcash_primitives::transaction::builder::Error<Infallible>> for TransactionError {
265    fn from(err: zcash_primitives::transaction::builder::Error<Infallible>) -> Self {
266        TransactionError::CoinbaseConstruction(err.to_string())
267    }
268}
269
270impl From<BalanceError> for TransactionError {
271    fn from(err: BalanceError) -> Self {
272        TransactionError::Balance(err.to_string())
273    }
274}
275
276impl From<libzcash_script::Error> for TransactionError {
277    fn from(err: libzcash_script::Error) -> Self {
278        TransactionError::Script(zebra_script::Error::from(err))
279    }
280}
281
282impl From<std::io::Error> for TransactionError {
283    fn from(err: std::io::Error) -> Self {
284        TransactionError::Io(err.to_string())
285    }
286}
287
288impl From<TryFromSliceError> for TransactionError {
289    fn from(err: TryFromSliceError) -> Self {
290        TransactionError::TryFromSlice(err.to_string())
291    }
292}
293
294impl From<amount::Error> for TransactionError {
295    fn from(err: amount::Error) -> Self {
296        TransactionError::Amount(err.to_string())
297    }
298}
299
300// TODO: use a dedicated variant and From impl for each concrete type, and update callers (#5732)
301impl From<BoxError> for TransactionError {
302    fn from(mut err: BoxError) -> Self {
303        // TODO: handle redpallas::Error, ScriptInvalid, InvalidSignature
304        match err.downcast::<zebra_chain::primitives::redjubjub::Error>() {
305            Ok(e) => return TransactionError::RedJubjub(*e),
306            Err(e) => err = e,
307        }
308
309        match err.downcast::<ValidateContextError>() {
310            Ok(e) => return (*e).into(),
311            Err(e) => err = e,
312        }
313
314        // buffered transaction verifier service error
315        match err.downcast::<TransactionError>() {
316            Ok(e) => return *e,
317            Err(e) => err = e,
318        }
319
320        TransactionError::InternalDowncastError(format!(
321            "downcast to known transaction error type failed, original error: {err:?}",
322        ))
323    }
324}
325
326impl TransactionError {
327    /// Returns a suggested misbehaviour score increment for a certain error when
328    /// verifying a mempool transaction.
329    pub fn mempool_misbehavior_score(&self) -> u32 {
330        use TransactionError::*;
331
332        // TODO: Adjust these values based on zcashd (#9258).
333        match self {
334            ImmatureTransparentCoinbaseSpend { .. }
335            | UnshieldedTransparentCoinbaseSpend { .. }
336            | CoinbasePosition
337            | CoinbaseAfterFirst
338            | CoinbaseHasJoinSplit
339            | CoinbaseHasSpend
340            | CoinbaseHasOutputPreHeartwood
341            | CoinbaseHasEnableSpendsOrchard
342            | CoinbaseOutputsNotDecryptable
343            | CoinbaseInMempool
344            | NonCoinbaseHasCoinbaseInput
345            | CoinbaseExpiryBlockHeight { .. }
346            | IncorrectFee
347            | Subsidy(_)
348            | WrongVersion
349            | NoInputs
350            | NoOutputs
351            | BadBalance
352            | Script(_)
353            | SmallOrder
354            | Groth16(_)
355            | MalformedGroth16(_)
356            | Ed25519(_)
357            | RedJubjub(_)
358            | RedPallas(_)
359            | BothVPubsNonZero
360            | DisabledAddToSproutPool
361            | NotEnoughFlags
362            | WrongConsensusBranchId
363            | MissingConsensusBranchId
364            | LockedUntilAfterBlockHeight(_)
365            | LockedUntilAfterBlockTime(_) => 100,
366
367            _other => 0,
368        }
369    }
370}
371
372#[derive(Error, Clone, Debug, PartialEq, Eq)]
373#[allow(missing_docs)]
374pub enum BlockError {
375    #[error("block contains invalid transactions")]
376    Transaction(#[from] TransactionError),
377
378    #[error("block has no transactions")]
379    NoTransactions,
380
381    #[error("block has mismatched merkle root")]
382    BadMerkleRoot {
383        actual: zebra_chain::block::merkle::Root,
384        expected: zebra_chain::block::merkle::Root,
385    },
386
387    #[error("block contains duplicate transactions")]
388    DuplicateTransaction,
389
390    #[error("block {0:?} is already in present in the state {1:?}")]
391    AlreadyInChain(zebra_chain::block::Hash, zebra_state::KnownBlock),
392
393    #[error("invalid block {0:?}: missing block height")]
394    MissingHeight(zebra_chain::block::Hash),
395
396    #[error("invalid block height {0:?} in {1:?}: greater than the maximum height {2:?}")]
397    MaxHeight(
398        zebra_chain::block::Height,
399        zebra_chain::block::Hash,
400        zebra_chain::block::Height,
401    ),
402
403    #[error("invalid difficulty threshold in block header {0:?} {1:?}")]
404    InvalidDifficulty(zebra_chain::block::Height, zebra_chain::block::Hash),
405
406    #[error("block {0:?} has a difficulty threshold {2:?} that is easier than the {3:?} difficulty limit {4:?}, hash: {1:?}")]
407    TargetDifficultyLimit(
408        zebra_chain::block::Height,
409        zebra_chain::block::Hash,
410        zebra_chain::work::difficulty::ExpandedDifficulty,
411        zebra_chain::parameters::Network,
412        zebra_chain::work::difficulty::ExpandedDifficulty,
413    ),
414
415    #[error(
416        "block {0:?} on {3:?} has a hash {1:?} that is easier than its difficulty threshold {2:?}"
417    )]
418    DifficultyFilter(
419        zebra_chain::block::Height,
420        zebra_chain::block::Hash,
421        zebra_chain::work::difficulty::ExpandedDifficulty,
422        zebra_chain::parameters::Network,
423    ),
424
425    #[error("transaction has wrong consensus branch id for block network upgrade")]
426    WrongTransactionConsensusBranchId,
427
428    #[error(
429        "block {height:?} {hash:?} has {sigops} legacy transparent signature operations, \
430         but the limit is {MAX_BLOCK_SIGOPS}"
431    )]
432    TooManyTransparentSignatureOperations {
433        height: zebra_chain::block::Height,
434        hash: zebra_chain::block::Hash,
435        sigops: u32,
436    },
437
438    #[error("summing miner fees for block {height:?} {hash:?} failed: {source:?}")]
439    SummingMinerFees {
440        height: zebra_chain::block::Height,
441        hash: zebra_chain::block::Hash,
442        source: amount::Error,
443    },
444
445    #[error("unexpected error occurred: {0}")]
446    Other(String),
447}
448
449impl From<SubsidyError> for BlockError {
450    fn from(err: SubsidyError) -> BlockError {
451        BlockError::Transaction(TransactionError::Subsidy(err))
452    }
453}
454
455impl From<amount::Error> for BlockError {
456    fn from(e: amount::Error) -> Self {
457        Self::from(SubsidyError::from(e))
458    }
459}
460
461impl BlockError {
462    /// Returns `true` if this is definitely a duplicate request.
463    /// Some duplicate requests might not be detected, and therefore return `false`.
464    pub fn is_duplicate_request(&self) -> bool {
465        matches!(self, BlockError::AlreadyInChain(..))
466    }
467
468    /// Returns a suggested misbehaviour score increment for a certain error.
469    pub(crate) fn misbehavior_score(&self) -> u32 {
470        use BlockError::*;
471
472        match self {
473            MissingHeight(_)
474            | MaxHeight(_, _, _)
475            | InvalidDifficulty(_, _)
476            | TargetDifficultyLimit(_, _, _, _, _)
477            | DifficultyFilter(_, _, _, _)
478            | NoTransactions
479            | BadMerkleRoot { .. }
480            | WrongTransactionConsensusBranchId
481            | TooManyTransparentSignatureOperations { .. } => 100,
482            Transaction(err) => err.mempool_misbehavior_score(),
483            _other => 0,
484        }
485    }
486}