diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index ea836e338..72c0369b9 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -17,12 +17,15 @@ use zebra_chain::{ parameters::NetworkUpgrade, primitives::{ed25519, redjubjub}, transaction::{self, HashType, Transaction}, + transparent::{self, Script}, }; use zebra_state as zs; use crate::{script, BoxError}; +mod check; + /// Asynchronous transaction verification. #[derive(Debug, Clone)] pub struct Verifier { @@ -48,6 +51,8 @@ where pub enum VerifyTransactionError { /// Only V4 and later transactions can be verified. WrongVersion, + /// A transaction MUST move money around. + NoTransfer, /// Could not verify a transparent script Script(#[from] zebra_script::Error), /// Could not verify a Groth16 proof of a JoinSplit/Spend/Output description @@ -154,13 +159,16 @@ where } } + check::some_money_is_spent(&tx)?; + check::any_coinbase_inputs_no_transparent_outputs(&tx)?; + let sighash = tx.sighash( NetworkUpgrade::Sapling, // TODO: pass this in HashType::ALL, // TODO: check these None, // TODO: check these ); - if let Some(_joinsplit_data) = joinsplit_data { + if let Some(joinsplit_data) = joinsplit_data { // XXX create a method on JoinSplitData // that prepares groth16::Items with the correct proofs // and proof inputs, handling interstitial treestates @@ -168,10 +176,7 @@ where // Then, pass those items to self.joinsplit to verify them. - // XXX refactor this into a nicely named check function - // ed25519::VerificationKey::try_from(joinsplit_data.pub_key) - // .and_then(|vk| vk.verify(&joinsplit_data.sig, sighash)) - // .map_err(VerifyTransactionError::Ed25519) + check::validate_joinsplit_sig(joinsplit_data, sighash.as_bytes())?; } if let Some(shielded_data) = shielded_data { diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs new file mode 100644 index 000000000..4756dc956 --- /dev/null +++ b/zebra-consensus/src/transaction/check.rs @@ -0,0 +1,78 @@ +use std::convert::TryFrom; + +use zebra_chain::{ + amount::Amount, + primitives::{ + ed25519, + redjubjub::{self, Binding}, + Groth16Proof, + }, + sapling::ValueCommitment, + transaction::{JoinSplitData, ShieldedData, Transaction}, +}; + +use crate::transaction::VerifyTransactionError; + +/// Validate the JoinSplit binding signature. +/// +/// https://zips.z.cash/protocol/canopy.pdf#sproutnonmalleability +/// https://zips.z.cash/protocol/canopy.pdf#txnencodingandconsensus +pub fn validate_joinsplit_sig( + joinsplit_data: &JoinSplitData, + sighash: &[u8], +) -> Result<(), VerifyTransactionError> { + ed25519::VerificationKey::try_from(joinsplit_data.pub_key) + .and_then(|vk| vk.verify(&joinsplit_data.sig, sighash)) + .map_err(VerifyTransactionError::Ed25519) +} + +/// Check that at least one of tx_in_count, nShieldedSpend, and nJoinSplit MUST +/// be non-zero. +/// +/// https://zips.z.cash/protocol/canopy.pdf#txnencodingandconsensus +pub fn some_money_is_spent(tx: &Transaction) -> Result<(), VerifyTransactionError> { + match tx { + Transaction::V4 { + inputs, + joinsplit_data: Some(joinsplit_data), + shielded_data: Some(shielded_data), + .. + } => { + if inputs.len() > 0 + || joinsplit_data.joinsplits().count() > 0 + || shielded_data.spends().count() > 0 + { + return Ok(()); + } else { + return Err(VerifyTransactionError::NoTransfer); + } + } + _ => return Err(VerifyTransactionError::WrongVersion), + } +} + +/// Check that a transaction with one or more transparent inputs from coinbase +/// transactions has no transparent outputs. +/// +/// Note that inputs from coinbase transactions include Founders’ Reward +/// outputs. +/// +/// https://zips.z.cash/protocol/canopy.pdf#consensusfrombitcoin +pub fn any_coinbase_inputs_no_transparent_outputs( + tx: &Transaction, +) -> Result<(), VerifyTransactionError> { + match tx { + Transaction::V4 { + inputs, outputs, .. + } => { + if !tx.contains_coinbase_input() { + return Ok(()); + } else if outputs.len() == 0 { + return Ok(()); + } else { + return Err(VerifyTransactionError::NoTransfer); + } + } + _ => return Err(VerifyTransactionError::WrongVersion), + } +}