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