From 4a954c7f8fcc80c3767adcecd2e5dd76f9eb9ab0 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 27 May 2020 12:09:59 -0600 Subject: [PATCH] Add tests for transaction builder support. --- zcash_extensions/Cargo.toml | 6 + zcash_extensions/src/transparent/demo.rs | 212 +++++++++++++++++--- zcash_primitives/src/prover.rs | 2 +- zcash_primitives/src/transaction/builder.rs | 53 +++-- zcash_primitives/src/transaction/mod.rs | 19 +- zcash_primitives/src/transaction/sighash.rs | 3 +- 6 files changed, 232 insertions(+), 63 deletions(-) diff --git a/zcash_extensions/Cargo.toml b/zcash_extensions/Cargo.toml index c975d48e9..454b1315b 100644 --- a/zcash_extensions/Cargo.toml +++ b/zcash_extensions/Cargo.toml @@ -11,3 +11,9 @@ edition = "2018" [dependencies] blake2b_simd = "0.5" zcash_primitives = { version = "0.3.0", path = "../zcash_primitives" } + +[dev-dependencies] +ff = { version = "0.7", path = "../ff" } +jubjub = { version = "0.4", path = "../jubjub" } +zcash_proofs = { path = "../zcash_proofs" } +rand_core = "0.5.1" diff --git a/zcash_extensions/src/transparent/demo.rs b/zcash_extensions/src/transparent/demo.rs index 62a26db75..68da1599d 100644 --- a/zcash_extensions/src/transparent/demo.rs +++ b/zcash_extensions/src/transparent/demo.rs @@ -304,21 +304,28 @@ fn builder_hashes(preimage_1: &[u8; 32], preimage_2: &[u8; 32]) -> ([u8; 32], [u (hash_1, hash_2) } -pub struct DemoBuilder<'a, B> { - txn_builder: &'a mut B, - extension_id: usize, +pub struct DemoBuilder { + pub txn_builder: B, + pub extension_id: usize, } +#[derive(Debug)] pub enum DemoBuildError { BaseBuilderError(E), ExpectedOpen, ExpectedClose, PrevoutParseFailure(Error), - TransferMismatch { expected: [u8; 32], actual: [u8; 32] }, - CloseMismatch { expected: [u8; 32], actual: [u8; 32] }, + TransferMismatch { + expected: [u8; 32], + actual: [u8; 32], + }, + CloseMismatch { + expected: [u8; 32], + actual: [u8; 32], + }, } -impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<'a, B> { +impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> { pub fn demo_open( &mut self, value: Amount, @@ -343,15 +350,20 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<'a, B> { let (hash_1, hash_2) = builder_hashes(&preimage_1, &preimage_2); // eagerly validate the relationship between prevout.1 and preimage_1 - match Precondition::from_payload(prevout.1.precondition.mode, &prevout.1.precondition.payload) { - Ok(Precondition::Open(hash)) => + match Precondition::from_payload( + prevout.1.precondition.mode, + &prevout.1.precondition.payload, + ) { + Ok(Precondition::Open(hash)) => { if hash.0 != hash_1 { - Err(DemoBuildError::TransferMismatch { expected: hash.0, actual: hash_1})? - } - Ok(Precondition::Close(_)) => - Err(DemoBuildError::ExpectedOpen)?, - Err(parse_failure) => - Err(DemoBuildError::PrevoutParseFailure(parse_failure))? + Err(DemoBuildError::TransferMismatch { + expected: hash.0, + actual: hash_1, + })? + } + } + Ok(Precondition::Close(_)) => Err(DemoBuildError::ExpectedOpen)?, + Err(parse_failure) => Err(DemoBuildError::PrevoutParseFailure(parse_failure))?, } self.txn_builder @@ -360,12 +372,13 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<'a, B> { }) .map_err(DemoBuildError::BaseBuilderError)?; - self.txn_builder.add_tze_output( - self.extension_id, - transfer_amount, // can this be > prevout.1.value? - &Precondition::close(hash_2), - ) - .map_err(DemoBuildError::BaseBuilderError) + self.txn_builder + .add_tze_output( + self.extension_id, + transfer_amount, // can this be > prevout.1.value? + &Precondition::close(hash_2), + ) + .map_err(DemoBuildError::BaseBuilderError) } pub fn demo_close( @@ -380,15 +393,20 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<'a, B> { }; // eagerly validate the relationship between prevout.1 and preimage_2 - match Precondition::from_payload(prevout.1.precondition.mode, &prevout.1.precondition.payload) { - Ok(Precondition::Open(_)) => - Err(DemoBuildError::ExpectedClose)?, - Ok(Precondition::Close(hash)) => + match Precondition::from_payload( + prevout.1.precondition.mode, + &prevout.1.precondition.payload, + ) { + Ok(Precondition::Open(_)) => Err(DemoBuildError::ExpectedClose)?, + Ok(Precondition::Close(hash)) => { if hash.0 != hash_2 { - Err(DemoBuildError::CloseMismatch { expected: hash.0, actual: hash_2})? - } - Err(parse_failure) => - Err(DemoBuildError::PrevoutParseFailure(parse_failure))? + Err(DemoBuildError::CloseMismatch { + expected: hash.0, + actual: hash_2, + })? + } + } + Err(parse_failure) => Err(DemoBuildError::PrevoutParseFailure(parse_failure))?, } self.txn_builder @@ -401,17 +419,33 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<'a, B> { #[cfg(test)] mod tests { + use ff::{Field, PrimeField}; use blake2b_simd::Params; + use rand_core::OsRng; + + use zcash_proofs::prover::LocalTxProver; - use super::{close, open, Context, Precondition, Program, Witness}; use zcash_primitives::{ + consensus::{ + BranchId, + TestNetwork, + }, extensions::transparent::{self as tze, Extension, FromPayload, ToPayload}, + legacy::TransparentAddress, + merkle_tree::{CommitmentTree, IncrementalWitness}, + primitives::Rseed, + sapling::Node, transaction::{ + builder::Builder, components::{Amount, OutPoint, TzeIn, TzeOut}, Transaction, TransactionData, }, + zip32::ExtendedSpendingKey, }; + use super::{close, open, Context, DemoBuilder, Precondition, Program, Witness}; + + #[test] fn precondition_open_round_trip() { let data = vec![7; 32]; @@ -519,7 +553,7 @@ mod tests { precondition: tze::Precondition::from(0, &Precondition::open(hash_1)), }; - let mut mtx_a = TransactionData::nu4(); + let mut mtx_a = TransactionData::future(); mtx_a.tze_outputs.push(out_a); let tx_a = mtx_a.freeze().unwrap(); @@ -535,7 +569,7 @@ mod tests { value: Amount::from_u64(1).unwrap(), precondition: tze::Precondition::from(0, &Precondition::close(hash_2)), }; - let mut mtx_b = TransactionData::nu4(); + let mut mtx_b = TransactionData::future(); mtx_b.tze_inputs.push(in_b); mtx_b.tze_outputs.push(out_b); let tx_b = mtx_b.freeze().unwrap(); @@ -549,7 +583,7 @@ mod tests { witness: tze::Witness::from(0, &Witness::close(preimage_2)), }; - let mut mtx_c = TransactionData::nu4(); + let mut mtx_c = TransactionData::future(); mtx_c.tze_inputs.push(in_c); let tx_c = mtx_c.freeze().unwrap(); @@ -579,4 +613,118 @@ mod tests { ); } } + + #[test] + fn demo_builder_program() { + let preimage_1 = [1; 32]; + let preimage_2 = [2; 32]; + + let prover = LocalTxProver::with_default_location().unwrap(); + + // + // Opening transaction + // + + let mut rng = OsRng; + let mut builder_a: Builder<'_, TestNetwork, OsRng> = Builder::new_future(0); + + // create some inputs to spend + let extsk = ExtendedSpendingKey::master(&[]); + let to = extsk.default_address().unwrap().1; + let note1 = to + .create_note(110000, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))) + .unwrap(); + let cm1 = Node::new(note1.cmu().to_repr()); + let mut tree = CommitmentTree::new(); + // fake that the note appears in some previous + // shielded output + tree.append(cm1).unwrap(); + let witness1 = IncrementalWitness::from_tree(&tree); + + builder_a + .add_sapling_spend( + extsk.clone(), + *to.diversifier(), + note1.clone(), + witness1.path().unwrap(), + ) + .unwrap(); + + let mut db_a = DemoBuilder { + txn_builder: &mut builder_a, + extension_id: 0, + }; + + let value = Amount::from_u64(100000).unwrap(); + db_a.demo_open(value, preimage_1, preimage_2) + .map_err(|e| format!("open failure: {:?}", e)) + .unwrap(); + let (tx_a, _) = builder_a + .build(BranchId::Canopy, &prover) + .map_err(|e| format!("build failure: {:?}", e)) + .unwrap(); + + // + // Transfer + // + + let mut builder_b: Builder<'_, TestNetwork, OsRng> = Builder::new_future(0); + let mut db_b = DemoBuilder { + txn_builder: &mut builder_b, + extension_id: 0, + }; + let prevout_a = (OutPoint::new(tx_a.txid().0, 0), tx_a.data.tze_outputs[0].clone()); + let value_xfr = Amount::from_u64(90000).unwrap(); + db_b.demo_transfer_to_close(prevout_a, value_xfr, preimage_1, preimage_2) + .map_err(|e| format!("transfer failure: {:?}", e)) + .unwrap(); + let (tx_b, _) = builder_b + .build(BranchId::Canopy, &prover) + .map_err(|e| format!("build failure: {:?}", e)) + .unwrap(); + + // + // Closing transaction + // + + let mut builder_c: Builder<'_, TestNetwork, OsRng> = Builder::new_future(0); + let mut db_c = DemoBuilder { + txn_builder: &mut builder_c, + extension_id: 0, + }; + let prevout_b = (OutPoint::new(tx_a.txid().0, 0), tx_b.data.tze_outputs[0].clone()); + db_c.demo_close(prevout_b, preimage_2) + .map_err(|e| format!("close failure: {:?}", e)) + .unwrap(); + + builder_c.add_transparent_output(&TransparentAddress::PublicKey([0; 20]), Amount::from_u64(80000).unwrap()) + .unwrap(); + + let (tx_c, _) = builder_c + .build(BranchId::Canopy, &prover) + .map_err(|e| format!("build failure: {:?}", e)) + .unwrap(); + + // Verify tx_b + let ctx0 = Ctx { tx: &tx_b }; + assert_eq!( + Program.verify( + &tx_a.data.tze_outputs[0].precondition, + &tx_b.data.tze_inputs[0].witness, + &ctx0 + ), + Ok(()) + ); + + // Verify tx_c + let ctx1 = Ctx { tx: &tx_b }; + assert_eq!( + Program.verify( + &tx_b.data.tze_outputs[0].precondition, + &tx_c.data.tze_inputs[0].witness, + &ctx1 + ), + Ok(()) + ); + } } diff --git a/zcash_primitives/src/prover.rs b/zcash_primitives/src/prover.rs index 83cf4d4d1..9efc0cdb0 100644 --- a/zcash_primitives/src/prover.rs +++ b/zcash_primitives/src/prover.rs @@ -60,7 +60,7 @@ pub trait TxProver { } #[cfg(test)] -pub(crate) mod mock { +pub mod mock { use ff::Field; use rand_core::OsRng; diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index 1247d6ad5..0c7f1dbf9 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -371,20 +371,14 @@ impl<'a, P: consensus::Parameters> Builder<'a, P, OsRng> { pub fn new(height: u32) -> Self { Builder::new_with_rng(height, OsRng) } + + pub fn new_future(height: u32) -> Self { + Builder::new_with_rng_future(height, OsRng) + } } impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> { - /// Creates a new `Builder` targeted for inclusion in the block with the given height - /// and randomness source, using default values for general transaction fields. - /// - /// # Default values - /// - /// The expiry height will be set to the given height plus the default transaction - /// expiry delta (20 blocks). - /// - /// The fee will be set to the default fee (0.0001 ZEC). - pub fn new_with_rng(height: u32, rng: R) -> Builder<'a, P, R> { - let mut mtx = TransactionData::new(); + fn new_with_mtx(height: u32, rng: R, mut mtx: TransactionData) -> Builder<'a, P, R> { mtx.expiry_height = height + DEFAULT_TX_EXPIRY_DELTA; Builder { @@ -402,6 +396,25 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> { } } + /// Creates a new `Builder` targeted for inclusion in the block with the given height + /// and randomness source, using default values for general transaction fields. + /// + /// # Default values + /// + /// The expiry height will be set to the given height plus the default transaction + /// expiry delta (20 blocks). + /// + /// The fee will be set to the default fee (0.0001 ZEC). + pub fn new_with_rng(height: u32, rng: R) -> Builder<'a, P, R> { + let mtx = TransactionData::new(); + Self::new_with_mtx(height, rng, mtx) + } + + pub fn new_with_rng_future(height: u32, rng: R) -> Builder<'a, P, R> { + let mtx = TransactionData::future(); + Self::new_with_mtx(height, rng, mtx) + } + /// Adds a Sapling note to be spent in this transaction. /// /// Returns an error if the given Merkle path does not have the same anchor as the @@ -508,7 +521,6 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> { mut self, consensus_branch_id: consensus::BranchId, prover: &impl TxProver, - // epoch: &Epoch ) -> Result<(Transaction, TransactionMetadata), Error> { let mut tx_metadata = TransactionMetadata::new(); @@ -517,7 +529,9 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> { // // Valid change - let change = self.mtx.value_balance - self.fee + self.transparent_inputs.value_sum() + let change = self.mtx.value_balance + - self.fee + + self.transparent_inputs.value_sum() - self.mtx.vout.iter().map(|vo| vo.value).sum::() + self .tze_inputs @@ -735,15 +749,15 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> { } // Add a binding signature if needed - if binding_sig_needed { - self.mtx.binding_sig = Some( + self.mtx.binding_sig = if binding_sig_needed { + Some( prover .binding_sig(&mut ctx, self.mtx.value_balance, &sighash) - .map_err(|()| Error::BindingSig)?, - ); + .map_err(|_| Error::BindingSig)?, + ) } else { - self.mtx.binding_sig = None; - } + None + }; // // Create TZE input witnesses for tze_in in self.tze_inputs.builders { @@ -784,7 +798,6 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a where WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result), { - // where WBuilder: WitnessBuilder { self.tze_inputs.push(extension_id, prevout, witness_builder); Ok(()) } diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index 42cf8fc31..2af83bfc9 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -44,8 +44,8 @@ impl fmt::Display for TxId { /// A Zcash transaction. #[derive(Debug)] pub struct Transaction { - txid: TxId, - data: TransactionData, + pub txid: TxId, + pub data: TransactionData, } impl Deref for Transaction { @@ -142,7 +142,7 @@ impl TransactionData { } } - pub fn nu4() -> Self { + pub fn future() -> Self { TransactionData { overwintered: true, version: FUTURE_TX_VERSION, @@ -310,10 +310,11 @@ impl Transaction { let is_sapling_v4 = self.overwintered && self.version_group_id == SAPLING_VERSION_GROUP_ID && self.version == SAPLING_TX_VERSION; - let is_nu4_v5 = self.overwintered + let has_tze = self.overwintered && self.version_group_id == FUTURE_VERSION_GROUP_ID && self.version == FUTURE_TX_VERSION; - if self.overwintered && !(is_overwinter_v3 || is_sapling_v4 || is_nu4_v5) { + + if self.overwintered && !(is_overwinter_v3 || is_sapling_v4 || has_tze) { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Unknown transaction format", @@ -322,16 +323,16 @@ impl Transaction { Vector::write(&mut writer, &self.vin, |w, e| e.write(w))?; Vector::write(&mut writer, &self.vout, |w, e| e.write(w))?; - if is_nu4_v5 { + if has_tze { Vector::write(&mut writer, &self.tze_inputs, |w, e| e.write(w))?; Vector::write(&mut writer, &self.tze_outputs, |w, e| e.write(w))?; } writer.write_u32::(self.lock_time)?; - if is_overwinter_v3 || is_sapling_v4 || is_nu4_v5 { + if is_overwinter_v3 || is_sapling_v4 || has_tze { writer.write_u32::(self.expiry_height)?; } - if is_sapling_v4 || is_nu4_v5 { + if is_sapling_v4 || has_tze { writer.write_all(&self.value_balance.to_i64_le_bytes())?; Vector::write(&mut writer, &self.shielded_spends, |w, e| e.write(w))?; Vector::write(&mut writer, &self.shielded_outputs, |w, e| e.write(w))?; @@ -376,7 +377,7 @@ impl Transaction { } } - if (is_sapling_v4 || is_nu4_v5) + if (is_sapling_v4 || has_tze) && !(self.shielded_spends.is_empty() && self.shielded_outputs.is_empty()) { match self.binding_sig { diff --git a/zcash_primitives/src/transaction/sighash.rs b/zcash_primitives/src/transaction/sighash.rs index 2cc847b59..f8f0e010e 100644 --- a/zcash_primitives/src/transaction/sighash.rs +++ b/zcash_primitives/src/transaction/sighash.rs @@ -6,7 +6,7 @@ use group::GroupEncoding; use super::{ components::{Amount, TxOut}, Transaction, TransactionData, OVERWINTER_VERSION_GROUP_ID, SAPLING_TX_VERSION, - SAPLING_VERSION_GROUP_ID, + SAPLING_VERSION_GROUP_ID, FUTURE_VERSION_GROUP_ID, }; use crate::{consensus, legacy::Script}; @@ -54,6 +54,7 @@ impl SigHashVersion { match tx.version_group_id { OVERWINTER_VERSION_GROUP_ID => SigHashVersion::Overwinter, SAPLING_VERSION_GROUP_ID => SigHashVersion::Sapling, + FUTURE_VERSION_GROUP_ID => SigHashVersion::Sapling, //FIXME _ => unimplemented!(), } } else {