diff --git a/zcash_primitives/Cargo.toml b/zcash_primitives/Cargo.toml index 9cf56b630..f1baf4a36 100644 --- a/zcash_primitives/Cargo.toml +++ b/zcash_primitives/Cargo.toml @@ -41,6 +41,7 @@ subtle = "2.2.3" criterion = "0.3" hex-literal = "0.2" rand_xorshift = "0.2" +proptest = "0.10.1" [features] transparent-inputs = ["ripemd160", "secp256k1"] diff --git a/zcash_primitives/src/extensions/transparent.rs b/zcash_primitives/src/extensions/transparent.rs index ca165525d..63ba5c84e 100644 --- a/zcash_primitives/src/extensions/transparent.rs +++ b/zcash_primitives/src/extensions/transparent.rs @@ -32,7 +32,7 @@ pub trait ToPayload { /// and extension-specific types. The payload field of this struct /// is treated as opaque to all but extension corresponding to the /// encapsulated extension_id value. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Precondition { pub extension_id: u32, pub mode: u32, @@ -66,7 +66,7 @@ impl Precondition { /// and extension-specific types. The payload field of this struct /// is treated as opaque to all but extension corresponding to the /// encapsulated extension_id value. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Witness { pub extension_id: u32, pub mode: u32, diff --git a/zcash_primitives/src/legacy.rs b/zcash_primitives/src/legacy.rs index d005b2515..c79e81e0f 100644 --- a/zcash_primitives/src/legacy.rs +++ b/zcash_primitives/src/legacy.rs @@ -26,7 +26,7 @@ enum OpCode { } /// A serialized script, used inside transparent inputs and outputs of a transaction. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Script(pub Vec); impl Script { diff --git a/zcash_primitives/src/redjubjub.rs b/zcash_primitives/src/redjubjub.rs index 312f6db28..449888e86 100644 --- a/zcash_primitives/src/redjubjub.rs +++ b/zcash_primitives/src/redjubjub.rs @@ -36,7 +36,7 @@ pub struct Signature { pub struct PrivateKey(pub jubjub::Fr); -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct PublicKey(pub ExtendedPoint); impl Signature { diff --git a/zcash_primitives/src/transaction/components.rs b/zcash_primitives/src/transaction/components.rs index 7699681d0..2bc095b4e 100644 --- a/zcash_primitives/src/transaction/components.rs +++ b/zcash_primitives/src/transaction/components.rs @@ -56,7 +56,7 @@ impl OutPoint { } } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub struct TxIn { pub prevout: OutPoint, pub script_sig: Script, @@ -93,7 +93,7 @@ impl TxIn { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct TxOut { pub value: Amount, pub script_pubkey: Script, @@ -121,7 +121,7 @@ impl TxOut { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct TzeIn { pub prevout: OutPoint, pub witness: tze::Witness, @@ -171,7 +171,7 @@ impl TzeIn { &mut writer, usize::try_from(self.witness.mode).map_err(|e| to_io_error(e))?, ) - } + } pub fn write(&self, mut writer: W) -> io::Result<()> { self.write_without_witness(&mut writer)?; @@ -179,7 +179,7 @@ impl TzeIn { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct TzeOut { pub value: Amount, pub precondition: tze::Precondition, @@ -225,6 +225,7 @@ impl TzeOut { } } +#[derive(Clone)] pub struct SpendDescription { pub cv: jubjub::ExtendedPoint, pub anchor: bls12_381::Scalar, @@ -314,6 +315,7 @@ impl SpendDescription { } } +#[derive(Clone)] pub struct OutputDescription { pub cv: jubjub::ExtendedPoint, pub cmu: bls12_381::Scalar, @@ -405,6 +407,7 @@ impl OutputDescription { } } +#[derive(Clone)] enum SproutProof { Groth([u8; GROTH_PROOF_SIZE]), PHGR([u8; PHGR_PROOF_SIZE]), @@ -419,6 +422,7 @@ impl std::fmt::Debug for SproutProof { } } +#[derive(Clone)] pub struct JSDescription { vpub_old: Amount, vpub_new: Amount, diff --git a/zcash_primitives/src/transaction/components/amount.rs b/zcash_primitives/src/transaction/components/amount.rs index f3f89afab..7f5564266 100644 --- a/zcash_primitives/src/transaction/components/amount.rs +++ b/zcash_primitives/src/transaction/components/amount.rs @@ -2,7 +2,7 @@ use std::iter::Sum; use std::ops::{Add, AddAssign, Sub, SubAssign}; const COIN: i64 = 1_0000_0000; -const MAX_MONEY: i64 = 21_000_000 * COIN; +pub const MAX_MONEY: i64 = 21_000_000 * COIN; pub const DEFAULT_FEE: Amount = Amount(10000); diff --git a/zcash_primitives/src/transaction/mod.rs b/zcash_primitives/src/transaction/mod.rs index 90399ace1..a4e15a4ac 100644 --- a/zcash_primitives/src/transaction/mod.rs +++ b/zcash_primitives/src/transaction/mod.rs @@ -42,7 +42,7 @@ impl fmt::Display for TxId { } /// A Zcash transaction. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Transaction { txid: TxId, data: TransactionData, @@ -62,6 +62,7 @@ impl PartialEq for Transaction { } } +#[derive(Clone)] pub struct TransactionData { pub overwintered: bool, pub version: u32, diff --git a/zcash_primitives/src/transaction/tests.rs b/zcash_primitives/src/transaction/tests.rs index 495d22b2c..2ee7cf67b 100644 --- a/zcash_primitives/src/transaction/tests.rs +++ b/zcash_primitives/src/transaction/tests.rs @@ -1,14 +1,124 @@ use ff::Field; use rand_core::OsRng; -use crate::{constants::SPENDING_KEY_GENERATOR, redjubjub::PrivateKey}; +use proptest::collection::vec; +use proptest::prelude::*; + +use crate::{ + consensus::BranchId, constants::SPENDING_KEY_GENERATOR, extensions::transparent as tze, + legacy::Script, redjubjub::PrivateKey, +}; use super::{ - components::Amount, + components::amount::MAX_MONEY, + components::{Amount, OutPoint, TxIn, TxOut, TzeIn, TzeOut}, sighash::{signature_hash, SignableInput}, - Transaction, TransactionData, + Transaction, TransactionData, FUTURE_TX_VERSION, FUTURE_VERSION_GROUP_ID, + OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID, SAPLING_TX_VERSION, + SAPLING_VERSION_GROUP_ID, }; +prop_compose! { + fn arb_outpoint()(hash in prop::array::uniform32(1u8..), n in 1..(100 as u32)) -> OutPoint { + OutPoint::new(hash, n) + } +} + +// TODO: actually generate real possible script values? +prop_compose! { + fn arb_script()(v in vec(any::(), 1..256)) -> Script { + Script(v) + } +} + +prop_compose! { + fn arb_txin()(prevout in arb_outpoint(), script_sig in arb_script(), sequence in any::()) -> TxIn { + TxIn { prevout, script_sig, sequence } + } +} + +prop_compose! { + fn arb_amount()(value in 0..MAX_MONEY) -> Amount { + Amount::from_i64(value).unwrap() + } +} + +prop_compose! { + fn arb_txout()(value in arb_amount(), script_pubkey in arb_script()) -> TxOut { + TxOut { value, script_pubkey } + } +} + +prop_compose! { + fn arb_witness()(extension_id in 0..(100 as u32), mode in (0..100 as u32), payload in vec(any::(), 32..256)) -> tze::Witness { + tze::Witness { extension_id, mode, payload } + } +} + +prop_compose! { + fn arb_tzein()(prevout in arb_outpoint(), witness in arb_witness()) -> TzeIn { + TzeIn { prevout, witness } + } +} + +prop_compose! { + fn arb_precondition()(extension_id in 0..(100 as u32), mode in (0..100 as u32), payload in vec(any::(), 32..256)) -> tze::Precondition { + tze::Precondition { extension_id, mode, payload } + } +} + +prop_compose! { + fn arb_tzeout()(value in arb_amount(), precondition in arb_precondition()) -> TzeOut { + TzeOut { value, precondition } + } +} + +fn tx_versions(branch_id: BranchId) -> impl Strategy { + match branch_id { + BranchId::Sprout => (1..(2 as u32)).prop_map(|i| (i, 0)).boxed(), + BranchId::Overwinter => Just((OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID)).boxed(), + BranchId::Future => Just((FUTURE_TX_VERSION, FUTURE_VERSION_GROUP_ID)).boxed(), + _otherwise => Just((SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID)).boxed(), + } +} + +prop_compose! { + fn arb_txdata(branch_id: BranchId)( + (version, version_group_id) in tx_versions(branch_id), + vin in vec(arb_txin(), 0..10), + vout in vec(arb_txout(), 0..10), + tze_inputs in vec(arb_tzein(), 0..10), + tze_outputs in vec(arb_tzeout(), 0..10), + lock_time in any::(), + expiry_height in any::(), + value_balance in arb_amount(), + ) -> TransactionData { + TransactionData { + overwintered: branch_id != BranchId::Sprout, + version, + version_group_id, + vin, vout, + tze_inputs: if branch_id == BranchId::Future { tze_inputs } else { vec![] }, + tze_outputs: if branch_id == BranchId::Future { tze_outputs } else { vec![] }, + lock_time, + expiry_height, + value_balance, + shielded_spends: vec![], //FIXME + shielded_outputs: vec![], //FIXME + joinsplits: vec![], //FIXME + joinsplit_pubkey: None, //FIXME + joinsplit_sig: None, //FIXME + binding_sig: None, //FIXME + } + } +} + +prop_compose! { + fn arb_tx(branch_id: BranchId)(tx_data in arb_txdata(branch_id)) -> Transaction { + Transaction::from_data(tx_data).unwrap() + } +} + #[test] fn tx_read_write() { let data = &self::data::tx_read_write::TX_READ_WRITE; @@ -66,6 +176,26 @@ fn tx_write_rejects_unexpected_binding_sig() { } } +proptest! { + #[test] + fn test_tze_roundtrip(tx in arb_tx(BranchId::Future)) { + let mut txn_bytes = vec![]; + tx.write(&mut txn_bytes).unwrap(); + + let txo = Transaction::read(&txn_bytes[..]).unwrap(); + + assert_eq!(tx.overwintered, txo.overwintered); + assert_eq!(tx.version, txo.version); + assert_eq!(tx.version_group_id, txo.version_group_id); + assert_eq!(tx.vin, txo.vin); + assert_eq!(tx.vout, txo.vout); + assert_eq!(tx.tze_inputs, txo.tze_inputs); + assert_eq!(tx.tze_outputs, txo.tze_outputs); + assert_eq!(tx.lock_time, txo.lock_time); + assert_eq!(tx.value_balance, txo.value_balance); + } +} + #[test] fn test_tze_tx_parse() { let txn_bytes = vec![