diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index ea72120beb9..76d842c4381 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -26,6 +26,7 @@ displaydoc = "0.2.1" equihash = "0.1" futures = "0.3" hex = "0.4" +itertools = "0.10.0" jubjub = "0.6.0" lazy_static = "1.4.0" primitive-types = "0.9.0" @@ -55,8 +56,6 @@ criterion = { version = "0.3", features = ["html_reports"] } spandoc = "0.2" tracing = "0.1.25" -itertools = "0.10.0" - proptest = "0.10" proptest-derive = "0.3" diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 929563fd74a..05e52fa6d6a 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -10,7 +10,7 @@ mod serialize; mod sighash; #[cfg(any(test, feature = "proptest-impl"))] -mod arbitrary; +pub mod arbitrary; #[cfg(test)] mod tests; diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index ab582382f62..09f044eaf81 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -11,6 +11,8 @@ use crate::{ sapling, sprout, transparent, LedgerState, }; +use itertools::Itertools; + use super::{FieldNotPresent, JoinSplitData, LockTime, Memo, Transaction}; use sapling::{AnchorVariant, PerSpendAnchor, SharedAnchor}; @@ -330,3 +332,130 @@ impl Arbitrary for Transaction { type Strategy = BoxedStrategy; } + +/// Transaction utility tests functions + +/// Convert `trans` into a fake v5 transaction, +/// converting sapling shielded data from v4 to v5 if possible. +pub fn transaction_to_fake_v5(trans: &Transaction) -> Transaction { + use Transaction::*; + + match trans { + V1 { + inputs, + outputs, + lock_time, + } => V5 { + inputs: inputs.to_vec(), + outputs: outputs.to_vec(), + lock_time: *lock_time, + expiry_height: block::Height(0), + sapling_shielded_data: None, + }, + V2 { + inputs, + outputs, + lock_time, + .. + } => V5 { + inputs: inputs.to_vec(), + outputs: outputs.to_vec(), + lock_time: *lock_time, + expiry_height: block::Height(0), + sapling_shielded_data: None, + }, + V3 { + inputs, + outputs, + lock_time, + expiry_height, + .. + } => V5 { + inputs: inputs.to_vec(), + outputs: outputs.to_vec(), + lock_time: *lock_time, + expiry_height: *expiry_height, + sapling_shielded_data: None, + }, + V4 { + inputs, + outputs, + lock_time, + expiry_height, + sapling_shielded_data, + .. + } => V5 { + inputs: inputs.to_vec(), + outputs: outputs.to_vec(), + lock_time: *lock_time, + expiry_height: *expiry_height, + sapling_shielded_data: sapling_shielded_data + .clone() + .map(sapling_shielded_v4_to_fake_v5) + .flatten(), + }, + v5 @ V5 { .. } => v5.clone(), + } +} + +/// Convert a v4 sapling shielded data into a fake v5 sapling shielded data, +/// if possible. +fn sapling_shielded_v4_to_fake_v5( + v4_shielded: sapling::ShieldedData, +) -> Option> { + use sapling::ShieldedData; + use sapling::TransferData::*; + + let unique_anchors: Vec<_> = v4_shielded + .spends() + .map(|spend| spend.per_spend_anchor) + .unique() + .collect(); + + let fake_spends: Vec<_> = v4_shielded + .spends() + .cloned() + .map(sapling_spend_v4_to_fake_v5) + .collect(); + + let transfers = match v4_shielded.transfers { + SpendsAndMaybeOutputs { maybe_outputs, .. } => { + let shared_anchor = match unique_anchors.as_slice() { + [unique_anchor] => *unique_anchor, + // Multiple different anchors, can't convert to v5 + _ => return None, + }; + + SpendsAndMaybeOutputs { + shared_anchor, + spends: fake_spends.try_into().unwrap(), + maybe_outputs, + } + } + JustOutputs { outputs } => JustOutputs { outputs }, + }; + + let fake_shielded_v5 = ShieldedData:: { + value_balance: v4_shielded.value_balance, + transfers, + binding_sig: v4_shielded.binding_sig, + }; + + Some(fake_shielded_v5) +} + +/// Convert a v4 sapling spend into a fake v5 sapling spend. +fn sapling_spend_v4_to_fake_v5( + v4_spend: sapling::Spend, +) -> sapling::Spend { + use sapling::Spend; + + Spend:: { + cv: v4_spend.cv, + per_spend_anchor: FieldNotPresent, + nullifier: v4_spend.nullifier, + rk: v4_spend.rk, + zkproof: v4_spend.zkproof, + spend_auth_sig: v4_spend.spend_auth_sig, + } +} diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs index 1dc154a441f..90ade53361d 100644 --- a/zebra-chain/src/transaction/tests/vectors.rs +++ b/zebra-chain/src/transaction/tests/vectors.rs @@ -2,12 +2,9 @@ use super::super::*; use crate::{ block::{Block, MAX_BLOCK_BYTES}, - sapling::{PerSpendAnchor, SharedAnchor}, serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}, }; -use itertools::Itertools; - use std::convert::TryInto; #[test] @@ -186,7 +183,7 @@ fn fake_v5_round_trip() { .transactions .iter() .map(AsRef::as_ref) - .map(transaction_to_fake_v5) + .map(arbitrary::transaction_to_fake_v5) .map(Into::into) .collect(); @@ -264,130 +261,3 @@ fn fake_v5_round_trip() { ); } } - -// Utility functions - -/// Convert `trans` into a fake v5 transaction, -/// converting sapling shielded data from v4 to v5 if possible. -fn transaction_to_fake_v5(trans: &Transaction) -> Transaction { - use Transaction::*; - - match trans { - V1 { - inputs, - outputs, - lock_time, - } => V5 { - inputs: inputs.to_vec(), - outputs: outputs.to_vec(), - lock_time: *lock_time, - expiry_height: block::Height(0), - sapling_shielded_data: None, - }, - V2 { - inputs, - outputs, - lock_time, - .. - } => V5 { - inputs: inputs.to_vec(), - outputs: outputs.to_vec(), - lock_time: *lock_time, - expiry_height: block::Height(0), - sapling_shielded_data: None, - }, - V3 { - inputs, - outputs, - lock_time, - expiry_height, - .. - } => V5 { - inputs: inputs.to_vec(), - outputs: outputs.to_vec(), - lock_time: *lock_time, - expiry_height: *expiry_height, - sapling_shielded_data: None, - }, - V4 { - inputs, - outputs, - lock_time, - expiry_height, - sapling_shielded_data, - .. - } => V5 { - inputs: inputs.to_vec(), - outputs: outputs.to_vec(), - lock_time: *lock_time, - expiry_height: *expiry_height, - sapling_shielded_data: sapling_shielded_data - .clone() - .map(sapling_shielded_v4_to_fake_v5) - .flatten(), - }, - v5 @ V5 { .. } => v5.clone(), - } -} - -/// Convert a v4 sapling shielded data into a fake v5 sapling shielded data, -/// if possible. -fn sapling_shielded_v4_to_fake_v5( - v4_shielded: sapling::ShieldedData, -) -> Option> { - use sapling::ShieldedData; - use sapling::TransferData::*; - - let unique_anchors: Vec<_> = v4_shielded - .spends() - .map(|spend| spend.per_spend_anchor) - .unique() - .collect(); - - let fake_spends: Vec<_> = v4_shielded - .spends() - .cloned() - .map(sapling_spend_v4_to_fake_v5) - .collect(); - - let transfers = match v4_shielded.transfers { - SpendsAndMaybeOutputs { maybe_outputs, .. } => { - let shared_anchor = match unique_anchors.as_slice() { - [unique_anchor] => *unique_anchor, - // Multiple different anchors, can't convert to v5 - _ => return None, - }; - - SpendsAndMaybeOutputs { - shared_anchor, - spends: fake_spends.try_into().unwrap(), - maybe_outputs, - } - } - JustOutputs { outputs } => JustOutputs { outputs }, - }; - - let fake_shielded_v5 = ShieldedData:: { - value_balance: v4_shielded.value_balance, - transfers, - binding_sig: v4_shielded.binding_sig, - }; - - Some(fake_shielded_v5) -} - -/// Convert a v4 sapling spend into a fake v5 sapling spend. -fn sapling_spend_v4_to_fake_v5( - v4_spend: sapling::Spend, -) -> sapling::Spend { - use sapling::Spend; - - Spend:: { - cv: v4_spend.cv, - per_spend_anchor: FieldNotPresent, - nullifier: v4_spend.nullifier, - rk: v4_spend.rk, - zkproof: v4_spend.zkproof, - spend_auth_sig: v4_spend.spend_auth_sig, - } -} diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index e0022b49e34..96878d68f59 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -42,4 +42,5 @@ tokio = { version = "0.3.6", features = ["full"] } tracing-error = "0.1.2" tracing-subscriber = "0.2.17" +zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] } zebra-test = { path = "../zebra-test/" } diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 6cea0280b29..28bd0ad0057 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -26,6 +26,8 @@ use zebra_state as zs; use crate::{error::TransactionError, primitives, script, BoxError}; mod check; +#[cfg(test)] +mod tests; /// Asynchronous transaction verification. #[derive(Debug, Clone)] diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index 8f72f443fe5..7981b11b5db 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -55,8 +55,30 @@ pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => { unreachable!("tx version is checked first") } - Transaction::V5 { .. } => { - unimplemented!("v5 transaction format as specified in ZIP-225") + Transaction::V5 { + inputs, + outputs, + sapling_shielded_data, + .. + } => { + let tx_in_count = inputs.len(); + let tx_out_count = outputs.len(); + let n_shielded_spend = sapling_shielded_data + .as_ref() + .map(|d| d.spends().count()) + .unwrap_or(0); + let n_shielded_output = sapling_shielded_data + .as_ref() + .map(|d| d.outputs().count()) + .unwrap_or(0); + + if tx_in_count + n_shielded_spend == 0 { + Err(TransactionError::NoInputs) + } else if tx_out_count + n_shielded_output == 0 { + Err(TransactionError::NoOutputs) + } else { + Ok(()) + } } } } @@ -100,15 +122,18 @@ pub fn coinbase_tx_no_joinsplit_or_spend(tx: &Transaction) -> Result<(), Transac Err(TransactionError::CoinbaseHasSpend) } - Transaction::V4 { .. } => Ok(()), + Transaction::V5 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } if sapling_shielded_data.spends().count() > 0 => { + Err(TransactionError::CoinbaseHasSpend) + } + + Transaction::V4 { .. } | Transaction::V5 { .. } => Ok(()), Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => { unreachable!("tx version is checked first") } - - Transaction::V5 { .. } => { - unimplemented!("v5 coinbase validation as specified in ZIP-225 and the draft spec") - } } } else { Ok(()) diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs new file mode 100644 index 00000000000..1c75b1c671b --- /dev/null +++ b/zebra-consensus/src/transaction/tests.rs @@ -0,0 +1,65 @@ +use zebra_chain::{ + block::Block, + serialization::ZcashDeserializeInto, + transaction::{arbitrary::transaction_to_fake_v5, Transaction}, +}; + +use crate::error::TransactionError::*; +use color_eyre::eyre::Report; + +#[test] +fn v5_fake_transactions() -> Result<(), Report> { + zebra_test::init(); + + // get all the blocks we have available + for original_bytes in zebra_test::vectors::BLOCKS.iter() { + let original_block = original_bytes + .zcash_deserialize_into::() + .expect("block is structurally valid"); + + // convert all transactions from the block to V5 + let transactions: Vec = original_block + .transactions + .iter() + .map(AsRef::as_ref) + .map(transaction_to_fake_v5) + .map(Into::into) + .collect(); + + // after the conversion some transactions end up with no inputs nor outputs. + for transaction in transactions { + match super::check::has_inputs_and_outputs(&transaction) { + Err(e) => { + if e != NoInputs && e != NoOutputs { + panic!("error must be NoInputs or NoOutputs") + } + } + Ok(()) => (), + }; + + // make sure there are no joinsplits nor spends in coinbase + super::check::coinbase_tx_no_joinsplit_or_spend(&transaction)?; + + // validate the sapling shielded data + match transaction { + Transaction::V5 { + sapling_shielded_data, + .. + } => { + if let Some(s) = sapling_shielded_data { + super::check::shielded_balances_match(&s)?; + + for spend in s.spends_per_anchor() { + super::check::spend_cv_rk_not_small_order(&spend)? + } + for output in s.outputs() { + super::check::output_cv_epk_not_small_order(&output)?; + } + } + } + _ => panic!("we should have no tx other than 5"), + } + } + } + Ok(()) +}