zebra/zebra-consensus/src/transaction/check.rs

197 lines
7.0 KiB
Rust
Raw Normal View History

//! Transaction checks.
//!
//! Code in this file can freely assume that no pre-V4 transactions are present.
Reject a mempool transaction if it has internal spend conflicts (#2843) * Reorder imports to follow convention Place the imports from `std` at the top. * Add transaction errors for double spends Add a variant for each pool. They represent a double spend inside a transaction. * Add `check::spend_conflicts` implementation Checks if a transaction has spend conflicts, i.e., if a transaction spends a UTXO more than once or if it reveals a nullifier more than once. * Reject transactions with internal spend conflicts The transaction verifier should reject transactions that spend the same transparent UTXO or that reveal the same nullifier. * Add transparent spend consensus rule Add it to the documentation to help with understanding and auditing it. Co-authored-by: teor <teor@riseup.net> * Use different nullifiers by default Don't use the same nullifier twice when mocking a `sprout::JoinSplitData` because it will lead to an invalid transaction. * Test transactions with repeated spend outpoints Since that represents a spend conflict, they should be rejected. * Test duplicate nullifiers in joinsplit Check if a mock transaction with a joinsplit that reveals the same nullifier twice is rejected. * Test duplicate nullifiers across joinsplits Check if a duplicate nullifier in two different joinsplits in the same transaction is rejected. * Test V4 transaction with duplicate Sapling spend Check if a V4 transaction that has a duplicate Sapling spend is rejected. * Test V5 transaction with duplicate Sapling spend Check if a V5 transaction that has a duplicate Sapling spend is rejected. * Test V5 transaction with duplicate Orchard actions Check if a V5 transaction that has duplicate Orchard actions is rejected by the transaction verifier. Co-authored-by: teor <teor@riseup.net>
2021-10-27 19:49:28 -07:00
use std::{borrow::Cow, collections::HashSet, convert::TryFrom, hash::Hash};
use zebra_chain::{
amount::{Amount, NonNegative},
block::Height,
orchard::Flags,
parameters::{Network, NetworkUpgrade},
Move the check in `transaction::check::sapling_balances_match` to `V4` deserialization (#2234) * Implement `PartialEq<i64>` for `Amount` Allows to compare an `Amount` instance directly to an integer. * Add `SerializationError::BadTransactionBalance` Error variant representing deserialization of a transaction that doesn't conform to the Sapling consensus rule where the balance MUST be zero if there aren't any shielded spends and outputs. * Validate consensus rule when deserializing Return an error if the deserialized V4 transaction has a non-zero value balance but doesn't have any Sapling shielded spends nor outputs. * Add consensus rule link to field documentation Describe how the consensus rule is validated structurally by `ShieldedData`. * Clarify that `value_balance` is zero Make the description more concise and objective. Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> * Update field documentation Include information about how the consensus rule is guaranteed during serialization. Co-authored-by: teor <teor@riseup.net> * Remove `check::sapling_balances_match` function The check is redundant because the respective consensus rule is validated structurally by `ShieldedData`. * Test deserialization of invalid V4 transaction A transaction with no Sapling shielded spends and no outputs but with a non-zero balance value should fail to deserialize. * Change least-significant byte of the value balance State how the byte index is calculated, and change the least significant-byte to be non-zero. Co-authored-by: teor <teor@riseup.net>
2021-06-03 15:53:00 -07:00
sapling::{Output, PerSpendAnchor, Spend},
transaction::Transaction,
};
use crate::error::TransactionError;
/// Checks that the transaction has inputs and outputs.
///
/// For `Transaction::V4`:
/// * 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.
///
/// For `Transaction::V5`:
/// * 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)
///
/// This check counts both `Coinbase` and `PrevOut` transparent inputs.
///
2021-03-02 11:35:13 -08:00
/// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> {
if !tx.has_transparent_or_shielded_inputs() {
Err(TransactionError::NoInputs)
} else if !tx.has_transparent_or_shielded_outputs() {
Err(TransactionError::NoOutputs)
} else {
Ok(())
}
}
/// 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.
///
2021-03-02 11:35:13 -08:00
/// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), TransactionError> {
if tx.has_valid_coinbase_transaction_inputs() {
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);
}
if let Some(orchard_shielded_data) = tx.orchard_shielded_data() {
if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) {
return Err(TransactionError::CoinbaseHasEnableSpendsOrchard);
}
}
}
Ok(())
}
/// 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
pub fn spend_cv_rk_not_small_order(spend: &Spend<PerSpendAnchor>) -> Result<(), TransactionError> {
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(())
}
}
/// 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");
Check remaining transaction value & make value balance signs match the spec (#2566) * Make Amount arithmetic more generic To modify generated amounts, we need some extra operations on `Amount`. We also need to extend existing operations to both `NonNegative` and `NegativeAllowed` amounts. * Add a constrain method for ValueBalance * Derive Eq for ValueBalance * impl Neg for ValueBalance * Make some Amount arithmetic expectations explicit * Explain why we use i128 for multiplication And expand the overflow error details. * Expand Amount::sum error details * Make amount::Error field order consistent * Rename an amount::Error variant to Constraint, so it's clearer * Add specific pool variants to ValueBalanceError * Update coinbase remaining value consensus rule comment This consensus rule was updated recently to include coinbase transactions, but Zebra doesn't check block subsidy or miner fees yet. * Add test methods for modifying transparent values and shielded value balances * Temporarily set values and value balances to zero in proptests In both generated chains and proptests that construct their own transactions. Using zero values reduces value calculation and value check test coverage. A future change will use non-zero values, and fix them so the check passes. * Add extra fields to remaining transaction value errors * Swap the transparent value balance sign to match shielded value balances This makes the signs of all the chain value pools consistent. * Use a NonNegative constraint for transparent values This fix: * makes the type signature match the consensus rules * avoids having to write code to handle negative values * Allocate total generated transaction input value to outputs If there isn't enough input value for an output, set it to zero. Temporarily reduce all generated values to avoid overflow. (We'll remove this workaround when we calculate chain value balances.) * Consistently use ValueBalanceError for ValueBalances * Make the value balance signs match the spec And rename and document methods so their signs are clearer. * Convert amount::Errors to specific pool ValueBalanceErrors * Move some error changes to the next PR * Add extra info to remaining transaction value errors (#2585) * Distinguish between overflow and negative remaining transaction value errors And make some error types cloneable. * Add methods for updating chain value pools (#2586) * Move amount::test to amount::tests:vectors * Make ValueBalance traits more consistent with Amount - implement Add and Sub variants with Result and Assign - derive Hash * Clarify some comments and expects * Create ValueBalance update methods for blocks and transactions Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
2021-08-09 10:22:26 -07:00
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(())
}
Reject a mempool transaction if it has internal spend conflicts (#2843) * Reorder imports to follow convention Place the imports from `std` at the top. * Add transaction errors for double spends Add a variant for each pool. They represent a double spend inside a transaction. * Add `check::spend_conflicts` implementation Checks if a transaction has spend conflicts, i.e., if a transaction spends a UTXO more than once or if it reveals a nullifier more than once. * Reject transactions with internal spend conflicts The transaction verifier should reject transactions that spend the same transparent UTXO or that reveal the same nullifier. * Add transparent spend consensus rule Add it to the documentation to help with understanding and auditing it. Co-authored-by: teor <teor@riseup.net> * Use different nullifiers by default Don't use the same nullifier twice when mocking a `sprout::JoinSplitData` because it will lead to an invalid transaction. * Test transactions with repeated spend outpoints Since that represents a spend conflict, they should be rejected. * Test duplicate nullifiers in joinsplit Check if a mock transaction with a joinsplit that reveals the same nullifier twice is rejected. * Test duplicate nullifiers across joinsplits Check if a duplicate nullifier in two different joinsplits in the same transaction is rejected. * Test V4 transaction with duplicate Sapling spend Check if a V4 transaction that has a duplicate Sapling spend is rejected. * Test V5 transaction with duplicate Sapling spend Check if a V5 transaction that has a duplicate Sapling spend is rejected. * Test V5 transaction with duplicate Orchard actions Check if a V5 transaction that has duplicate Orchard actions is rejected by the transaction verifier. Co-authored-by: teor <teor@riseup.net>
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(())
}