2020-11-19 19:06:10 -08:00
|
|
|
|
//! Transaction checks.
|
|
|
|
|
//!
|
|
|
|
|
//! Code in this file can freely assume that no pre-V4 transactions are present.
|
|
|
|
|
|
2021-10-27 19:49:28 -07:00
|
|
|
|
use std::{borrow::Cow, collections::HashSet, convert::TryFrom, hash::Hash};
|
|
|
|
|
|
2020-10-16 14:40:00 -07:00
|
|
|
|
use zebra_chain::{
|
2021-07-01 16:03:34 -07:00
|
|
|
|
amount::{Amount, NonNegative},
|
|
|
|
|
block::Height,
|
2021-06-02 18:54:08 -07:00
|
|
|
|
orchard::Flags,
|
2021-07-01 16:03:34 -07:00
|
|
|
|
parameters::{Network, NetworkUpgrade},
|
2021-06-03 15:53:00 -07:00
|
|
|
|
sapling::{Output, PerSpendAnchor, Spend},
|
2021-03-31 14:34:25 -07:00
|
|
|
|
transaction::Transaction,
|
2020-10-16 14:40:00 -07:00
|
|
|
|
};
|
|
|
|
|
|
2020-10-26 23:42:27 -07:00
|
|
|
|
use crate::error::TransactionError;
|
2020-10-16 14:40:00 -07:00
|
|
|
|
|
2020-11-19 19:13:52 -08:00
|
|
|
|
/// Checks that the transaction has inputs and outputs.
|
|
|
|
|
///
|
2021-04-27 17:43:00 -07:00
|
|
|
|
/// For `Transaction::V4`:
|
2021-06-28 15:28:49 -07:00
|
|
|
|
/// * At least one of `tx_in_count`, `nSpendsSapling`, and `nJoinSplit` MUST be non-zero.
|
|
|
|
|
/// * At least one of `tx_out_count`, `nOutputsSapling`, and `nJoinSplit` MUST be non-zero.
|
2020-11-19 19:13:52 -08:00
|
|
|
|
///
|
2021-04-27 17:43:00 -07:00
|
|
|
|
/// For `Transaction::V5`:
|
2021-06-28 15:28:49 -07:00
|
|
|
|
/// * This condition must hold: `tx_in_count` > 0 or `nSpendsSapling` > 0 or
|
|
|
|
|
/// (`nActionsOrchard` > 0 and `enableSpendsOrchard` = 1)
|
|
|
|
|
/// * This condition must hold: `tx_out_count` > 0 or `nOutputsSapling` > 0 or
|
|
|
|
|
/// (`nActionsOrchard` > 0 and `enableOutputsOrchard` = 1)
|
2021-04-27 17:43:00 -07:00
|
|
|
|
///
|
|
|
|
|
/// This check counts both `Coinbase` and `PrevOut` transparent inputs.
|
2020-10-16 14:40:00 -07:00
|
|
|
|
///
|
2021-03-02 11:35:13 -08:00
|
|
|
|
/// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
|
2020-11-19 19:13:52 -08:00
|
|
|
|
pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> {
|
2021-07-28 21:23:50 -07:00
|
|
|
|
if !tx.has_transparent_or_shielded_inputs() {
|
2021-04-27 17:43:00 -07:00
|
|
|
|
Err(TransactionError::NoInputs)
|
2021-07-28 21:23:50 -07:00
|
|
|
|
} else if !tx.has_transparent_or_shielded_outputs() {
|
2021-04-27 17:43:00 -07:00
|
|
|
|
Err(TransactionError::NoOutputs)
|
|
|
|
|
} else {
|
|
|
|
|
Ok(())
|
2020-10-16 14:40:00 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-27 17:43:00 -07:00
|
|
|
|
/// Check that a coinbase transaction has no PrevOut inputs, JoinSplits, or spends.
|
|
|
|
|
///
|
|
|
|
|
/// A coinbase transaction MUST NOT have any transparent inputs, JoinSplit descriptions,
|
|
|
|
|
/// or Spend descriptions.
|
|
|
|
|
///
|
|
|
|
|
/// In a version 5 coinbase transaction, the enableSpendsOrchard flag MUST be 0.
|
|
|
|
|
///
|
|
|
|
|
/// This check only counts `PrevOut` transparent inputs.
|
2020-10-13 23:10:18 -07:00
|
|
|
|
///
|
2021-03-02 11:35:13 -08:00
|
|
|
|
/// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
|
2021-04-27 17:43:00 -07:00
|
|
|
|
pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), TransactionError> {
|
2021-09-01 17:06:20 -07:00
|
|
|
|
if tx.has_valid_coinbase_transaction_inputs() {
|
2021-04-27 17:43:00 -07:00
|
|
|
|
if tx.contains_prevout_input() {
|
|
|
|
|
return Err(TransactionError::CoinbaseHasPrevOutInput);
|
|
|
|
|
} else if tx.joinsplit_count() > 0 {
|
|
|
|
|
return Err(TransactionError::CoinbaseHasJoinSplit);
|
|
|
|
|
} else if tx.sapling_spends_per_anchor().count() > 0 {
|
|
|
|
|
return Err(TransactionError::CoinbaseHasSpend);
|
2020-10-13 23:10:18 -07:00
|
|
|
|
}
|
2021-04-27 17:43:00 -07:00
|
|
|
|
|
2021-06-02 18:54:08 -07:00
|
|
|
|
if let Some(orchard_shielded_data) = tx.orchard_shielded_data() {
|
|
|
|
|
if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) {
|
|
|
|
|
return Err(TransactionError::CoinbaseHasEnableSpendsOrchard);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-10-13 23:10:18 -07:00
|
|
|
|
}
|
2021-04-27 17:43:00 -07:00
|
|
|
|
|
|
|
|
|
Ok(())
|
2020-10-13 23:10:18 -07:00
|
|
|
|
}
|
2021-03-24 09:28:25 -07:00
|
|
|
|
|
|
|
|
|
/// Check that a Spend description's cv and rk are not of small order,
|
|
|
|
|
/// i.e. [h_J]cv MUST NOT be 𝒪_J and [h_J]rk MUST NOT be 𝒪_J.
|
|
|
|
|
///
|
|
|
|
|
/// https://zips.z.cash/protocol/protocol.pdf#spenddesc
|
2021-03-31 14:34:25 -07:00
|
|
|
|
pub fn spend_cv_rk_not_small_order(spend: &Spend<PerSpendAnchor>) -> Result<(), TransactionError> {
|
2021-03-24 09:28:25 -07:00
|
|
|
|
if bool::from(spend.cv.0.is_small_order())
|
|
|
|
|
|| bool::from(
|
|
|
|
|
jubjub::AffinePoint::from_bytes(spend.rk.into())
|
|
|
|
|
.unwrap()
|
|
|
|
|
.is_small_order(),
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
Err(TransactionError::SmallOrder)
|
|
|
|
|
} else {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check that a Output description's cv and epk are not of small order,
|
|
|
|
|
/// i.e. [h_J]cv MUST NOT be 𝒪_J and [h_J]epk MUST NOT be 𝒪_J.
|
|
|
|
|
///
|
|
|
|
|
/// https://zips.z.cash/protocol/protocol.pdf#outputdesc
|
|
|
|
|
pub fn output_cv_epk_not_small_order(output: &Output) -> Result<(), TransactionError> {
|
|
|
|
|
if bool::from(output.cv.0.is_small_order())
|
|
|
|
|
|| bool::from(
|
|
|
|
|
jubjub::AffinePoint::from_bytes(output.ephemeral_key.into())
|
|
|
|
|
.unwrap()
|
|
|
|
|
.is_small_order(),
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
Err(TransactionError::SmallOrder)
|
|
|
|
|
} else {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-07-01 16:03:34 -07:00
|
|
|
|
|
|
|
|
|
/// Check if a transaction is adding to the sprout pool after Canopy
|
|
|
|
|
/// network upgrade given a block height and a network.
|
|
|
|
|
///
|
|
|
|
|
/// https://zips.z.cash/zip-0211
|
|
|
|
|
/// https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
|
|
|
|
|
pub fn disabled_add_to_sprout_pool(
|
|
|
|
|
tx: &Transaction,
|
|
|
|
|
height: Height,
|
|
|
|
|
network: Network,
|
|
|
|
|
) -> Result<(), TransactionError> {
|
|
|
|
|
let canopy_activation_height = NetworkUpgrade::Canopy
|
|
|
|
|
.activation_height(network)
|
|
|
|
|
.expect("Canopy activation height must be present for both networks");
|
|
|
|
|
|
|
|
|
|
// [Canopy onward]: `vpub_old` MUST be zero.
|
|
|
|
|
// https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc
|
|
|
|
|
if height >= canopy_activation_height {
|
|
|
|
|
let zero = Amount::<NonNegative>::try_from(0).expect("an amount of 0 is always valid");
|
|
|
|
|
|
2021-08-09 10:22:26 -07:00
|
|
|
|
let tx_sprout_pool = tx.output_values_to_sprout();
|
2021-07-01 16:03:34 -07:00
|
|
|
|
for vpub_old in tx_sprout_pool {
|
|
|
|
|
if *vpub_old != zero {
|
|
|
|
|
return Err(TransactionError::DisabledAddToSproutPool);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2021-10-27 19:49:28 -07:00
|
|
|
|
|
|
|
|
|
/// Check if a transaction has any internal spend conflicts.
|
|
|
|
|
///
|
|
|
|
|
/// An internal spend conflict happens if the transaction spends a UTXO more than once or if it
|
|
|
|
|
/// reveals a nullifier more than once.
|
|
|
|
|
///
|
|
|
|
|
/// Consensus rules:
|
|
|
|
|
///
|
|
|
|
|
/// "each output of a particular transaction
|
|
|
|
|
/// can only be used as an input once in the block chain.
|
|
|
|
|
/// Any subsequent reference is a forbidden double spend-
|
|
|
|
|
/// an attempt to spend the same satoshis twice."
|
|
|
|
|
///
|
|
|
|
|
/// https://developer.bitcoin.org/devguide/block_chain.html#introduction
|
|
|
|
|
///
|
|
|
|
|
/// A _nullifier_ *MUST NOT* repeat either within a _transaction_, or across _transactions_ in a
|
|
|
|
|
/// _valid blockchain_ . *Sprout* and *Sapling* and *Orchard* _nulliers_ are considered disjoint,
|
|
|
|
|
/// even if they have the same bit pattern.
|
|
|
|
|
///
|
|
|
|
|
/// https://zips.z.cash/protocol/protocol.pdf#nullifierset
|
|
|
|
|
pub fn spend_conflicts(transaction: &Transaction) -> Result<(), TransactionError> {
|
|
|
|
|
use crate::error::TransactionError::*;
|
|
|
|
|
|
|
|
|
|
let transparent_outpoints = transaction.spent_outpoints().map(Cow::Owned);
|
|
|
|
|
let sprout_nullifiers = transaction.sprout_nullifiers().map(Cow::Borrowed);
|
|
|
|
|
let sapling_nullifiers = transaction.sapling_nullifiers().map(Cow::Borrowed);
|
|
|
|
|
let orchard_nullifiers = transaction.orchard_nullifiers().map(Cow::Borrowed);
|
|
|
|
|
|
|
|
|
|
check_for_duplicates(transparent_outpoints, DuplicateTransparentSpend)?;
|
|
|
|
|
check_for_duplicates(sprout_nullifiers, DuplicateSproutNullifier)?;
|
|
|
|
|
check_for_duplicates(sapling_nullifiers, DuplicateSaplingNullifier)?;
|
|
|
|
|
check_for_duplicates(orchard_nullifiers, DuplicateOrchardNullifier)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check for duplicate items in a collection.
|
|
|
|
|
///
|
|
|
|
|
/// Each item should be wrapped by a [`Cow`] instance so that this helper function can properly
|
|
|
|
|
/// handle borrowed items and owned items.
|
|
|
|
|
///
|
|
|
|
|
/// If a duplicate is found, an error created by the `error_wrapper` is returned.
|
|
|
|
|
fn check_for_duplicates<'t, T>(
|
|
|
|
|
items: impl IntoIterator<Item = Cow<'t, T>>,
|
|
|
|
|
error_wrapper: impl FnOnce(T) -> TransactionError,
|
|
|
|
|
) -> Result<(), TransactionError>
|
|
|
|
|
where
|
|
|
|
|
T: Clone + Eq + Hash + 't,
|
|
|
|
|
{
|
|
|
|
|
let mut hash_set = HashSet::new();
|
|
|
|
|
|
|
|
|
|
for item in items {
|
|
|
|
|
if let Some(duplicate) = hash_set.replace(item) {
|
|
|
|
|
return Err(error_wrapper(duplicate.into_owned()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|