use std::{
borrow::Cow,
collections::{HashMap, HashSet},
hash::Hash,
sync::Arc,
};
use chrono::{DateTime, Utc};
use zebra_chain::{
amount::{Amount, NonNegative},
block::Height,
orchard::Flags,
parameters::{Network, NetworkUpgrade},
primitives::zcash_note_encryption,
transaction::{LockTime, Transaction},
transparent,
};
use crate::error::TransactionError;
pub fn lock_time_has_passed(
tx: &Transaction,
block_height: Height,
block_time: impl Into<Option<DateTime<Utc>>>,
) -> Result<(), TransactionError> {
match tx.lock_time() {
Some(LockTime::Height(unlock_height)) => {
if block_height > unlock_height {
Ok(())
} else {
Err(TransactionError::LockedUntilAfterBlockHeight(unlock_height))
}
}
Some(LockTime::Time(unlock_time)) => {
let block_time = block_time
.into()
.expect("time must be provided if LockTime is a time");
if block_time > unlock_time {
Ok(())
} else {
Err(TransactionError::LockedUntilAfterBlockTime(unlock_time))
}
}
None => Ok(()),
}
}
pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> {
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
let has_other_circulation_effects = tx.has_zip233_amount();
#[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))]
let has_other_circulation_effects = false;
if !tx.has_transparent_or_shielded_inputs() {
Err(TransactionError::NoInputs)
} else if !tx.has_transparent_or_shielded_outputs() && !has_other_circulation_effects {
Err(TransactionError::NoOutputs)
} else {
Ok(())
}
}
pub fn has_enough_orchard_flags(tx: &Transaction) -> Result<(), TransactionError> {
if !tx.has_enough_orchard_flags() {
return Err(TransactionError::NotEnoughFlags);
}
Ok(())
}
pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), TransactionError> {
if tx.is_coinbase() {
if tx.joinsplit_count() > 0 {
return Err(TransactionError::CoinbaseHasJoinSplit);
} else if tx.sapling_spends_per_anchor().count() > 0 {
return Err(TransactionError::CoinbaseHasSpend);
}
if let Some(orchard_shielded_data) = tx.orchard_shielded_data() {
if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) {
return Err(TransactionError::CoinbaseHasEnableSpendsOrchard);
}
}
}
Ok(())
}
pub fn joinsplit_has_vpub_zero(tx: &Transaction) -> Result<(), TransactionError> {
let zero = Amount::<NonNegative>::zero();
let vpub_pairs = tx
.output_values_to_sprout()
.zip(tx.input_values_from_sprout());
for (vpub_old, vpub_new) in vpub_pairs {
if *vpub_old != zero && *vpub_new != zero {
return Err(TransactionError::BothVPubsNonZero);
}
}
Ok(())
}
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");
if height >= canopy_activation_height {
let zero = Amount::<NonNegative>::zero();
let tx_sprout_pool = tx.output_values_to_sprout();
for vpub_old in tx_sprout_pool {
if *vpub_old != zero {
return Err(TransactionError::DisabledAddToSproutPool);
}
}
}
Ok(())
}
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(())
}
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(())
}
pub fn coinbase_outputs_are_decryptable(
transaction: &Transaction,
network: &Network,
height: Height,
) -> Result<(), TransactionError> {
if !transaction.has_shielded_outputs() {
return Ok(());
}
if height
< NetworkUpgrade::Heartwood
.activation_height(network)
.expect("Heartwood height is known")
{
return Ok(());
}
if !transaction.is_coinbase() {
return Err(TransactionError::NotCoinbase);
}
if !zcash_note_encryption::decrypts_successfully(transaction, network, height) {
return Err(TransactionError::CoinbaseOutputsNotDecryptable);
}
Ok(())
}
pub fn coinbase_expiry_height(
block_height: &Height,
coinbase: &Transaction,
network: &Network,
) -> Result<(), TransactionError> {
let expiry_height = coinbase.expiry_height();
if let Some(nu5_activation_height) = NetworkUpgrade::Nu5.activation_height(network) {
if *block_height >= nu5_activation_height {
if expiry_height != Some(*block_height) {
return Err(TransactionError::CoinbaseExpiryBlockHeight {
expiry_height,
block_height: *block_height,
transaction_hash: coinbase.hash(),
});
} else {
return Ok(());
}
}
}
validate_expiry_height_max(expiry_height, true, block_height, coinbase)
}
pub fn non_coinbase_expiry_height(
block_height: &Height,
transaction: &Transaction,
) -> Result<(), TransactionError> {
if transaction.is_overwintered() {
let expiry_height = transaction.expiry_height();
validate_expiry_height_max(expiry_height, false, block_height, transaction)?;
validate_expiry_height_mined(expiry_height, block_height, transaction)?;
}
Ok(())
}
fn validate_expiry_height_max(
expiry_height: Option<Height>,
is_coinbase: bool,
block_height: &Height,
transaction: &Transaction,
) -> Result<(), TransactionError> {
if let Some(expiry_height) = expiry_height {
if expiry_height > Height::MAX_EXPIRY_HEIGHT {
Err(TransactionError::MaximumExpiryHeight {
expiry_height,
is_coinbase,
block_height: *block_height,
transaction_hash: transaction.hash(),
})?;
}
}
Ok(())
}
fn validate_expiry_height_mined(
expiry_height: Option<Height>,
block_height: &Height,
transaction: &Transaction,
) -> Result<(), TransactionError> {
if let Some(expiry_height) = expiry_height {
if *block_height > expiry_height {
Err(TransactionError::ExpiredTransaction {
expiry_height,
block_height: *block_height,
transaction_hash: transaction.hash(),
})?;
}
}
Ok(())
}
pub fn tx_transparent_coinbase_spends_maturity(
network: &Network,
tx: Arc<Transaction>,
height: Height,
block_new_outputs: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
spent_utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
) -> Result<(), TransactionError> {
for spend in tx.spent_outpoints() {
let utxo = block_new_outputs
.get(&spend)
.map(|ordered_utxo| ordered_utxo.utxo.clone())
.or_else(|| spent_utxos.get(&spend).cloned())
.expect("load_spent_utxos_fut.await should return an error if a utxo is missing");
let spend_restriction = tx.coinbase_spend_restriction(network, height);
zebra_state::check::transparent_coinbase_spend(spend, spend_restriction, &utxo)?;
}
Ok(())
}
pub fn consensus_branch_id(
tx: &Transaction,
height: Height,
network: &Network,
) -> Result<(), TransactionError> {
let current_nu = NetworkUpgrade::current(network, height);
if current_nu < NetworkUpgrade::Nu5 || tx.version() < 5 {
return Ok(());
}
let Some(tx_nu) = tx.network_upgrade() else {
return Err(TransactionError::MissingConsensusBranchId);
};
if tx_nu != current_nu {
return Err(TransactionError::WrongConsensusBranchId);
}
Ok(())
}