Merge pull request #392 from nuttycom/feature/zip-225-bundles

ZIP-225/244 #3: Create separate bundles for each part of the transaction.
This commit is contained in:
str4d 2021-06-04 03:43:15 +01:00 committed by GitHub
commit 4c4d226f40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1684 additions and 1024 deletions

View File

@ -238,10 +238,14 @@ where
}, },
RecipientAddress::Transparent(addr) => { RecipientAddress::Transparent(addr) => {
let script = addr.script(); let script = addr.script();
tx.vout tx.transparent_bundle
.iter() .as_ref()
.enumerate() .and_then(|b| {
.find(|(_, tx_out)| tx_out.script_pubkey == script) b.vout
.iter()
.enumerate()
.find(|(_, tx_out)| tx_out.script_pubkey == script)
})
.map(|(index, _)| index) .map(|(index, _)| index)
.expect("we sent to this address") .expect("we sent to this address")
} }

View File

@ -44,27 +44,30 @@ pub fn decrypt_transaction<P: consensus::Parameters>(
) -> Vec<DecryptedOutput> { ) -> Vec<DecryptedOutput> {
let mut decrypted = vec![]; let mut decrypted = vec![];
for (account, extfvk) in extfvks.iter() { if let Some(bundle) = tx.sapling_bundle.as_ref() {
let ivk = extfvk.fvk.vk.ivk(); for (account, extfvk) in extfvks.iter() {
let ovk = extfvk.fvk.ovk; let ivk = extfvk.fvk.vk.ivk();
let ovk = extfvk.fvk.ovk;
for (index, output) in tx.shielded_outputs.iter().enumerate() { for (index, output) in bundle.shielded_outputs.iter().enumerate() {
let ((note, to, memo), outgoing) = let ((note, to, memo), outgoing) =
match try_sapling_note_decryption(params, height, &ivk, output) { match try_sapling_note_decryption(params, height, &ivk, output) {
Some(ret) => (ret, false), Some(ret) => (ret, false),
None => match try_sapling_output_recovery(params, height, &ovk, output) { None => match try_sapling_output_recovery(params, height, &ovk, output) {
Some(ret) => (ret, true), Some(ret) => (ret, true),
None => continue, None => continue,
}, },
}; };
decrypted.push(DecryptedOutput {
index, decrypted.push(DecryptedOutput {
note, index,
account: *account, note,
to, account: *account,
memo, to,
outgoing, memo,
}) outgoing,
})
}
} }
} }

View File

@ -9,7 +9,7 @@ use zcash_primitives::{
consensus::BlockHeight, consensus::BlockHeight,
sapling::Nullifier, sapling::Nullifier,
transaction::{ transaction::{
components::sapling::{CompactOutputDescription, OutputDescription}, components::sapling::{self, CompactOutputDescription, OutputDescription},
TxId, TxId,
}, },
}; };
@ -113,8 +113,8 @@ impl compact_formats::CompactOutput {
} }
} }
impl From<OutputDescription> for compact_formats::CompactOutput { impl<A: sapling::Authorization> From<OutputDescription<A>> for compact_formats::CompactOutput {
fn from(out: OutputDescription) -> compact_formats::CompactOutput { fn from(out: OutputDescription<A>) -> compact_formats::CompactOutput {
let mut result = compact_formats::CompactOutput::new(); let mut result = compact_formats::CompactOutput::new();
result.set_cmu(out.cmu.to_repr().to_vec()); result.set_cmu(out.cmu.to_repr().to_vec());
result.set_epk(out.ephemeral_key.to_bytes().to_vec()); result.set_epk(out.ephemeral_key.to_bytes().to_vec());

View File

@ -489,8 +489,10 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
// //
// Assumes that create_spend_to_address() will never be called in parallel, which is a // Assumes that create_spend_to_address() will never be called in parallel, which is a
// reasonable assumption for a light client such as a mobile phone. // reasonable assumption for a light client such as a mobile phone.
for spend in &sent_tx.tx.shielded_spends { if let Some(bundle) = sent_tx.tx.sapling_bundle.as_ref() {
wallet::mark_spent(up, tx_ref, &spend.nullifier)?; for spend in &bundle.shielded_spends {
wallet::mark_spent(up, tx_ref, &spend.nullifier)?;
}
} }
wallet::insert_sent_note( wallet::insert_sent_note(

View File

@ -631,7 +631,8 @@ mod tests {
) )
.unwrap(); .unwrap();
let output = &tx.shielded_outputs[output_index as usize]; let output =
&tx.sapling_bundle.as_ref().unwrap().shielded_outputs[output_index as usize];
try_sapling_output_recovery( try_sapling_output_recovery(
&network, &network,

View File

@ -17,3 +17,9 @@ ff = "0.9"
jubjub = "0.6" jubjub = "0.6"
rand_core = "0.6" rand_core = "0.6"
zcash_proofs = { version = "0.5", path = "../zcash_proofs" } zcash_proofs = { version = "0.5", path = "../zcash_proofs" }
orchard = { git = "https://github.com/zcash/orchard", branch = "main" }
secp256k1 = { version = "0.20", features = ["rand", "bitcoin_hashes"] }
[features]
transparent-inputs = []

View File

@ -2,8 +2,10 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use zcash_primitives::consensus::{BlockHeight, BranchId}; use zcash_primitives::consensus::{BlockHeight, BranchId};
use zcash_primitives::extensions::transparent::{Error, Extension, Precondition, Witness}; use zcash_primitives::extensions::transparent::{
use zcash_primitives::transaction::{components::TzeOut, Transaction}; AuthData, Error, Extension, Precondition, Witness,
};
use zcash_primitives::transaction::{components::tze::TzeOut, Transaction};
use crate::transparent::demo; use crate::transparent::demo;
@ -67,7 +69,7 @@ pub trait Epoch {
fn verify<'a>( fn verify<'a>(
&self, &self,
precondition: &Precondition, precondition: &Precondition,
witness: &Witness, witness: &Witness<AuthData>,
ctx: &Context<'a>, ctx: &Context<'a>,
) -> Result<(), Error<Self::Error>>; ) -> Result<(), Error<Self::Error>>;
} }
@ -76,15 +78,18 @@ pub trait Epoch {
/// by the context. /// by the context.
impl<'a> demo::Context for Context<'a> { impl<'a> demo::Context for Context<'a> {
fn is_tze_only(&self) -> bool { fn is_tze_only(&self) -> bool {
self.tx.vin.is_empty() self.tx.transparent_bundle.is_none()
&& self.tx.vout.is_empty() && self.tx.sapling_bundle.is_none()
&& self.tx.shielded_spends.is_empty() && self.tx.sprout_bundle.is_none()
&& self.tx.shielded_outputs.is_empty() && self.tx.orchard_bundle.is_none()
&& self.tx.joinsplits.is_empty()
} }
fn tx_tze_outputs(&self) -> &[TzeOut] { fn tx_tze_outputs(&self) -> &[TzeOut] {
&self.tx.tze_outputs if let Some(bundle) = &self.tx.tze_bundle {
&bundle.vout
} else {
&[]
}
} }
} }
@ -98,7 +103,7 @@ impl Epoch for EpochVTest {
fn verify<'a>( fn verify<'a>(
&self, &self,
precondition: &Precondition, precondition: &Precondition,
witness: &Witness, witness: &Witness<AuthData>,
ctx: &Context<'a>, ctx: &Context<'a>,
) -> Result<(), Error<Self::Error>> { ) -> Result<(), Error<Self::Error>> {
let ext_id = ExtensionId::try_from(precondition.extension_id) let ext_id = ExtensionId::try_from(precondition.extension_id)

View File

@ -21,12 +21,16 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use std::convert::TryInto; use std::convert::TryInto;
use std::fmt; use std::fmt;
use std::ops::{Deref, DerefMut};
use blake2b_simd::Params; use blake2b_simd::Params;
use zcash_primitives::{ use zcash_primitives::{
extensions::transparent::{Extension, ExtensionTxBuilder, FromPayload, ToPayload}, extensions::transparent::{Extension, ExtensionTxBuilder, FromPayload, ToPayload},
transaction::components::{amount::Amount, TzeOut, TzeOutPoint}, transaction::components::{
amount::Amount,
tze::{OutPoint, TzeOut},
},
}; };
/// Types and constants used for Mode 0 (open a channel) /// Types and constants used for Mode 0 (open a channel)
@ -336,6 +340,20 @@ pub struct DemoBuilder<B> {
pub extension_id: u32, pub extension_id: u32,
} }
impl<B> Deref for DemoBuilder<B> {
type Target = B;
fn deref(&self) -> &Self::Target {
&self.txn_builder
}
}
impl<B> DerefMut for DemoBuilder<B> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.txn_builder
}
}
/// Errors that can occur in construction of transactions using `DemoBuilder`. /// Errors that can occur in construction of transactions using `DemoBuilder`.
#[derive(Debug)] #[derive(Debug)]
pub enum DemoBuildError<E> { pub enum DemoBuildError<E> {
@ -356,7 +374,7 @@ pub enum DemoBuildError<E> {
/// Convenience methods for use with [`zcash_primitives::transaction::builder::Builder`] /// Convenience methods for use with [`zcash_primitives::transaction::builder::Builder`]
/// for constructing transactions that utilize the features of the demo extension. /// for constructing transactions that utilize the features of the demo extension.
impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> { impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<B> {
/// Add a channel-opening precondition to the outputs of the transaction under /// Add a channel-opening precondition to the outputs of the transaction under
/// construction. /// construction.
pub fn demo_open( pub fn demo_open(
@ -374,7 +392,7 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> {
/// precondition to the transaction under construction. /// precondition to the transaction under construction.
pub fn demo_transfer_to_close( pub fn demo_transfer_to_close(
&mut self, &mut self,
prevout: (TzeOutPoint, TzeOut), prevout: (OutPoint, TzeOut),
transfer_amount: Amount, transfer_amount: Amount,
preimage_1: [u8; 32], preimage_1: [u8; 32],
hash_2: [u8; 32], hash_2: [u8; 32],
@ -416,7 +434,7 @@ impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> {
/// Add a channel-closing witness to the transaction under construction. /// Add a channel-closing witness to the transaction under construction.
pub fn demo_close( pub fn demo_close(
&mut self, &mut self,
prevout: (TzeOutPoint, TzeOut), prevout: (OutPoint, TzeOut),
preimage_2: [u8; 32], preimage_2: [u8; 32],
) -> Result<(), DemoBuildError<B::BuildError>> { ) -> Result<(), DemoBuildError<B::BuildError>> {
let hash_2 = { let hash_2 = {
@ -467,20 +485,19 @@ mod tests {
use zcash_proofs::prover::LocalTxProver; use zcash_proofs::prover::LocalTxProver;
use zcash_primitives::{ use zcash_primitives::{
consensus::{BlockHeight, NetworkUpgrade, Parameters}, consensus::{BlockHeight, BranchId, NetworkUpgrade, Parameters},
constants, constants,
extensions::transparent::{self as tze, Extension, FromPayload, ToPayload}, extensions::transparent::{self as tze, Extension, FromPayload, ToPayload},
legacy::TransparentAddress, legacy::TransparentAddress,
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::Node, sapling::{Node, Rseed},
sapling::Rseed,
transaction::{ transaction::{
builder::Builder, builder::Builder,
components::{ components::{
amount::{Amount, DEFAULT_FEE}, amount::{Amount, DEFAULT_FEE},
TzeIn, TzeOut, TzeOutPoint, tze::{Bundle, OutPoint, TzeIn, TzeOut},
}, },
Transaction, TransactionData, Transaction, TransactionData, TxVersion,
}, },
zip32::ExtendedSpendingKey, zip32::ExtendedSpendingKey,
}; };
@ -604,15 +621,24 @@ mod tests {
/// by the context. /// by the context.
impl<'a> Context for Ctx<'a> { impl<'a> Context for Ctx<'a> {
fn is_tze_only(&self) -> bool { fn is_tze_only(&self) -> bool {
self.tx.vin.is_empty() self.tx.transparent_bundle.is_none()
&& self.tx.vout.is_empty() && self.tx.sprout_bundle.is_none()
&& self.tx.shielded_spends.is_empty() && self.tx.sapling_bundle.is_none()
&& self.tx.shielded_outputs.is_empty() && self.tx.orchard_bundle.is_none()
&& self.tx.joinsplits.is_empty()
} }
fn tx_tze_outputs(&self) -> &[TzeOut] { fn tx_tze_outputs(&self) -> &[TzeOut] {
&self.tx.tze_outputs match &self.tx.tze_bundle {
Some(b) => &b.vout,
None => &[],
}
}
}
fn demo_builder<'a>(height: BlockHeight) -> DemoBuilder<Builder<'a, FutureNetwork, OsRng>> {
DemoBuilder {
txn_builder: Builder::new(FutureNetwork, height),
extension_id: 0,
} }
} }
@ -657,47 +683,83 @@ mod tests {
precondition: tze::Precondition::from(0, &Precondition::open(hash_1)), precondition: tze::Precondition::from(0, &Precondition::open(hash_1)),
}; };
let mut mtx_a = TransactionData::zfuture(); let tx_a = TransactionData {
mtx_a.tze_outputs.push(out_a); version: TxVersion::ZFuture,
let tx_a = mtx_a.freeze().unwrap(); lock_time: 0,
expiry_height: 0u32.into(),
transparent_bundle: None,
sprout_bundle: None,
sapling_bundle: None,
orchard_bundle: None,
tze_bundle: Some(Bundle {
vin: vec![],
vout: vec![out_a],
}),
}
.freeze(BranchId::ZFuture)
.unwrap();
// //
// Transfer // Transfer
// //
let in_b = TzeIn { let in_b = TzeIn {
prevout: TzeOutPoint::new(tx_a.txid(), 0), prevout: OutPoint::new(tx_a.txid(), 0),
witness: tze::Witness::from(0, &Witness::open(preimage_1)), witness: tze::Witness::from(0, &Witness::open(preimage_1)),
}; };
let out_b = TzeOut { let out_b = TzeOut {
value: Amount::from_u64(1).unwrap(), value: Amount::from_u64(1).unwrap(),
precondition: tze::Precondition::from(0, &Precondition::close(hash_2)), precondition: tze::Precondition::from(0, &Precondition::close(hash_2)),
}; };
let mut mtx_b = TransactionData::zfuture();
mtx_b.tze_inputs.push(in_b); let tx_b = TransactionData {
mtx_b.tze_outputs.push(out_b); version: TxVersion::ZFuture,
let tx_b = mtx_b.freeze().unwrap(); lock_time: 0,
expiry_height: 0u32.into(),
transparent_bundle: None,
sprout_bundle: None,
sapling_bundle: None,
orchard_bundle: None,
tze_bundle: Some(Bundle {
vin: vec![in_b],
vout: vec![out_b],
}),
}
.freeze(BranchId::ZFuture)
.unwrap();
// //
// Closing transaction // Closing transaction
// //
let in_c = TzeIn { let in_c = TzeIn {
prevout: TzeOutPoint::new(tx_b.txid(), 0), prevout: OutPoint::new(tx_b.txid(), 0),
witness: tze::Witness::from(0, &Witness::close(preimage_2)), witness: tze::Witness::from(0, &Witness::close(preimage_2)),
}; };
let mut mtx_c = TransactionData::zfuture(); let tx_c = TransactionData {
mtx_c.tze_inputs.push(in_c); version: TxVersion::ZFuture,
let tx_c = mtx_c.freeze().unwrap(); lock_time: 0,
expiry_height: 0u32.into(),
transparent_bundle: None,
sprout_bundle: None,
sapling_bundle: None,
orchard_bundle: None,
tze_bundle: Some(Bundle {
vin: vec![in_c],
vout: vec![],
}),
}
.freeze(BranchId::ZFuture)
.unwrap();
// Verify tx_b // Verify tx_b
{ {
let ctx = Ctx { tx: &tx_b }; let ctx = Ctx { tx: &tx_b };
assert_eq!( assert_eq!(
Program.verify( Program.verify(
&tx_a.tze_outputs[0].precondition, &tx_a.tze_bundle.as_ref().unwrap().vout[0].precondition,
&tx_b.tze_inputs[0].witness, &tx_b.tze_bundle.as_ref().unwrap().vin[0].witness,
&ctx &ctx
), ),
Ok(()) Ok(())
@ -709,8 +771,8 @@ mod tests {
let ctx = Ctx { tx: &tx_c }; let ctx = Ctx { tx: &tx_c };
assert_eq!( assert_eq!(
Program.verify( Program.verify(
&tx_b.tze_outputs[0].precondition, &tx_b.tze_bundle.as_ref().unwrap().vout[0].precondition,
&tx_c.tze_inputs[0].witness, &tx_c.tze_bundle.as_ref().unwrap().vin[0].witness,
&ctx &ctx
), ),
Ok(()) Ok(())
@ -738,7 +800,6 @@ mod tests {
// //
let mut rng = OsRng; let mut rng = OsRng;
let mut builder_a = Builder::new(FutureNetwork, tx_height);
// create some inputs to spend // create some inputs to spend
let extsk = ExtendedSpendingKey::master(&[]); let extsk = ExtendedSpendingKey::master(&[]);
@ -753,61 +814,50 @@ mod tests {
tree.append(cm1).unwrap(); tree.append(cm1).unwrap();
let witness1 = IncrementalWitness::from_tree(&tree); let witness1 = IncrementalWitness::from_tree(&tree);
let mut builder_a = demo_builder(tx_height);
builder_a builder_a
.add_sapling_spend(extsk, *to.diversifier(), note1, witness1.path().unwrap()) .add_sapling_spend(extsk, *to.diversifier(), note1, witness1.path().unwrap())
.unwrap(); .unwrap();
let mut db_a = DemoBuilder {
txn_builder: &mut builder_a,
extension_id: 0,
};
let value = Amount::from_u64(100000).unwrap(); let value = Amount::from_u64(100000).unwrap();
let (h1, h2) = demo_hashes(&preimage_1, &preimage_2); let (h1, h2) = demo_hashes(&preimage_1, &preimage_2);
db_a.demo_open(value, h1) builder_a
.demo_open(value, h1)
.map_err(|e| format!("open failure: {:?}", e)) .map_err(|e| format!("open failure: {:?}", e))
.unwrap(); .unwrap();
let (tx_a, _) = builder_a let (tx_a, _) = builder_a
.txn_builder
.build(&prover) .build(&prover)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_a = tx_a.tze_bundle.as_ref().unwrap();
// //
// Transfer // Transfer
// //
let mut builder_b = Builder::new(FutureNetwork, tx_height + 1); let mut builder_b = demo_builder(tx_height + 1);
let mut db_b = DemoBuilder { let prevout_a = (OutPoint::new(tx_a.txid(), 0), tze_a.vout[0].clone());
txn_builder: &mut builder_b,
extension_id: 0,
};
let prevout_a = (
TzeOutPoint::new(tx_a.txid(), 0),
tx_a.tze_outputs[0].clone(),
);
let value_xfr = (value - DEFAULT_FEE).unwrap(); let value_xfr = (value - DEFAULT_FEE).unwrap();
db_b.demo_transfer_to_close(prevout_a, value_xfr, preimage_1, h2) builder_b
.demo_transfer_to_close(prevout_a, value_xfr, preimage_1, h2)
.map_err(|e| format!("transfer failure: {:?}", e)) .map_err(|e| format!("transfer failure: {:?}", e))
.unwrap(); .unwrap();
let (tx_b, _) = builder_b let (tx_b, _) = builder_b
.txn_builder
.build(&prover) .build(&prover)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_b = tx_b.tze_bundle.as_ref().unwrap();
// //
// Closing transaction // Closing transaction
// //
let mut builder_c = Builder::new(FutureNetwork, tx_height + 2); let mut builder_c = demo_builder(tx_height + 2);
let mut db_c = DemoBuilder { let prevout_b = (OutPoint::new(tx_a.txid(), 0), tze_b.vout[0].clone());
txn_builder: &mut builder_c, builder_c
extension_id: 0, .demo_close(prevout_b, preimage_2)
};
let prevout_b = (
TzeOutPoint::new(tx_a.txid(), 0),
tx_b.tze_outputs[0].clone(),
);
db_c.demo_close(prevout_b, preimage_2)
.map_err(|e| format!("close failure: {:?}", e)) .map_err(|e| format!("close failure: {:?}", e))
.unwrap(); .unwrap();
@ -819,29 +869,23 @@ mod tests {
.unwrap(); .unwrap();
let (tx_c, _) = builder_c let (tx_c, _) = builder_c
.txn_builder
.build(&prover) .build(&prover)
.map_err(|e| format!("build failure: {:?}", e)) .map_err(|e| format!("build failure: {:?}", e))
.unwrap(); .unwrap();
let tze_c = tx_c.tze_bundle.as_ref().unwrap();
// Verify tx_b // Verify tx_b
let ctx0 = Ctx { tx: &tx_b }; let ctx0 = Ctx { tx: &tx_b };
assert_eq!( assert_eq!(
Program.verify( Program.verify(&tze_a.vout[0].precondition, &tze_b.vin[0].witness, &ctx0),
&tx_a.tze_outputs[0].precondition,
&tx_b.tze_inputs[0].witness,
&ctx0
),
Ok(()) Ok(())
); );
// Verify tx_c // Verify tx_c
let ctx1 = Ctx { tx: &tx_c }; let ctx1 = Ctx { tx: &tx_c };
assert_eq!( assert_eq!(
Program.verify( Program.verify(&tze_b.vout[0].precondition, &tze_c.vin[0].witness, &ctx1),
&tx_b.tze_outputs[0].precondition,
&tx_c.tze_inputs[0].witness,
&ctx1
),
Ok(()) Ok(())
); );
} }

View File

@ -12,7 +12,7 @@ and this library adheres to Rust's notion of
sent. sent.
- `zcash_primitives::transaction::Txid::{read, write, from_bytes}` - `zcash_primitives::transaction::Txid::{read, write, from_bytes}`
- `zcash_primitives::sapling::NoteValue` a typesafe wrapper for Sapling note values. - `zcash_primitives::sapling::NoteValue` a typesafe wrapper for Sapling note values.
- `zcash_primitives::consensus::BranchId::{height_range, height_bounds}` functions - `zcash_primitives::consensus::BranchId::{height_range, height_bounds}` functions
to provide range values for branch active heights. to provide range values for branch active heights.
- `zcash_primitives::consensus::NetworkUpgrade::Nu5` value representing the Nu5 upgrade. - `zcash_primitives::consensus::NetworkUpgrade::Nu5` value representing the Nu5 upgrade.
- `zcash_primitives::consensus::BranchId::Nu5` value representing the Nu5 consensus branch. - `zcash_primitives::consensus::BranchId::Nu5` value representing the Nu5 consensus branch.
@ -21,6 +21,31 @@ and this library adheres to Rust's notion of
- `sapling::builder` for Sapling transaction components. - `sapling::builder` for Sapling transaction components.
- `transparent::builder` for transparent transaction components. - `transparent::builder` for transparent transaction components.
- `tze::builder` for TZE transaction components. - `tze::builder` for TZE transaction components.
- `orchard` parsing and serialization for Orchard transaction components.
- `zcash_primitives::transaction::Authorization` a trait representing a type-level
record of authorization types that correspond to signatures, witnesses, and
proofs for each Zcash sub-protocol (transparent, Sprout, Sapling, TZE, and
Orchard). This type makes it possible to encode a type-safe state machine
for the application of authorizing data to a transaction; implementations of
this trait represent different states of the authorization process.
- New bundle types under the `zcash_primitives::transaction` submodules, one for
each Zcash sub-protocol. These are now used instead of bare fields
within the `TransactionData` type.
- `components::sapling::Bundle` bundle of
Sapling transaction elements. This new struct is parameterized by a
type bounded on a newly added `sapling::Authorization` trait which
is used to enable static reasoning about the state of Sapling proofs and
authorizing data, as described above.
- `components::transparent::Bundle` bundle of
transparent transaction elements. This new struct is parameterized by a
type bounded on a newly added `transparent::Authorization` trait which
is used to enable static reasoning about the state of transparent witness
data, as described above.
- `components::tze::Bundle` bundle of TZE
transaction elements. This new struct is parameterized by a
type bounded on a newly added `tze::Authorization` trait which
is used to enable static reasoning about the state of TZE witness
data, as described above.
### Changed ### Changed
- MSRV is now 1.51.0. - MSRV is now 1.51.0.
@ -48,6 +73,23 @@ and this library adheres to Rust's notion of
- `Builder::build` no longer takes a consensus branch ID parameter. The - `Builder::build` no longer takes a consensus branch ID parameter. The
builder now selects the correct consensus branch ID for the given target builder now selects the correct consensus branch ID for the given target
height. height.
- The `zcash_primitives::transaction::TransactionData` struct has been modified
such that it now contains common header information, and then contains
a separate `Bundle` value for each sub-protocol (transparent, Sprout, Sapling,
and TZE) and an Orchard bundle value has been added. `TransactionData` is now
parameterized by a type bounded on the newly added
`zcash_primitives::transaction::Authorization` trait. This bound has been
propagated to the individual transaction builders, such that the authorization
state of a transaction is clearly represented in the type and the presence
or absence of witness and/or proof data is statically known, instead of being only
determined at runtime via the presence or absence of `Option`al values.
- `zcash_primitives::transaction::components::sapling` parsing and serialization
have been adapted for use with the new `sapling::Bundle` type.
- `zcash_primitives::transaction::Transaction` parsing and serialization
have been adapted for use with the new `TransactionData` organization.
- Generators for property testing have been moved out of the main transaction
module such that they are now colocated in the modules with the types
that they generate.
## [0.5.0] - 2021-03-26 ## [0.5.0] - 2021-03-26
### Added ### Added
@ -150,7 +192,7 @@ and this library adheres to Rust's notion of
- `try_sapling_output_recovery` - `try_sapling_output_recovery`
- `try_sapling_output_recovery_with_ock` - `try_sapling_output_recovery_with_ock`
- `zcash_primitives::primitives::SaplingIvk` is now used where functions - `zcash_primitives::primitives::SaplingIvk` is now used where functions
previously used undistinguished `jubjub::Fr` values; this affects Sapling previously used undistinguished `jubjub::Fr` values; this affects Sapling
note decryption and handling of IVKs by the wallet backend code. note decryption and handling of IVKs by the wallet backend code.
- `zcash_primitives::primitives::ViewingKey::ivk` now returns `SaplingIvk` - `zcash_primitives::primitives::ViewingKey::ivk` now returns `SaplingIvk`
- `zcash_primitives::primitives::Note::nf` now returns `Nullifier`. - `zcash_primitives::primitives::Note::nf` now returns `Nullifier`.

View File

@ -9,7 +9,10 @@ use zcash_primitives::{
util::generate_random_rseed, util::generate_random_rseed,
Diversifier, PaymentAddress, SaplingIvk, ValueCommitment, Diversifier, PaymentAddress, SaplingIvk, ValueCommitment,
}, },
transaction::components::{OutputDescription, GROTH_PROOF_SIZE}, transaction::components::{
sapling::{Authorized, OutputDescription},
GROTH_PROOF_SIZE,
},
}; };
fn bench_note_decryption(c: &mut Criterion) { fn bench_note_decryption(c: &mut Criterion) {
@ -20,7 +23,7 @@ fn bench_note_decryption(c: &mut Criterion) {
let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
// Construct a fake Sapling output as if we had just deserialized a transaction. // Construct a fake Sapling output as if we had just deserialized a transaction.
let output = { let output: OutputDescription<Authorized> = {
let diversifier = Diversifier([0; 11]); let diversifier = Diversifier([0; 11]);
let pk_d = diversifier.g_d().unwrap() * valid_ivk.0; let pk_d = diversifier.g_d().unwrap() * valid_ivk.0;
let pa = PaymentAddress::from_parts(diversifier, pk_d).unwrap(); let pa = PaymentAddress::from_parts(diversifier, pk_d).unwrap();

View File

@ -4,4 +4,3 @@
# #
# It is recommended to check this file in to source control so that # It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases. # everyone who runs the test benefits from these saved cases.
cc 23a823e9da7e6ae62a79153ab97362dd9d81b8d9eafc396c87870dfa8aa7354c # shrinks to tx = Transaction { txid: TxId([67, 236, 122, 87, 159, 85, 97, 164, 42, 126, 150, 55, 173, 65, 86, 103, 39, 53, 166, 88, 190, 39, 82, 24, 24, 1, 247, 35, 186, 51, 22, 210]), data: TransactionData( version = Sprout(1), vin = [], vout = [], lock_time = 0, expiry_height = BlockHeight(0), value_balance = Amount(1), shielded_spends = [], shielded_outputs = [], joinsplits = [], joinsplit_pubkey = None, binding_sig = None) }

View File

@ -424,7 +424,7 @@ pub const ZIP212_GRACE_PERIOD: u32 = 32256;
/// ///
/// See [ZIP 200](https://zips.z.cash/zip-0200) for more details. /// See [ZIP 200](https://zips.z.cash/zip-0200) for more details.
/// ///
/// [`signature_hash`]: crate::transaction::signature_hash /// [`signature_hash`]: crate::transaction::sighash::signature_hash
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum BranchId { pub enum BranchId {
/// The consensus rules at the launch of Zcash. /// The consensus rules at the launch of Zcash.
@ -554,6 +554,10 @@ impl BranchId {
.map(|lower| (lower, None)), .map(|lower| (lower, None)),
} }
} }
pub fn sprout_uses_groth_proofs(&self) -> bool {
!matches!(self, BranchId::Sprout | BranchId::Overwinter)
}
} }
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]

View File

@ -1,8 +1,16 @@
//! Core traits and structs for Transparent Zcash Extensions. //! Core traits and structs for Transparent Zcash Extensions.
use crate::transaction::components::{Amount, TzeOut, TzeOutPoint};
use std::fmt; use std::fmt;
use crate::transaction::components::{
tze::{self, TzeOut},
Amount,
};
/// A typesafe wrapper for witness payloads
#[derive(Debug, Clone, PartialEq)]
pub struct AuthData(pub Vec<u8>);
/// Binary parsing capability for TZE preconditions & witnesses. /// Binary parsing capability for TZE preconditions & witnesses.
/// ///
/// Serialization formats interpreted by implementations of this trait become consensus-critical /// Serialization formats interpreted by implementations of this trait become consensus-critical
@ -62,28 +70,28 @@ impl Precondition {
/// treated as opaque to all but the extension corresponding to the encapsulated `extension_id` /// treated as opaque to all but the extension corresponding to the encapsulated `extension_id`
/// value. /// value.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Witness { pub struct Witness<T> {
pub extension_id: u32, pub extension_id: u32,
pub mode: u32, pub mode: u32,
pub payload: Vec<u8>, pub payload: T,
} }
impl Witness { impl Witness<AuthData> {
/// Produce the intermediate format for an extension-specific witness /// Produce the intermediate format for an extension-specific witness
/// type. /// type.
pub fn from<P: ToPayload>(extension_id: u32, value: &P) -> Witness { pub fn from<P: ToPayload>(extension_id: u32, value: &P) -> Witness<AuthData> {
let (mode, payload) = value.to_payload(); let (mode, payload) = value.to_payload();
Witness { Witness {
extension_id, extension_id,
mode, mode,
payload, payload: AuthData(payload),
} }
} }
/// Attempt to parse an extension-specific witness value from the /// Attempt to parse an extension-specific witness value from the
/// intermediate representation. /// intermediate representation.
pub fn try_to<P: FromPayload>(&self) -> Result<P, P::Error> { pub fn try_to<P: FromPayload>(&self) -> Result<P, P::Error> {
P::from_payload(self.mode, &self.payload) P::from_payload(self.mode, &self.payload.0)
} }
} }
@ -137,7 +145,7 @@ pub trait Extension<C> {
fn verify( fn verify(
&self, &self,
precondition: &Precondition, precondition: &Precondition,
witness: &Witness, witness: &Witness<AuthData>,
context: &C, context: &C,
) -> Result<(), Self::Error> ) -> Result<(), Self::Error>
where where
@ -146,7 +154,7 @@ pub trait Extension<C> {
{ {
self.verify_inner( self.verify_inner(
&Self::Precondition::from_payload(precondition.mode, &precondition.payload)?, &Self::Precondition::from_payload(precondition.mode, &precondition.payload)?,
&Self::Witness::from_payload(witness.mode, &witness.payload)?, &Self::Witness::from_payload(witness.mode, &witness.payload.0)?,
&context, &context,
) )
} }
@ -178,7 +186,7 @@ pub trait ExtensionTxBuilder<'a> {
&mut self, &mut self,
extension_id: u32, extension_id: u32,
mode: u32, mode: u32,
prevout: (TzeOutPoint, TzeOut), prevout: (tze::OutPoint, TzeOut),
witness_builder: WBuilder, witness_builder: WBuilder,
) -> Result<(), Self::BuildError> ) -> Result<(), Self::BuildError>
where where

View File

@ -47,7 +47,7 @@ impl<Node: Hashable> PathFiller<Node> {
/// ///
/// The depth of the Merkle tree is fixed at 32, equal to the depth of the Sapling /// The depth of the Merkle tree is fixed at 32, equal to the depth of the Sapling
/// commitment tree. /// commitment tree.
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct CommitmentTree<Node: Hashable> { pub struct CommitmentTree<Node: Hashable> {
left: Option<Node>, left: Option<Node>,
right: Option<Node>, right: Option<Node>,
@ -1057,3 +1057,25 @@ mod tests {
} }
} }
} }
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use core::fmt::Debug;
use proptest::collection::vec;
use proptest::prelude::*;
use super::{CommitmentTree, Hashable};
pub fn arb_commitment_tree<Node: Hashable + Debug, T: Strategy<Value = Node>>(
min_size: usize,
arb_node: T,
) -> impl Strategy<Value = CommitmentTree<Node>> {
vec(arb_node, min_size..(min_size + 100)).prop_map(|v| {
let mut tree = CommitmentTree::empty();
for node in v.into_iter() {
tree.append(node).unwrap();
}
tree
})
}
}

View File

@ -17,7 +17,10 @@ use crate::{
consensus::{self, BlockHeight, NetworkUpgrade::Canopy, ZIP212_GRACE_PERIOD}, consensus::{self, BlockHeight, NetworkUpgrade::Canopy, ZIP212_GRACE_PERIOD},
memo::MemoBytes, memo::MemoBytes,
sapling::{keys::OutgoingViewingKey, Diversifier, Note, PaymentAddress, Rseed, SaplingIvk}, sapling::{keys::OutgoingViewingKey, Diversifier, Note, PaymentAddress, Rseed, SaplingIvk},
transaction::components::{amount::Amount, sapling::OutputDescription}, transaction::components::{
amount::Amount,
sapling::{self, OutputDescription},
},
}; };
pub const KDF_SAPLING_PERSONALIZATION: &[u8; 16] = b"Zcash_SaplingKDF"; pub const KDF_SAPLING_PERSONALIZATION: &[u8; 16] = b"Zcash_SaplingKDF";
@ -383,7 +386,7 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
params: &P, params: &P,
height: BlockHeight, height: BlockHeight,
ock: &OutgoingCipherKey, ock: &OutgoingCipherKey,
output: &OutputDescription, output: &OutputDescription<sapling::Authorized>,
) -> Option<(Note, PaymentAddress, MemoBytes)> { ) -> Option<(Note, PaymentAddress, MemoBytes)> {
let domain = SaplingDomain { let domain = SaplingDomain {
params: params.clone(), params: params.clone(),
@ -405,7 +408,7 @@ pub fn try_sapling_output_recovery<P: consensus::Parameters>(
params: &P, params: &P,
height: BlockHeight, height: BlockHeight,
ovk: &OutgoingViewingKey, ovk: &OutgoingViewingKey,
output: &OutputDescription, output: &OutputDescription<sapling::Authorized>,
) -> Option<(Note, PaymentAddress, MemoBytes)> { ) -> Option<(Note, PaymentAddress, MemoBytes)> {
let domain = SaplingDomain { let domain = SaplingDomain {
params: params.clone(), params: params.clone(),
@ -450,7 +453,7 @@ mod tests {
}, },
transaction::components::{ transaction::components::{
amount::Amount, amount::Amount,
sapling::{CompactOutputDescription, OutputDescription}, sapling::{self, CompactOutputDescription, OutputDescription},
GROTH_PROOF_SIZE, GROTH_PROOF_SIZE,
}, },
}; };
@ -462,7 +465,7 @@ mod tests {
OutgoingViewingKey, OutgoingViewingKey,
OutgoingCipherKey, OutgoingCipherKey,
SaplingIvk, SaplingIvk,
OutputDescription, OutputDescription<sapling::Authorized>,
) { ) {
let ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); let ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
@ -492,7 +495,11 @@ mod tests {
height: BlockHeight, height: BlockHeight,
ivk: &SaplingIvk, ivk: &SaplingIvk,
mut rng: &mut R, mut rng: &mut R,
) -> (OutgoingViewingKey, OutgoingCipherKey, OutputDescription) { ) -> (
OutgoingViewingKey,
OutgoingCipherKey,
OutputDescription<sapling::Authorized>,
) {
let diversifier = Diversifier([0; 11]); let diversifier = Diversifier([0; 11]);
let pk_d = diversifier.g_d().unwrap() * ivk.0; let pk_d = diversifier.g_d().unwrap() * ivk.0;
let pa = PaymentAddress::from_parts_unchecked(diversifier, pk_d); let pa = PaymentAddress::from_parts_unchecked(diversifier, pk_d);

View File

@ -1,11 +1,13 @@
//! Structs for building transactions. //! Structs for building transactions.
use core::array; use rand::{rngs::OsRng, CryptoRng, RngCore};
use std::array;
use std::error; use std::error;
use std::fmt; use std::fmt;
use std::io;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use rand::{rngs::OsRng, CryptoRng, RngCore}; use orchard::bundle::{self as orchard};
use crate::{ use crate::{
consensus::{self, BlockHeight, BranchId}, consensus::{self, BlockHeight, BranchId},
@ -13,15 +15,20 @@ use crate::{
memo::MemoBytes, memo::MemoBytes,
merkle_tree::MerklePath, merkle_tree::MerklePath,
sapling::{ sapling::{
keys::OutgoingViewingKey, prover::TxProver, Diversifier, Node, Note, PaymentAddress, keys::OutgoingViewingKey, prover::TxProver, redjubjub, Diversifier, Node, Note,
PaymentAddress,
}, },
transaction::{ transaction::{
components::{ components::{
amount::{Amount, DEFAULT_FEE}, amount::{Amount, DEFAULT_FEE},
sapling::builder::{self as sapling, SaplingBuilder, SaplingMetadata}, sapling::{
transparent::builder::{self as transparent, TransparentBuilder}, self,
builder::{SaplingBuilder, SaplingMetadata},
},
transparent::{self, builder::TransparentBuilder},
}, },
signature_hash_data, SignableInput, Transaction, TransactionData, TxVersion, SIGHASH_ALL, signature_hash_data, SignableInput, Transaction, TransactionData, TxVersion, Unauthorized,
SIGHASH_ALL,
}, },
zip32::ExtendedSpendingKey, zip32::ExtendedSpendingKey,
}; };
@ -30,14 +37,14 @@ use crate::{
use std::marker::PhantomData; use std::marker::PhantomData;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use crate::transaction::components::{OutPoint, TxOut}; use crate::{legacy::Script, transaction::components::transparent::TxOut};
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
use crate::{ use crate::{
extensions::transparent::{ExtensionTxBuilder, ToPayload}, extensions::transparent::{AuthData, ExtensionTxBuilder, ToPayload},
transaction::components::{ transaction::components::{
tze::builder::{self as tzebuilder, TzeBuilder}, tze::builder::TzeBuilder,
TzeOut, TzeOutPoint, tze::{self, TzeOut},
}, },
}; };
@ -51,10 +58,10 @@ pub enum Error {
ChangeIsNegative(Amount), ChangeIsNegative(Amount),
InvalidAmount, InvalidAmount,
NoChangeAddress, NoChangeAddress,
TransparentBuild(transparent::Error), TransparentBuild(transparent::builder::Error),
SaplingBuild(sapling::Error), SaplingBuild(sapling::builder::Error),
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
TzeBuild(tzebuilder::Error), TzeBuild(tze::builder::Error),
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@ -109,7 +116,7 @@ enum ChangeAddress {
} }
/// Generates a [`Transaction`] from its inputs and outputs. /// Generates a [`Transaction`] from its inputs and outputs.
pub struct Builder<'a, P: consensus::Parameters, R: RngCore> { pub struct Builder<'a, P, R> {
params: P, params: P,
rng: R, rng: R,
target_height: BlockHeight, target_height: BlockHeight,
@ -119,7 +126,7 @@ pub struct Builder<'a, P: consensus::Parameters, R: RngCore> {
sapling_builder: SaplingBuilder<P>, sapling_builder: SaplingBuilder<P>,
change_address: Option<ChangeAddress>, change_address: Option<ChangeAddress>,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
tze_builder: TzeBuilder<'a, TransactionData>, tze_builder: TzeBuilder<'a, TransactionData<Unauthorized>>,
#[cfg(not(feature = "zfuture"))] #[cfg(not(feature = "zfuture"))]
tze_builder: PhantomData<&'a ()>, tze_builder: PhantomData<&'a ()>,
progress_notifier: Option<Sender<Progress>>, progress_notifier: Option<Sender<Progress>>,
@ -168,7 +175,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA, expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA,
fee: DEFAULT_FEE, fee: DEFAULT_FEE,
transparent_builder: TransparentBuilder::empty(), transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::empty(params, target_height), sapling_builder: SaplingBuilder::new(params, target_height),
change_address: None, change_address: None,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::empty(), tze_builder: TzeBuilder::empty(),
@ -213,7 +220,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
pub fn add_transparent_input( pub fn add_transparent_input(
&mut self, &mut self,
sk: secp256k1::SecretKey, sk: secp256k1::SecretKey,
utxo: OutPoint, utxo: transparent::OutPoint,
coin: TxOut, coin: TxOut,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.transparent_builder self.transparent_builder
@ -316,10 +323,10 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
} }
} }
let (vin, vout) = self.transparent_builder.build(); let transparent_bundle = self.transparent_builder.build();
let mut ctx = prover.new_sapling_proving_context(); let mut ctx = prover.new_sapling_proving_context();
let (spend_descs, output_descs, tx_metadata) = self let (sapling_bundle, tx_metadata) = self
.sapling_builder .sapling_builder
.build( .build(
prover, prover,
@ -331,25 +338,18 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
.map_err(Error::SaplingBuild)?; .map_err(Error::SaplingBuild)?;
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
let (tze_inputs, tze_outputs) = self.tze_builder.build(); let tze_bundle = self.tze_builder.build();
let mut mtx = TransactionData { let unauthed_tx = TransactionData {
version, version,
vin,
vout,
#[cfg(feature = "zfuture")]
tze_inputs,
#[cfg(feature = "zfuture")]
tze_outputs,
lock_time: 0, lock_time: 0,
expiry_height: self.expiry_height, expiry_height: self.expiry_height,
value_balance: self.sapling_builder.value_balance(), transparent_bundle,
shielded_spends: spend_descs, sprout_bundle: None,
shielded_outputs: output_descs, sapling_bundle,
joinsplits: vec![], orchard_bundle: None,
joinsplit_pubkey: None, #[cfg(feature = "zfuture")]
joinsplit_sig: None, tze_bundle,
binding_sig: None,
}; };
// //
@ -358,68 +358,126 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
let mut sighash = [0u8; 32]; let mut sighash = [0u8; 32];
sighash.copy_from_slice(&signature_hash_data( sighash.copy_from_slice(&signature_hash_data(
&mtx, &unauthed_tx,
consensus_branch_id, consensus_branch_id,
SIGHASH_ALL, SIGHASH_ALL,
SignableInput::Shielded, SignableInput::Shielded,
)); ));
let (sapling_spend_auth_sigs, sapling_binding_sig) = self #[cfg(feature = "transparent-inputs")]
let transparent_sigs = self
.transparent_builder
.create_signatures(&unauthed_tx, consensus_branch_id);
let sapling_sigs = self
.sapling_builder .sapling_builder
.create_signatures(prover, &mut ctx, &mut self.rng, &sighash, &tx_metadata) .create_signatures(prover, &mut ctx, &mut self.rng, &sighash, &tx_metadata)
.map_err(Error::SaplingBuild)?; .map_err(Error::SaplingBuild)?;
for (i, spend_auth_sig) in sapling_spend_auth_sigs.into_iter().enumerate() {
mtx.shielded_spends[i].spend_auth_sig = spend_auth_sig;
}
mtx.binding_sig = sapling_binding_sig;
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
{ let tze_witnesses = self
// Create TZE input witnesses .tze_builder
let tze_payloads = self .create_witnesses(&unauthed_tx)
.tze_builder .map_err(Error::TzeBuild)?;
.create_witnesses(&mtx)
.map_err(Error::TzeBuild)?;
for (i, payload) in tze_payloads.into_iter().enumerate() {
mtx.tze_inputs[i].witness.payload = payload;
}
}
#[cfg(feature = "transparent-inputs")]
{
let script_sigs = self
.transparent_builder
.create_signatures(&mtx, consensus_branch_id);
for (i, sig) in script_sigs.into_iter().enumerate() {
mtx.vin[i].script_sig = sig;
}
}
Ok(( Ok((
mtx.freeze().expect("Transaction should be complete"), Self::apply_signatures(
consensus_branch_id,
unauthed_tx,
#[cfg(feature = "transparent-inputs")]
transparent_sigs,
sapling_sigs,
None,
#[cfg(feature = "zfuture")]
tze_witnesses,
)
.expect("An IO error occurred applying signatures."),
tx_metadata, tx_metadata,
)) ))
} }
fn apply_signatures(
consensus_branch_id: consensus::BranchId,
unauthed_tx: TransactionData<Unauthorized>,
#[cfg(feature = "transparent-inputs")] transparent_sigs: Option<Vec<Script>>,
sapling_sigs: Option<(Vec<redjubjub::Signature>, redjubjub::Signature)>,
_orchard_auth: Option<(
Vec<<orchard::Authorized as orchard::Authorization>::SpendAuth>,
orchard::Authorized,
)>,
#[cfg(feature = "zfuture")] tze_witnesses: Option<Vec<AuthData>>,
) -> io::Result<Transaction> {
#[cfg(feature = "transparent-inputs")]
let transparent_bundle = match (unauthed_tx.transparent_bundle, transparent_sigs) {
(Some(bundle), Some(script_sigs)) => Some(bundle.apply_signatures(script_sigs)),
(None, None) => None,
(b, s) => {
panic!(
"Mismatch between transparent bundle ({}) and signatures ({}).",
b.is_some(),
s.is_some()
);
}
};
#[cfg(not(feature = "transparent-inputs"))]
let transparent_bundle = unauthed_tx
.transparent_bundle
.map(|b| b.apply_signatures(vec![]));
let signed_sapling_bundle = match (unauthed_tx.sapling_bundle, sapling_sigs) {
(Some(bundle), Some((spend_sigs, binding_sig))) => {
Some(bundle.apply_signatures(spend_sigs, binding_sig))
}
(None, None) => None,
_ => {
panic!("Mismatch between sapling bundle and signatures.");
}
};
#[cfg(feature = "zfuture")]
let signed_tze_bundle = match (unauthed_tx.tze_bundle, tze_witnesses) {
(Some(bundle), Some(witness_payloads)) => {
Some(bundle.apply_signatures(witness_payloads))
}
(None, None) => None,
_ => {
panic!("Mismatch between TZE bundle and witnesses.")
}
};
let authorized_tx = TransactionData {
version: unauthed_tx.version,
lock_time: unauthed_tx.lock_time,
expiry_height: unauthed_tx.expiry_height,
transparent_bundle,
sprout_bundle: unauthed_tx.sprout_bundle,
sapling_bundle: signed_sapling_bundle,
orchard_bundle: None,
#[cfg(feature = "zfuture")]
tze_bundle: signed_tze_bundle,
};
authorized_tx.freeze(consensus_branch_id)
}
} }
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a> impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a>
for Builder<'a, P, R> for Builder<'a, P, R>
{ {
type BuildCtx = TransactionData; type BuildCtx = TransactionData<Unauthorized>;
type BuildError = tzebuilder::Error; type BuildError = tze::builder::Error;
fn add_tze_input<WBuilder, W: ToPayload>( fn add_tze_input<WBuilder, W: ToPayload>(
&mut self, &mut self,
extension_id: u32, extension_id: u32,
mode: u32, mode: u32,
prevout: (TzeOutPoint, TzeOut), prevout: (tze::OutPoint, TzeOut),
witness_builder: WBuilder, witness_builder: WBuilder,
) -> Result<(), Self::BuildError> ) -> Result<(), Self::BuildError>
where where
WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result<W, tzebuilder::Error>), WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result<W, tze::builder::Error>),
{ {
self.tze_builder self.tze_builder
.add_input(extension_id, mode, prevout, witness_builder); .add_input(extension_id, mode, prevout, witness_builder);
@ -454,31 +512,6 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
Self::new_internal(params, height, rng) Self::new_internal(params, height, rng)
} }
/// Creates a new `Builder` targeted for inclusion in the block with the given height,
/// and randomness source, using default values for general transaction fields
/// and the `ZFUTURE_TX_VERSION` and `ZFUTURE_VERSION_GROUP_ID` version identifiers.
///
/// # 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).
///
/// The transaction will be constructed and serialized according to the
/// NetworkUpgrade::ZFuture rules. This is intended only for use in
/// integration testing of new features.
///
/// WARNING: DO NOT USE IN PRODUCTION
#[cfg(feature = "zfuture")]
pub fn test_only_new_with_rng_zfuture(
params: P,
height: BlockHeight,
rng: R,
) -> Builder<'a, P, R> {
Self::new_internal(params, height, rng)
}
pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error> { pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error> {
self.build(&MockTxProver) self.build(&MockTxProver)
} }
@ -496,7 +529,7 @@ mod tests {
sapling::{prover::mock::MockTxProver, Node, Rseed}, sapling::{prover::mock::MockTxProver, Node, Rseed},
transaction::components::{ transaction::components::{
amount::{Amount, DEFAULT_FEE}, amount::{Amount, DEFAULT_FEE},
sapling::builder::{self as sapling}, sapling::builder::{self as build_s},
transparent::builder::{self as transparent}, transparent::builder::{self as transparent},
}, },
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
@ -524,7 +557,7 @@ mod tests {
let mut builder = Builder::new(TEST_NETWORK, sapling_activation_height); let mut builder = Builder::new(TEST_NETWORK, sapling_activation_height);
assert_eq!( assert_eq!(
builder.add_sapling_output(Some(ovk), to, Amount::from_i64(-1).unwrap(), None), builder.add_sapling_output(Some(ovk), to, Amount::from_i64(-1).unwrap(), None),
Err(Error::SaplingBuild(sapling::Error::InvalidAmount)) Err(Error::SaplingBuild(build_s::Error::InvalidAmount))
); );
} }
@ -545,7 +578,7 @@ mod tests {
expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA, expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA,
fee: Amount::zero(), fee: Amount::zero(),
transparent_builder: TransparentBuilder::empty(), transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::empty(TEST_NETWORK, sapling_activation_height), sapling_builder: SaplingBuilder::new(TEST_NETWORK, sapling_activation_height),
change_address: None, change_address: None,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::empty(), tze_builder: TzeBuilder::empty(),
@ -561,7 +594,7 @@ mod tests {
let (tx, _) = builder.build(&MockTxProver).unwrap(); let (tx, _) = builder.build(&MockTxProver).unwrap();
// No binding signature, because only t input and outputs // No binding signature, because only t input and outputs
assert!(tx.binding_sig.is_none()); assert!(tx.sapling_bundle.is_none());
} }
#[test] #[test]
@ -598,7 +631,7 @@ mod tests {
// that a binding signature was attempted // that a binding signature was attempted
assert_eq!( assert_eq!(
builder.build(&MockTxProver), builder.build(&MockTxProver),
Err(Error::SaplingBuild(sapling::Error::BindingSig)) Err(Error::SaplingBuild(build_s::Error::BindingSig))
); );
} }
@ -748,7 +781,7 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
builder.build(&MockTxProver), builder.build(&MockTxProver),
Err(Error::SaplingBuild(sapling::Error::BindingSig)) Err(Error::SaplingBuild(build_s::Error::BindingSig))
) )
} }
} }

View File

@ -1,6 +1,7 @@
//! Structs representing the components within Zcash transactions. //! Structs representing the components within Zcash transactions.
pub mod amount; pub mod amount;
pub mod orchard;
pub mod sapling; pub mod sapling;
pub mod sprout; pub mod sprout;
pub mod transparent; pub mod transparent;
@ -13,7 +14,7 @@ pub use self::{
}; };
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
pub use self::tze::{TzeIn, TzeOut, TzeOutPoint}; pub use self::tze::{TzeIn, TzeOut};
// π_A + π_B + π_C // π_A + π_B + π_C
pub const GROTH_PROOF_SIZE: usize = 48 + 96 + 48; pub const GROTH_PROOF_SIZE: usize = 48 + 96 + 48;

View File

@ -0,0 +1,181 @@
/// Functions for parsing & serialization of Orchard transaction components.
use std::convert::TryFrom;
use std::io::{self, Read, Write};
use orchard::{
bundle::{Action, Authorization, Authorized, Flags},
note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext},
primitives::redpallas::{self, SigType, Signature, SpendAuth, VerificationKey},
value::ValueCommitment,
Anchor,
};
pub const FLAG_SPENDS_ENABLED: u8 = 0b0000_0001;
pub const FLAG_OUTPUTS_ENABLED: u8 = 0b0000_0010;
pub const FLAGS_EXPECTED_UNSET: u8 = !(FLAG_SPENDS_ENABLED | FLAG_OUTPUTS_ENABLED);
pub fn read_value_commitment<R: Read>(mut reader: R) -> io::Result<ValueCommitment> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
let cv = ValueCommitment::from_bytes(&bytes);
if cv.is_none().into() {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid Pallas point for value commitment".to_owned(),
))
} else {
Ok(cv.unwrap())
}
}
pub fn read_nullifier<R: Read>(mut reader: R) -> io::Result<Nullifier> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
let nullifier_ctopt = Nullifier::from_bytes(&bytes);
if nullifier_ctopt.is_none().into() {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid Pallas point for nullifier".to_owned(),
))
} else {
Ok(nullifier_ctopt.unwrap())
}
}
pub fn read_verification_key<R: Read>(mut reader: R) -> io::Result<VerificationKey<SpendAuth>> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
VerificationKey::try_from(bytes).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid verification key".to_owned(),
)
})
}
pub fn read_cmx<R: Read>(mut reader: R) -> io::Result<ExtractedNoteCommitment> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
let cmx = ExtractedNoteCommitment::from_bytes(&bytes);
Option::from(cmx).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid Pallas base for field cmx".to_owned(),
)
})
}
pub fn read_note_ciphertext<R: Read>(mut reader: R) -> io::Result<TransmittedNoteCiphertext> {
let mut tnc = TransmittedNoteCiphertext {
epk_bytes: [0u8; 32],
enc_ciphertext: [0u8; 580],
out_ciphertext: [0u8; 80],
};
reader.read_exact(&mut tnc.epk_bytes)?;
reader.read_exact(&mut tnc.enc_ciphertext)?;
reader.read_exact(&mut tnc.out_ciphertext)?;
Ok(tnc)
}
pub fn read_action_without_auth<R: Read>(mut reader: R) -> io::Result<Action<()>> {
let cv_net = read_value_commitment(&mut reader)?;
let nf_old = read_nullifier(&mut reader)?;
let rk = read_verification_key(&mut reader)?;
let cmx = read_cmx(&mut reader)?;
let encrypted_note = read_note_ciphertext(&mut reader)?;
Ok(Action::from_parts(
nf_old,
rk,
cmx,
encrypted_note,
cv_net,
(),
))
}
pub fn read_flags<R: Read>(mut reader: R) -> io::Result<Flags> {
let mut byte = [0u8; 1];
reader.read_exact(&mut byte)?;
if byte[0] & FLAGS_EXPECTED_UNSET == 0 {
Ok(Flags::from_parts(
byte[0] & FLAG_SPENDS_ENABLED != 0,
byte[0] & FLAG_OUTPUTS_ENABLED != 0,
))
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Unexpected bits set in Orchard flags value.",
))
}
}
pub fn read_anchor<R: Read>(mut reader: R) -> io::Result<Anchor> {
let mut anchor = Anchor([0u8; 32]);
reader.read_exact(&mut anchor.0)?;
Ok(anchor)
}
pub fn read_signature<R: Read, T: SigType>(mut reader: R) -> io::Result<Signature<T>> {
let mut bytes = [0u8; 64];
reader.read_exact(&mut bytes)?;
Ok(Signature::from(bytes))
}
pub fn write_value_commitment<W: Write>(mut writer: W, cv: &ValueCommitment) -> io::Result<()> {
writer.write_all(&cv.to_bytes())
}
pub fn write_nullifier<W: Write>(mut writer: W, nf: &Nullifier) -> io::Result<()> {
writer.write_all(&nf.to_bytes())
}
pub fn write_verification_key<W: Write>(
mut writer: W,
rk: &redpallas::VerificationKey<SpendAuth>,
) -> io::Result<()> {
writer.write_all(&<[u8; 32]>::from(rk))
}
pub fn write_cmx<W: Write>(mut writer: W, cmx: &ExtractedNoteCommitment) -> io::Result<()> {
writer.write_all(&cmx.to_bytes())
}
pub fn write_note_ciphertext<W: Write>(
mut writer: W,
nc: &TransmittedNoteCiphertext,
) -> io::Result<()> {
writer.write_all(&nc.epk_bytes)?;
writer.write_all(&nc.enc_ciphertext)?;
writer.write_all(&nc.out_ciphertext)
}
pub fn write_action_without_auth<W: Write>(
mut writer: W,
act: &Action<<Authorized as Authorization>::SpendAuth>,
) -> io::Result<()> {
write_value_commitment(&mut writer, &act.cv_net())?;
write_nullifier(&mut writer, &act.nullifier())?;
write_verification_key(&mut writer, &act.rk())?;
write_cmx(&mut writer, &act.cmx())?;
write_note_ciphertext(&mut writer, &act.encrypted_note())?;
Ok(())
}
pub fn write_flags<W: Write>(mut writer: W, flags: &Flags) -> io::Result<()> {
let mut byte = 0u8;
if flags.spends_enabled() {
byte |= FLAG_SPENDS_ENABLED;
}
if flags.outputs_enabled() {
byte |= FLAG_OUTPUTS_ENABLED;
}
writer.write_all(&[byte])
}
pub fn write_anchor<W: Write>(mut writer: W, anchor: &Anchor) -> io::Result<()> {
writer.write_all(&anchor.0)
}

View File

@ -1,36 +1,113 @@
use core::fmt::Debug;
use ff::PrimeField; use ff::PrimeField;
use group::GroupEncoding; use group::GroupEncoding;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use zcash_note_encryption::ShieldedOutput; use zcash_note_encryption::{ShieldedOutput, COMPACT_NOTE_SIZE};
use crate::{ use crate::{
consensus, consensus,
sapling::{ sapling::{
note_encryption::SaplingDomain, note_encryption::SaplingDomain,
redjubjub::{PublicKey, Signature}, redjubjub::{self, PublicKey, Signature},
Nullifier, Nullifier,
}, },
}; };
use zcash_note_encryption::COMPACT_NOTE_SIZE; use super::{amount::Amount, GROTH_PROOF_SIZE};
use super::GROTH_PROOF_SIZE; pub type GrothProofBytes = [u8; GROTH_PROOF_SIZE];
pub mod builder; pub mod builder;
pub trait Authorization: Debug {
type Proof: Clone + Debug;
type AuthSig: Clone + Debug;
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Unproven;
impl Authorization for Unproven {
type Proof = ();
type AuthSig = ();
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Unauthorized;
impl Authorization for Unauthorized {
type Proof = GrothProofBytes;
type AuthSig = ();
}
#[derive(Debug, Copy, Clone)]
pub struct Authorized {
pub binding_sig: redjubjub::Signature,
}
impl Authorization for Authorized {
type Proof = GrothProofBytes;
type AuthSig = redjubjub::Signature;
}
#[derive(Debug, Clone)]
pub struct Bundle<A: Authorization> {
pub shielded_spends: Vec<SpendDescription<A>>,
pub shielded_outputs: Vec<OutputDescription<A>>,
pub value_balance: Amount,
pub authorization: A,
}
impl Bundle<Unauthorized> {
pub fn apply_signatures(
self,
spend_auth_sigs: Vec<Signature>,
binding_sig: Signature,
) -> Bundle<Authorized> {
assert!(self.shielded_spends.len() == spend_auth_sigs.len());
Bundle {
shielded_spends: self
.shielded_spends
.iter()
.zip(spend_auth_sigs.iter())
.map(|(spend, sig)| spend.apply_signature(*sig))
.collect(),
shielded_outputs: self
.shielded_outputs
.into_iter()
.map(|o| o.into_authorized())
.collect(), //TODO, is there a zero-cost way to do this?
value_balance: self.value_balance,
authorization: Authorized { binding_sig },
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct SpendDescription { pub struct SpendDescription<A: Authorization> {
pub cv: jubjub::ExtendedPoint, pub cv: jubjub::ExtendedPoint,
pub anchor: bls12_381::Scalar, pub anchor: bls12_381::Scalar,
pub nullifier: Nullifier, pub nullifier: Nullifier,
pub rk: PublicKey, pub rk: PublicKey,
pub zkproof: [u8; GROTH_PROOF_SIZE], pub zkproof: A::Proof,
pub spend_auth_sig: Option<Signature>, pub spend_auth_sig: A::AuthSig,
} }
impl std::fmt::Debug for SpendDescription { impl SpendDescription<Unauthorized> {
pub fn apply_signature(&self, spend_auth_sig: Signature) -> SpendDescription<Authorized> {
SpendDescription {
cv: self.cv,
anchor: self.anchor,
nullifier: self.nullifier,
rk: self.rk.clone(),
zkproof: self.zkproof,
spend_auth_sig,
}
}
}
impl<A: Authorization> std::fmt::Debug for SpendDescription<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!( write!(
f, f,
@ -40,49 +117,84 @@ impl std::fmt::Debug for SpendDescription {
} }
} }
impl SpendDescription { /// Consensus rules (§4.4) & (§4.5):
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> { /// - Canonical encoding is enforced here.
// Consensus rules (§4.4): /// - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output())
// - Canonical encoding is enforced here. /// (located in zcash_proofs::sapling::verifier).
// - "Not small order" is enforced in SaplingVerificationContext::check_spend() pub fn read_point<R: Read>(mut reader: R, field: &str) -> io::Result<jubjub::ExtendedPoint> {
// (located in zcash_proofs::sapling::verifier). let mut bytes = [0u8; 32];
let cv = { reader.read_exact(&mut bytes)?;
let mut bytes = [0u8; 32]; let point = jubjub::ExtendedPoint::from_bytes(&bytes);
reader.read_exact(&mut bytes)?;
let cv = jubjub::ExtendedPoint::from_bytes(&bytes);
if cv.is_none().into() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid cv"));
}
cv.unwrap()
};
// Consensus rule (§7.3): Canonical encoding is enforced here if point.is_none().into() {
let anchor = { Err(io::Error::new(
let mut f = [0u8; 32]; io::ErrorKind::InvalidInput,
reader.read_exact(&mut f)?; format!("invalid {}", field),
bls12_381::Scalar::from_repr(f) ))
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "anchor not in field"))? } else {
}; Ok(point.unwrap())
}
}
/// Consensus rules (§7.3) & (§7.4):
/// - Canonical encoding is enforced here
pub fn read_base<R: Read>(mut reader: R, field: &str) -> io::Result<bls12_381::Scalar> {
let mut f = [0u8; 32];
reader.read_exact(&mut f)?;
bls12_381::Scalar::from_repr(f).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{} not in field", field),
)
})
}
/// Consensus rules (§4.4) & (§4.5):
/// - Canonical encoding is enforced by the API of SaplingVerificationContext::check_spend()
/// and SaplingVerificationContext::check_output() due to the need to parse this into a
/// bellman::groth16::Proof.
/// - Proof validity is enforced in SaplingVerificationContext::check_spend()
/// and SaplingVerificationContext::check_output()
pub fn read_zkproof<R: Read>(mut reader: R) -> io::Result<GrothProofBytes> {
let mut zkproof = [0u8; GROTH_PROOF_SIZE];
reader.read_exact(&mut zkproof)?;
Ok(zkproof)
}
impl SpendDescription<Authorized> {
pub fn read_nullifier<R: Read>(mut reader: R) -> io::Result<Nullifier> {
let mut nullifier = Nullifier([0u8; 32]); let mut nullifier = Nullifier([0u8; 32]);
reader.read_exact(&mut nullifier.0)?; reader.read_exact(&mut nullifier.0)?;
Ok(nullifier)
}
// Consensus rules (§4.4): /// Consensus rules (§4.4):
/// - Canonical encoding is enforced here.
/// - "Not small order" is enforced in SaplingVerificationContext::check_spend()
pub fn read_rk<R: Read>(mut reader: R) -> io::Result<PublicKey> {
PublicKey::read(&mut reader)
}
/// Consensus rules (§4.4):
/// - Canonical encoding is enforced here.
/// - Signature validity is enforced in SaplingVerificationContext::check_spend()
pub fn read_spend_auth_sig<R: Read>(mut reader: R) -> io::Result<Signature> {
Signature::read(&mut reader)
}
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
// Consensus rules (§4.4) & (§4.5):
// - Canonical encoding is enforced here. // - Canonical encoding is enforced here.
// - "Not small order" is enforced in SaplingVerificationContext::check_spend() // - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output())
let rk = PublicKey::read(&mut reader)?; // (located in zcash_proofs::sapling::verifier).
let cv = read_point(&mut reader, "cv")?;
// Consensus rules (§4.4): // Consensus rules (§7.3) & (§7.4):
// - Canonical encoding is enforced by the API of SaplingVerificationContext::check_spend() // - Canonical encoding is enforced here
// due to the need to parse this into a bellman::groth16::Proof. let anchor = read_base(&mut reader, "anchor")?;
// - Proof validity is enforced in SaplingVerificationContext::check_spend() let nullifier = Self::read_nullifier(&mut reader)?;
let mut zkproof = [0u8; GROTH_PROOF_SIZE]; let rk = Self::read_rk(&mut reader)?;
reader.read_exact(&mut zkproof)?; let zkproof = read_zkproof(&mut reader)?;
let spend_auth_sig = Self::read_spend_auth_sig(&mut reader)?;
// Consensus rules (§4.4):
// - Canonical encoding is enforced here.
// - Signature validity is enforced in SaplingVerificationContext::check_spend()
let spend_auth_sig = Some(Signature::read(&mut reader)?);
Ok(SpendDescription { Ok(SpendDescription {
cv, cv,
@ -100,27 +212,23 @@ impl SpendDescription {
writer.write_all(&self.nullifier.0)?; writer.write_all(&self.nullifier.0)?;
self.rk.write(&mut writer)?; self.rk.write(&mut writer)?;
writer.write_all(&self.zkproof)?; writer.write_all(&self.zkproof)?;
match self.spend_auth_sig { self.spend_auth_sig.write(&mut writer)
Some(sig) => sig.write(&mut writer),
None => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Missing spend auth signature",
)),
}
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct OutputDescription { pub struct OutputDescription<A: Authorization> {
pub cv: jubjub::ExtendedPoint, pub cv: jubjub::ExtendedPoint,
pub cmu: bls12_381::Scalar, pub cmu: bls12_381::Scalar,
pub ephemeral_key: jubjub::ExtendedPoint, pub ephemeral_key: jubjub::ExtendedPoint,
pub enc_ciphertext: [u8; 580], pub enc_ciphertext: [u8; 580],
pub out_ciphertext: [u8; 80], pub out_ciphertext: [u8; 80],
pub zkproof: [u8; GROTH_PROOF_SIZE], pub zkproof: A::Proof,
} }
impl<P: consensus::Parameters> ShieldedOutput<SaplingDomain<P>> for OutputDescription { impl<P: consensus::Parameters, A: Authorization> ShieldedOutput<SaplingDomain<P>>
for OutputDescription<A>
{
fn epk(&self) -> &jubjub::ExtendedPoint { fn epk(&self) -> &jubjub::ExtendedPoint {
&self.ephemeral_key &self.ephemeral_key
} }
@ -134,7 +242,7 @@ impl<P: consensus::Parameters> ShieldedOutput<SaplingDomain<P>> for OutputDescri
} }
} }
impl std::fmt::Debug for OutputDescription { impl<A: Authorization> std::fmt::Debug for OutputDescription<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!( write!(
f, f,
@ -144,7 +252,7 @@ impl std::fmt::Debug for OutputDescription {
} }
} }
impl OutputDescription { impl OutputDescription<Authorized> {
pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> { pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
// Consensus rules (§4.5): // Consensus rules (§4.5):
// - Canonical encoding is enforced here. // - Canonical encoding is enforced here.
@ -205,7 +313,9 @@ impl OutputDescription {
zkproof, zkproof,
}) })
} }
}
impl<A: Authorization<Proof = GrothProofBytes>> OutputDescription<A> {
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> { pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(&self.cv.to_bytes())?; writer.write_all(&self.cv.to_bytes())?;
writer.write_all(self.cmu.to_repr().as_ref())?; writer.write_all(self.cmu.to_repr().as_ref())?;
@ -216,14 +326,27 @@ impl OutputDescription {
} }
} }
impl OutputDescription<Unauthorized> {
pub fn into_authorized(self) -> OutputDescription<Authorized> {
OutputDescription {
cv: self.cv,
cmu: self.cmu,
ephemeral_key: self.ephemeral_key,
enc_ciphertext: self.enc_ciphertext,
out_ciphertext: self.out_ciphertext,
zkproof: self.zkproof,
}
}
}
pub struct CompactOutputDescription { pub struct CompactOutputDescription {
pub epk: jubjub::ExtendedPoint, pub epk: jubjub::ExtendedPoint,
pub cmu: bls12_381::Scalar, pub cmu: bls12_381::Scalar,
pub enc_ciphertext: Vec<u8>, pub enc_ciphertext: Vec<u8>,
} }
impl From<OutputDescription> for CompactOutputDescription { impl<A: Authorization> From<OutputDescription<A>> for CompactOutputDescription {
fn from(out: OutputDescription) -> CompactOutputDescription { fn from(out: OutputDescription<A>) -> CompactOutputDescription {
CompactOutputDescription { CompactOutputDescription {
epk: out.ephemeral_key, epk: out.ephemeral_key,
cmu: out.cmu, cmu: out.cmu,

View File

@ -1,11 +1,10 @@
//! Types and functions for building Sapling transaction components. //! Types and functions for building Sapling transaction components.
use std::fmt; use std::fmt;
use std::marker::PhantomData;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use ff::Field; use ff::Field;
use rand::{seq::SliceRandom, CryptoRng, RngCore}; use rand::{seq::SliceRandom, RngCore};
use crate::{ use crate::{
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
@ -22,7 +21,10 @@ use crate::{
}, },
transaction::{ transaction::{
builder::Progress, builder::Progress,
components::{amount::Amount, OutputDescription, SpendDescription}, components::{
amount::Amount,
sapling::{Bundle, OutputDescription, SpendDescription, Unauthorized},
},
}, },
zip32::ExtendedSpendingKey, zip32::ExtendedSpendingKey,
}; };
@ -63,32 +65,19 @@ struct SpendDescriptionInfo {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct SaplingOutput<P: consensus::Parameters> { struct SaplingOutput {
/// `None` represents the `ovk = ⊥` case. /// `None` represents the `ovk = ⊥` case.
ovk: Option<OutgoingViewingKey>, ovk: Option<OutgoingViewingKey>,
to: PaymentAddress, to: PaymentAddress,
note: Note, note: Note,
memo: MemoBytes, memo: MemoBytes,
_params: PhantomData<P>,
} }
impl<P: consensus::Parameters> SaplingOutput<P> { impl SaplingOutput {
pub fn new<R: RngCore + CryptoRng>( fn new_internal<P: consensus::Parameters, R: RngCore>(
params: &P, params: &P,
target_height: BlockHeight,
rng: &mut R, rng: &mut R,
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
value: Amount,
memo: Option<MemoBytes>,
) -> Result<Self, Error> {
Self::new_internal(params, target_height, rng, ovk, to, value, memo)
}
fn new_internal<R: RngCore>(
params: &P,
target_height: BlockHeight, target_height: BlockHeight,
rng: &mut R,
ovk: Option<OutgoingViewingKey>, ovk: Option<OutgoingViewingKey>,
to: PaymentAddress, to: PaymentAddress,
value: Amount, value: Amount,
@ -113,25 +102,15 @@ impl<P: consensus::Parameters> SaplingOutput<P> {
to, to,
note, note,
memo: memo.unwrap_or_else(MemoBytes::empty), memo: memo.unwrap_or_else(MemoBytes::empty),
_params: PhantomData::default(),
}) })
} }
pub fn build<Pr: TxProver, R: RngCore + CryptoRng>( fn build<P: consensus::Parameters, Pr: TxProver, R: RngCore>(
self, self,
prover: &Pr, prover: &Pr,
ctx: &mut Pr::SaplingProvingContext, ctx: &mut Pr::SaplingProvingContext,
rng: &mut R, rng: &mut R,
) -> OutputDescription { ) -> OutputDescription<Unauthorized> {
self.build_internal(prover, ctx, rng)
}
fn build_internal<Pr: TxProver, R: RngCore>(
self,
prover: &Pr,
ctx: &mut Pr::SaplingProvingContext,
rng: &mut R,
) -> OutputDescription {
let encryptor = sapling_note_encryption::<R, P>( let encryptor = sapling_note_encryption::<R, P>(
self.ovk, self.ovk,
self.note.clone(), self.note.clone(),
@ -204,17 +183,17 @@ impl SaplingMetadata {
} }
} }
pub struct SaplingBuilder<P: consensus::Parameters> { pub struct SaplingBuilder<P> {
params: P, params: P,
anchor: Option<bls12_381::Scalar>, anchor: Option<bls12_381::Scalar>,
target_height: BlockHeight, target_height: BlockHeight,
value_balance: Amount, value_balance: Amount,
spends: Vec<SpendDescriptionInfo>, spends: Vec<SpendDescriptionInfo>,
outputs: Vec<SaplingOutput<P>>, outputs: Vec<SaplingOutput>,
} }
impl<P: consensus::Parameters> SaplingBuilder<P> { impl<P: consensus::Parameters> SaplingBuilder<P> {
pub fn empty(params: P, target_height: BlockHeight) -> Self { pub fn new(params: P, target_height: BlockHeight) -> Self {
SaplingBuilder { SaplingBuilder {
params, params,
anchor: None, anchor: None,
@ -280,8 +259,8 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
) -> Result<(), Error> { ) -> Result<(), Error> {
let output = SaplingOutput::new_internal( let output = SaplingOutput::new_internal(
&self.params, &self.params,
self.target_height,
&mut rng, &mut rng,
self.target_height,
ovk, ovk,
to, to,
value, value,
@ -311,14 +290,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
mut rng: R, mut rng: R,
target_height: BlockHeight, target_height: BlockHeight,
progress_notifier: Option<&Sender<Progress>>, progress_notifier: Option<&Sender<Progress>>,
) -> Result< ) -> Result<(Option<Bundle<Unauthorized>>, SaplingMetadata), Error> {
(
Vec<SpendDescription>,
Vec<OutputDescription>,
SaplingMetadata,
),
Error,
> {
// Record initial positions of spends and outputs // Record initial positions of spends and outputs
let mut indexed_spends: Vec<_> = self.spends.iter().enumerate().collect(); let mut indexed_spends: Vec<_> = self.spends.iter().enumerate().collect();
let mut indexed_outputs: Vec<_> = self let mut indexed_outputs: Vec<_> = self
@ -350,7 +322,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
let mut progress = 0u32; let mut progress = 0u32;
// Create Sapling SpendDescriptions // Create Sapling SpendDescriptions
let spend_descs = if !indexed_spends.is_empty() { let shielded_spends: Vec<SpendDescription<Unauthorized>> = if !indexed_spends.is_empty() {
let anchor = self let anchor = self
.anchor .anchor
.expect("Sapling anchor must be set if Sapling spends are present."); .expect("Sapling anchor must be set if Sapling spends are present.");
@ -397,7 +369,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
nullifier, nullifier,
rk, rk,
zkproof, zkproof,
spend_auth_sig: None, spend_auth_sig: (),
}) })
}) })
.collect::<Result<Vec<_>, Error>>()? .collect::<Result<Vec<_>, Error>>()?
@ -406,7 +378,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
}; };
// Create Sapling OutputDescriptions // Create Sapling OutputDescriptions
let output_descs = indexed_outputs let shielded_outputs: Vec<OutputDescription<Unauthorized>> = indexed_outputs
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, output)| { .map(|(i, output)| {
@ -414,7 +386,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
// Record the post-randomized output location // Record the post-randomized output location
tx_metadata.output_indices[pos] = i; tx_metadata.output_indices[pos] = i;
output.clone().build_internal(prover, ctx, &mut rng) output.clone().build::<P, _, _>(prover, ctx, &mut rng)
} else { } else {
// This is a dummy output // This is a dummy output
let (dummy_to, dummy_note) = { let (dummy_to, dummy_note) = {
@ -491,7 +463,18 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
}) })
.collect(); .collect();
Ok((spend_descs, output_descs, tx_metadata)) let bundle = if shielded_spends.is_empty() && shielded_outputs.is_empty() {
None
} else {
Some(Bundle {
shielded_spends,
shielded_outputs,
value_balance: self.value_balance,
authorization: Unauthorized,
})
};
Ok((bundle, tx_metadata))
} }
pub fn create_signatures<Pr: TxProver, R: RngCore>( pub fn create_signatures<Pr: TxProver, R: RngCore>(
@ -501,8 +484,9 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
rng: &mut R, rng: &mut R,
sighash_bytes: &[u8; 32], sighash_bytes: &[u8; 32],
tx_metadata: &SaplingMetadata, tx_metadata: &SaplingMetadata,
) -> Result<(Vec<Option<Signature>>, Option<Signature>), Error> { ) -> Result<Option<(Vec<Signature>, Signature)>, Error> {
// Create Sapling spendAuth and binding signatures // Create Sapling spendAuth signatures. These must be properly ordered with respect to the
// shuffle that is described by tx_metadata.
let mut spend_sigs = vec![None; self.spends.len()]; let mut spend_sigs = vec![None; self.spends.len()];
for (i, spend) in self.spends.into_iter().enumerate() { for (i, spend) in self.spends.into_iter().enumerate() {
spend_sigs[tx_metadata.spend_indices[i]] = Some(spend_sig_internal( spend_sigs[tx_metadata.spend_indices[i]] = Some(spend_sig_internal(
@ -513,18 +497,100 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
)); ));
} }
// Add a binding signature if needed if tx_metadata.spend_indices.is_empty() && tx_metadata.output_indices.is_empty() {
let binding_sig = Ok(None)
if tx_metadata.spend_indices.is_empty() && tx_metadata.output_indices.is_empty() { } else {
None let spend_sigs = spend_sigs
} else { .into_iter()
Some( .collect::<Option<Vec<Signature>>>()
prover .unwrap_or_default();
.binding_sig(ctx, self.value_balance, &sighash_bytes)
.map_err(|_| Error::BindingSig)?,
)
};
Ok((spend_sigs, binding_sig)) let binding_sig = prover
.binding_sig(ctx, self.value_balance, sighash_bytes)
.map_err(|_| Error::BindingSig)?;
Ok(Some((spend_sigs, binding_sig)))
}
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::collection::vec;
use proptest::prelude::*;
use rand::{rngs::StdRng, SeedableRng};
use crate::{
consensus::{
testing::{arb_branch_id, arb_height},
TEST_NETWORK,
},
merkle_tree::{testing::arb_commitment_tree, IncrementalWitness},
sapling::{
prover::{mock::MockTxProver, TxProver},
testing::{arb_node, arb_note, arb_positive_note_value},
Diversifier,
},
transaction::components::{
amount::MAX_MONEY,
sapling::{Authorized, Bundle},
},
zip32::testing::arb_extended_spending_key,
};
use super::SaplingBuilder;
prop_compose! {
fn arb_bundle()(n_notes in 1..30usize)(
extsk in arb_extended_spending_key(),
spendable_notes in vec(
arb_positive_note_value(MAX_MONEY as u64 / 10000).prop_flat_map(arb_note),
n_notes
),
commitment_trees in vec(
arb_commitment_tree(n_notes, arb_node()).prop_map(
|t| IncrementalWitness::from_tree(&t).path().unwrap()
),
n_notes
),
diversifiers in vec(prop::array::uniform11(any::<u8>()).prop_map(Diversifier), n_notes),
target_height in arb_branch_id().prop_flat_map(|b| arb_height(b, &TEST_NETWORK)),
rng_seed in prop::array::uniform32(any::<u8>()),
fake_sighash_bytes in prop::array::uniform32(any::<u8>()),
) -> Bundle<Authorized> {
let mut builder = SaplingBuilder::new(TEST_NETWORK, target_height.unwrap());
let mut rng = StdRng::from_seed(rng_seed);
for ((note, path), diversifier) in spendable_notes.into_iter().zip(commitment_trees.into_iter()).zip(diversifiers.into_iter()) {
builder.add_spend(
&mut rng,
extsk.clone(),
diversifier,
note,
path
).unwrap();
}
let prover = MockTxProver;
let mut ctx = prover.new_sapling_proving_context();
let (bundle, meta) = builder.build(
&prover,
&mut ctx,
&mut rng,
target_height.unwrap(),
None
).unwrap();
let (spend_auth_sigs, binding_sig) = builder.create_signatures(
&prover,
&mut ctx,
&mut rng,
&fake_sighash_bytes,
&meta
).unwrap().unwrap();
bundle.unwrap().apply_signatures(spend_auth_sigs, binding_sig)
}
} }
} }

View File

@ -10,6 +10,13 @@ const PHGR_PROOF_SIZE: usize = 33 + 33 + 65 + 33 + 33 + 33 + 33 + 33;
const ZC_NUM_JS_INPUTS: usize = 2; const ZC_NUM_JS_INPUTS: usize = 2;
const ZC_NUM_JS_OUTPUTS: usize = 2; const ZC_NUM_JS_OUTPUTS: usize = 2;
#[derive(Debug)]
pub struct Bundle {
pub joinsplits: Vec<JsDescription>,
pub joinsplit_pubkey: [u8; 32],
pub joinsplit_sig: [u8; 64],
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) enum SproutProof { pub(crate) enum SproutProof {
Groth([u8; GROTH_PROOF_SIZE]), Groth([u8; GROTH_PROOF_SIZE]),

View File

@ -2,6 +2,7 @@
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::fmt::Debug;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use crate::legacy::Script; use crate::legacy::Script;
@ -10,6 +11,49 @@ use super::amount::Amount;
pub mod builder; pub mod builder;
pub trait Authorization: Debug {
type ScriptSig: Debug + Clone + PartialEq;
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Unauthorized;
impl Authorization for Unauthorized {
type ScriptSig = ();
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Authorized;
impl Authorization for Authorized {
type ScriptSig = Script;
}
#[derive(Debug, Clone, PartialEq)]
pub struct Bundle<A: Authorization> {
pub vin: Vec<TxIn<A>>,
pub vout: Vec<TxOut>,
}
impl Bundle<Unauthorized> {
pub fn apply_signatures(self, script_sigs: Vec<Script>) -> Bundle<Authorized> {
assert!(self.vin.len() == script_sigs.len());
Bundle {
vin: self
.vin
.into_iter()
.zip(script_sigs.into_iter())
.map(|(txin, sig)| TxIn {
prevout: txin.prevout,
script_sig: sig,
sequence: txin.sequence,
})
.collect(),
vout: self.vout,
}
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct OutPoint { pub struct OutPoint {
hash: [u8; 32], hash: [u8; 32],
@ -43,23 +87,25 @@ impl OutPoint {
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct TxIn { pub struct TxIn<A: Authorization> {
pub prevout: OutPoint, pub prevout: OutPoint,
pub script_sig: Script, pub script_sig: A::ScriptSig,
pub sequence: u32, pub sequence: u32,
} }
impl TxIn { impl TxIn<Unauthorized> {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
#[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))] #[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))]
pub fn new(prevout: OutPoint) -> Self { pub fn new(prevout: OutPoint) -> Self {
TxIn { TxIn {
prevout, prevout,
script_sig: Script::default(), script_sig: (),
sequence: std::u32::MAX, sequence: std::u32::MAX,
} }
} }
}
impl TxIn<Authorized> {
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> { pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
let prevout = OutPoint::read(&mut reader)?; let prevout = OutPoint::read(&mut reader)?;
let script_sig = Script::read(&mut reader)?; let script_sig = Script::read(&mut reader)?;
@ -106,3 +152,66 @@ impl TxOut {
self.script_pubkey.write(&mut writer) self.script_pubkey.write(&mut writer)
} }
} }
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::collection::vec;
use proptest::prelude::*;
use proptest::sample::select;
use crate::{legacy::Script, transaction::components::amount::testing::arb_nonnegative_amount};
use super::{Authorized, Bundle, OutPoint, TxIn, TxOut};
pub const VALID_OPCODES: [u8; 8] = [
0x00, // OP_FALSE,
0x51, // OP_1,
0x52, // OP_2,
0x53, // OP_3,
0xac, // OP_CHECKSIG,
0x63, // OP_IF,
0x65, // OP_VERIF,
0x6a, // OP_RETURN,
];
prop_compose! {
pub fn arb_outpoint()(hash in prop::array::uniform32(0u8..), n in 0..100u32) -> OutPoint {
OutPoint::new(hash, n)
}
}
prop_compose! {
pub fn arb_script()(v in vec(select(&VALID_OPCODES[..]), 1..256)) -> Script {
Script(v)
}
}
prop_compose! {
pub fn arb_txin()(
prevout in arb_outpoint(),
script_sig in arb_script(),
sequence in any::<u32>()
) -> TxIn<Authorized> {
TxIn { prevout, script_sig, sequence }
}
}
prop_compose! {
pub fn arb_txout()(value in arb_nonnegative_amount(), script_pubkey in arb_script()) -> TxOut {
TxOut { value, script_pubkey }
}
}
prop_compose! {
pub fn arb_bundle()(
vin in vec(arb_txin(), 0..10),
vout in vec(arb_txout(), 0..10),
) -> Option<Bundle<Authorized>> {
if vin.is_empty() && vout.is_empty() {
None
} else {
Some(Bundle { vin, vout })
}
}
}
}

View File

@ -4,7 +4,10 @@ use std::fmt;
use crate::{ use crate::{
legacy::TransparentAddress, legacy::TransparentAddress,
transaction::components::{amount::Amount, TxIn, TxOut}, transaction::components::{
amount::Amount,
transparent::{self, TxIn, TxOut},
},
}; };
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -13,7 +16,7 @@ use crate::{
legacy::Script, legacy::Script,
transaction::{ transaction::{
components::OutPoint, sighash::signature_hash_data, SignableInput, TransactionData, components::OutPoint, sighash::signature_hash_data, SignableInput, TransactionData,
SIGHASH_ALL, Unauthorized, SIGHASH_ALL,
}, },
}; };
@ -128,48 +131,66 @@ impl TransparentBuilder {
.sum::<Option<Amount>>()? .sum::<Option<Amount>>()?
} }
pub fn build(&self) -> (Vec<TxIn>, Vec<TxOut>) { pub fn build(&self) -> Option<transparent::Bundle<transparent::Unauthorized>> {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let vin = self let vin: Vec<TxIn<transparent::Unauthorized>> = self
.inputs .inputs
.iter() .iter()
.map(|i| TxIn::new(i.utxo.clone())) .map(|i| TxIn::new(i.utxo.clone()))
.collect(); .collect();
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
let vin = vec![]; let vin: Vec<TxIn<transparent::Unauthorized>> = vec![];
(vin, self.vout.clone()) if vin.is_empty() && self.vout.is_empty() {
None
} else {
Some(transparent::Bundle {
vin,
vout: self.vout.clone(),
})
}
} }
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
pub fn create_signatures( pub fn create_signatures(
self, self,
mtx: &TransactionData, mtx: &TransactionData<Unauthorized>,
consensus_branch_id: consensus::BranchId, consensus_branch_id: consensus::BranchId,
) -> Vec<Script> { ) -> Option<Vec<Script>> {
self.inputs if self.inputs.is_empty() && self.vout.is_empty() {
.iter() None
.enumerate() } else {
.map(|(i, info)| { Some(
let mut sighash = [0u8; 32]; self.inputs
sighash.copy_from_slice(&signature_hash_data( .iter()
mtx, .enumerate()
consensus_branch_id, .map(|(i, info)| {
SIGHASH_ALL, let mut sighash = [0u8; 32];
SignableInput::transparent(i, &info.coin.script_pubkey, info.coin.value), sighash.copy_from_slice(&signature_hash_data(
)); mtx,
consensus_branch_id,
SIGHASH_ALL,
SignableInput::transparent(
i,
&info.coin.script_pubkey,
info.coin.value,
),
));
let msg = secp256k1::Message::from_slice(sighash.as_ref()).expect("32 bytes"); let msg =
let sig = self.secp.sign(&msg, &info.sk); secp256k1::Message::from_slice(sighash.as_ref()).expect("32 bytes");
let sig = self.secp.sign(&msg, &info.sk);
// Signature has to have "SIGHASH_ALL" appended to it // Signature has to have "SIGHASH_ALL" appended to it
let mut sig_bytes: Vec<u8> = sig.serialize_der()[..].to_vec(); let mut sig_bytes: Vec<u8> = sig.serialize_der()[..].to_vec();
sig_bytes.extend(&[SIGHASH_ALL as u8]); sig_bytes.extend(&[SIGHASH_ALL as u8]);
// P2PKH scriptSig // P2PKH scriptSig
Script::default() << &sig_bytes[..] << &info.pubkey[..] Script::default() << &sig_bytes[..] << &info.pubkey[..]
}) })
.collect() .collect(),
)
}
} }
} }

View File

@ -3,9 +3,9 @@
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::io::{self, Read, Write};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt::Debug;
use std::io::{self, Read, Write};
use crate::{ use crate::{
extensions::transparent as tze, extensions::transparent as tze,
@ -21,21 +21,67 @@ fn to_io_error(_: std::num::TryFromIntError) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, "value out of range") io::Error::new(io::ErrorKind::InvalidData, "value out of range")
} }
pub trait Authorization: Debug {
type Witness: Debug + Clone + PartialEq;
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Unauthorized;
impl Authorization for Unauthorized {
type Witness = ();
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Authorized;
impl Authorization for Authorized {
type Witness = tze::AuthData;
}
#[derive(Debug, Clone, PartialEq)]
pub struct Bundle<A: Authorization> {
pub vin: Vec<TzeIn<A>>,
pub vout: Vec<TzeOut>,
}
impl Bundle<Unauthorized> {
pub fn apply_signatures(self, witnesses: Vec<tze::AuthData>) -> Bundle<Authorized> {
assert!(self.vin.len() == witnesses.len());
Bundle {
vin: self
.vin
.into_iter()
.zip(witnesses.into_iter())
.map(|(tzein, payload)| TzeIn {
prevout: tzein.prevout,
witness: tze::Witness {
extension_id: tzein.witness.extension_id,
mode: tzein.witness.mode,
payload,
},
})
.collect(),
vout: self.vout,
}
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct TzeOutPoint { pub struct OutPoint {
txid: TxId, txid: TxId,
n: u32, n: u32,
} }
impl TzeOutPoint { impl OutPoint {
pub fn new(txid: TxId, n: u32) -> Self { pub fn new(txid: TxId, n: u32) -> Self {
TzeOutPoint { txid, n } OutPoint { txid, n }
} }
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> { pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let txid = TxId::read(&mut reader)?; let txid = TxId::read(&mut reader)?;
let n = reader.read_u32::<LittleEndian>()?; let n = reader.read_u32::<LittleEndian>()?;
Ok(TzeOutPoint { txid, n }) Ok(OutPoint { txid, n })
} }
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> { pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
@ -53,48 +99,12 @@ impl TzeOutPoint {
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct TzeIn { pub struct TzeIn<A: Authorization> {
pub prevout: TzeOutPoint, pub prevout: OutPoint,
pub witness: tze::Witness, pub witness: tze::Witness<A::Witness>,
} }
/// Transaction encoding and decoding functions conforming to [ZIP 222]. impl<A: Authorization> TzeIn<A> {
///
/// [ZIP 222]: https://zips.z.cash/zip-0222#encoding-in-transactions
impl TzeIn {
/// Convenience constructor
pub fn new(prevout: TzeOutPoint, extension_id: u32, mode: u32) -> Self {
TzeIn {
prevout,
witness: tze::Witness {
extension_id,
mode,
payload: vec![],
},
}
}
/// Read witness metadata & payload
///
/// Used to decode the encoded form used within a serialized
/// transaction.
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
let prevout = TzeOutPoint::read(&mut reader)?;
let extension_id = CompactSize::read(&mut reader)?;
let mode = CompactSize::read(&mut reader)?;
let payload = Vector::read(&mut reader, |r| r.read_u8())?;
Ok(TzeIn {
prevout,
witness: tze::Witness {
extension_id: u32::try_from(extension_id).map_err(to_io_error)?,
mode: u32::try_from(mode).map_err(to_io_error)?,
payload,
},
})
}
/// Write without witness data (for signature hashing) /// Write without witness data (for signature hashing)
/// ///
/// This is also used as the prefix for the encoded form used /// This is also used as the prefix for the encoded form used
@ -112,6 +122,46 @@ impl TzeIn {
usize::try_from(self.witness.mode).map_err(to_io_error)?, usize::try_from(self.witness.mode).map_err(to_io_error)?,
) )
} }
}
/// Transaction encoding and decoding functions conforming to [ZIP 222].
///
/// [ZIP 222]: https://zips.z.cash/zip-0222#encoding-in-transactions
impl TzeIn<Unauthorized> {
/// Convenience constructor
pub fn new(prevout: OutPoint, extension_id: u32, mode: u32) -> Self {
TzeIn {
prevout,
witness: tze::Witness {
extension_id,
mode,
payload: (),
},
}
}
}
impl TzeIn<Authorized> {
/// Read witness metadata & payload
///
/// Used to decode the encoded form used within a serialized
/// transaction.
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
let prevout = OutPoint::read(&mut reader)?;
let extension_id = CompactSize::read(&mut reader)?;
let mode = CompactSize::read(&mut reader)?;
let payload = Vector::read(&mut reader, |r| r.read_u8())?;
Ok(TzeIn {
prevout,
witness: tze::Witness {
extension_id: u32::try_from(extension_id).map_err(to_io_error)?,
mode: u32::try_from(mode).map_err(to_io_error)?,
payload: tze::AuthData(payload),
},
})
}
/// Write prevout, extension, and mode followed by witness data. /// Write prevout, extension, and mode followed by witness data.
/// ///
@ -122,7 +172,7 @@ impl TzeIn {
/// [`write_without_witness`]: TzeIn::write_without_witness /// [`write_without_witness`]: TzeIn::write_without_witness
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> { pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
self.write_without_witness(&mut writer)?; self.write_without_witness(&mut writer)?;
Vector::write(&mut writer, &self.witness.payload, |w, b| w.write_u8(*b)) Vector::write(&mut writer, &self.witness.payload.0, |w, b| w.write_u8(*b))
} }
} }
@ -171,3 +221,61 @@ impl TzeOut {
}) })
} }
} }
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::collection::vec;
use proptest::prelude::*;
use crate::{
consensus::BranchId,
extensions::transparent::{AuthData, Precondition, Witness},
transaction::components::amount::testing::arb_nonnegative_amount,
transaction::testing::arb_txid,
};
use super::{Authorized, Bundle, OutPoint, TzeIn, TzeOut};
prop_compose! {
pub fn arb_outpoint()(txid in arb_txid(), n in 0..100u32) -> OutPoint {
OutPoint::new(txid, n)
}
}
prop_compose! {
pub fn arb_witness()(extension_id in 0..100u32, mode in 0..100u32, payload in vec(any::<u8>(), 32..256).prop_map(AuthData)) -> Witness<AuthData> {
Witness { extension_id, mode, payload }
}
}
prop_compose! {
pub fn arb_tzein()(prevout in arb_outpoint(), witness in arb_witness()) -> TzeIn<Authorized> {
TzeIn { prevout, witness }
}
}
prop_compose! {
pub fn arb_precondition()(extension_id in 0..100u32, mode in 0..100u32, payload in vec(any::<u8>(), 32..256)) -> Precondition {
Precondition { extension_id, mode, payload }
}
}
prop_compose! {
pub fn arb_tzeout()(value in arb_nonnegative_amount(), precondition in arb_precondition()) -> TzeOut {
TzeOut { value, precondition }
}
}
prop_compose! {
pub fn arb_bundle(branch_id: BranchId)(
vin in vec(arb_tzein(), 0..10),
vout in vec(arb_tzeout(), 0..10),
) -> Option<Bundle<Authorized>> {
if branch_id != BranchId::ZFuture || (vin.is_empty() && vout.is_empty()) {
None
} else {
Some(Bundle { vin, vout })
}
}
}
}

View File

@ -5,7 +5,10 @@ use std::fmt;
use crate::{ use crate::{
extensions::transparent::{self as tze, ToPayload}, extensions::transparent::{self as tze, ToPayload},
transaction::components::{amount::Amount, TzeIn, TzeOut, TzeOutPoint}, transaction::components::{
amount::Amount,
tze::{Bundle, OutPoint, TzeIn, TzeOut, Unauthorized},
},
}; };
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -32,16 +35,16 @@ struct TzeSigner<'a, BuildCtx> {
pub struct TzeBuilder<'a, BuildCtx> { pub struct TzeBuilder<'a, BuildCtx> {
signers: Vec<TzeSigner<'a, BuildCtx>>, signers: Vec<TzeSigner<'a, BuildCtx>>,
tze_inputs: Vec<TzeIn>, vin: Vec<TzeIn<Unauthorized>>,
tze_outputs: Vec<TzeOut>, vout: Vec<TzeOut>,
} }
impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> { impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> {
pub fn empty() -> Self { pub fn empty() -> Self {
TzeBuilder { TzeBuilder {
signers: vec![], signers: vec![],
tze_inputs: vec![], vin: vec![],
tze_outputs: vec![], vout: vec![],
} }
} }
@ -49,13 +52,12 @@ impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> {
&mut self, &mut self,
extension_id: u32, extension_id: u32,
mode: u32, mode: u32,
(outpoint, prevout): (TzeOutPoint, TzeOut), (outpoint, prevout): (OutPoint, TzeOut),
witness_builder: WBuilder, witness_builder: WBuilder,
) where ) where
WBuilder: 'a + FnOnce(&BuildCtx) -> Result<W, Error>, WBuilder: 'a + FnOnce(&BuildCtx) -> Result<W, Error>,
{ {
self.tze_inputs self.vin.push(TzeIn::new(outpoint, extension_id, mode));
.push(TzeIn::new(outpoint, extension_id, mode));
self.signers.push(TzeSigner { self.signers.push(TzeSigner {
prevout, prevout,
builder: Box::new(move |ctx| witness_builder(&ctx).map(|x| x.to_payload())), builder: Box::new(move |ctx| witness_builder(&ctx).map(|x| x.to_payload())),
@ -73,7 +75,7 @@ impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> {
} }
let (mode, payload) = guarded_by.to_payload(); let (mode, payload) = guarded_by.to_payload();
self.tze_outputs.push(TzeOut { self.vout.push(TzeOut {
value, value,
precondition: tze::Precondition { precondition: tze::Precondition {
extension_id, extension_id,
@ -91,36 +93,49 @@ impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> {
.map(|s| s.prevout.value) .map(|s| s.prevout.value)
.sum::<Option<Amount>>()? .sum::<Option<Amount>>()?
- self - self
.tze_outputs .vout
.iter() .iter()
.map(|tzo| tzo.value) .map(|tzo| tzo.value)
.sum::<Option<Amount>>()? .sum::<Option<Amount>>()?
} }
pub fn build(&self) -> (Vec<TzeIn>, Vec<TzeOut>) { pub fn build(&self) -> Option<Bundle<Unauthorized>> {
(self.tze_inputs.clone(), self.tze_outputs.clone()) if self.vin.is_empty() && self.vout.is_empty() {
None
} else {
Some(Bundle {
vin: self.vin.clone(),
vout: self.vout.clone(),
})
}
} }
pub fn create_witnesses(self, mtx: &BuildCtx) -> Result<Vec<Vec<u8>>, Error> { pub fn create_witnesses(self, mtx: &BuildCtx) -> Result<Option<Vec<tze::AuthData>>, Error> {
// Create TZE input witnesses // Create TZE input witnesses
let payloads = self if self.vin.is_empty() && self.vout.is_empty() {
.signers Ok(None)
.into_iter() } else {
.zip(self.tze_inputs.into_iter()) // Create TZE input witnesses
.map(|(signer, tzein)| { let payloads = self
// The witness builder function should have cached/closed over whatever data was .signers
// necessary for the witness to commit to at the time it was added to the .into_iter()
// transaction builder; here, it then computes those commitments. .zip(self.vin.into_iter())
let (mode, payload) = (signer.builder)(&mtx)?; .into_iter()
let input_mode = tzein.witness.mode; .map(|(signer, tzein)| {
if mode != input_mode { // The witness builder function should have cached/closed over whatever data was
return Err(Error::WitnessModeMismatch(input_mode, mode)); // necessary for the witness to commit to at the time it was added to the
} // transaction builder; here, it then computes those commitments.
let (mode, payload) = (signer.builder)(&mtx)?;
let input_mode = tzein.witness.mode;
if mode != input_mode {
return Err(Error::WitnessModeMismatch(input_mode, mode));
}
Ok(payload) Ok(tze::AuthData(payload))
}) })
.collect::<Result<Vec<_>, Error>>()?; .collect::<Result<Vec<_>, Error>>()?;
Ok(payloads) Ok(Some(payloads))
}
} }
} }

View File

@ -7,32 +7,40 @@ use std::ops::Deref;
use crate::{ use crate::{
consensus::{BlockHeight, BranchId}, consensus::{BlockHeight, BranchId},
sapling::redjubjub::Signature, sapling::redjubjub,
serialize::Vector, serialize::{CompactSize, Vector},
}; };
use self::util::sha256d::{HashReader, HashWriter}; use self::{
components::{
amount::Amount,
sapling::{self, OutputDescription, SpendDescription},
sprout::{self, JsDescription},
transparent::{self, TxIn, TxOut},
},
sighash::{signature_hash_data, SignableInput, SIGHASH_ALL},
util::sha256d::{HashReader, HashWriter},
};
#[cfg(feature = "zfuture")]
use self::components::tze;
pub mod builder; pub mod builder;
pub mod components; pub mod components;
mod sighash; pub mod sighash;
pub mod util; pub mod util;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub use self::sighash::{signature_hash, signature_hash_data, SignableInput, SIGHASH_ALL};
use self::components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut};
#[cfg(feature = "zfuture")]
use self::components::{TzeIn, TzeOut};
const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270; const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270;
const OVERWINTER_TX_VERSION: u32 = 3; const OVERWINTER_TX_VERSION: u32 = 3;
const SAPLING_VERSION_GROUP_ID: u32 = 0x892F2085; const SAPLING_VERSION_GROUP_ID: u32 = 0x892F2085;
const SAPLING_TX_VERSION: u32 = 4; const SAPLING_TX_VERSION: u32 = 4;
const V5_TX_VERSION: u32 = 5;
const V5_VERSION_GROUP_ID: u32 = 0x26A7270A;
/// These versions are used exclusively for in-development transaction /// These versions are used exclusively for in-development transaction
/// serialization, and will never be active under the consensus rules. /// serialization, and will never be active under the consensus rules.
/// When new consensus transaction versions are added, all call sites /// When new consensus transaction versions are added, all call sites
@ -90,6 +98,7 @@ pub enum TxVersion {
Sprout(u32), Sprout(u32),
Overwinter, Overwinter,
Sapling, Sapling,
ZcashTxV5,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
ZFuture, ZFuture,
} }
@ -133,6 +142,7 @@ impl TxVersion {
TxVersion::Sprout(v) => *v, TxVersion::Sprout(v) => *v,
TxVersion::Overwinter => OVERWINTER_TX_VERSION, TxVersion::Overwinter => OVERWINTER_TX_VERSION,
TxVersion::Sapling => SAPLING_TX_VERSION, TxVersion::Sapling => SAPLING_TX_VERSION,
TxVersion::ZcashTxV5 => V5_TX_VERSION,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
TxVersion::ZFuture => ZFUTURE_TX_VERSION, TxVersion::ZFuture => ZFUTURE_TX_VERSION,
} }
@ -143,6 +153,7 @@ impl TxVersion {
TxVersion::Sprout(_) => 0, TxVersion::Sprout(_) => 0,
TxVersion::Overwinter => OVERWINTER_VERSION_GROUP_ID, TxVersion::Overwinter => OVERWINTER_VERSION_GROUP_ID,
TxVersion::Sapling => SAPLING_VERSION_GROUP_ID, TxVersion::Sapling => SAPLING_VERSION_GROUP_ID,
TxVersion::ZcashTxV5 => V5_VERSION_GROUP_ID,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
TxVersion::ZFuture => ZFUTURE_VERSION_GROUP_ID, TxVersion::ZFuture => ZFUTURE_VERSION_GROUP_ID,
} }
@ -160,15 +171,17 @@ impl TxVersion {
match self { match self {
TxVersion::Sprout(v) => *v >= 2u32, TxVersion::Sprout(v) => *v >= 2u32,
TxVersion::Overwinter | TxVersion::Sapling => true, TxVersion::Overwinter | TxVersion::Sapling => true,
TxVersion::ZcashTxV5 => false,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
TxVersion::ZFuture => true, TxVersion::ZFuture => true,
} }
} }
pub fn uses_groth_proofs(&self) -> bool { pub fn has_sapling(&self) -> bool {
match self { match self {
TxVersion::Sprout(_) | TxVersion::Overwinter => false, TxVersion::Sprout(_) | TxVersion::Overwinter => false,
TxVersion::Sapling => true, TxVersion::Sapling => true,
TxVersion::ZcashTxV5 => true,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
TxVersion::ZFuture => true, TxVersion::ZFuture => true,
} }
@ -188,17 +201,49 @@ impl TxVersion {
} }
} }
/// Authorization state for a bundle of transaction data.
pub trait Authorization {
type TransparentAuth: transparent::Authorization;
type SaplingAuth: sapling::Authorization;
type OrchardAuth: orchard::bundle::Authorization;
#[cfg(feature = "zfuture")]
type TzeAuth: tze::Authorization;
}
pub struct Authorized;
impl Authorization for Authorized {
type TransparentAuth = transparent::Authorized;
type SaplingAuth = sapling::Authorized;
type OrchardAuth = orchard::bundle::Authorized;
#[cfg(feature = "zfuture")]
type TzeAuth = tze::Authorized;
}
pub struct Unauthorized;
impl Authorization for Unauthorized {
type TransparentAuth = transparent::Unauthorized;
type SaplingAuth = sapling::Unauthorized;
type OrchardAuth = orchard::builder::Unauthorized;
#[cfg(feature = "zfuture")]
type TzeAuth = tze::Unauthorized;
}
/// A Zcash transaction. /// A Zcash transaction.
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct Transaction { pub struct Transaction {
txid: TxId, txid: TxId,
data: TransactionData, data: TransactionData<Authorized>,
} }
impl Deref for Transaction { impl Deref for Transaction {
type Target = TransactionData; type Target = TransactionData<Authorized>;
fn deref(&self) -> &TransactionData { fn deref(&self) -> &TransactionData<Authorized> {
&self.data &self.data
} }
} }
@ -209,95 +254,107 @@ impl PartialEq for Transaction {
} }
} }
#[derive(Clone)] pub struct TransactionData<A: Authorization> {
pub struct TransactionData {
pub version: TxVersion, pub version: TxVersion,
pub vin: Vec<TxIn>,
pub vout: Vec<TxOut>,
#[cfg(feature = "zfuture")]
pub tze_inputs: Vec<TzeIn>,
#[cfg(feature = "zfuture")]
pub tze_outputs: Vec<TzeOut>,
pub lock_time: u32, pub lock_time: u32,
pub expiry_height: BlockHeight, pub expiry_height: BlockHeight,
pub value_balance: Amount, pub transparent_bundle: Option<transparent::Bundle<A::TransparentAuth>>,
pub shielded_spends: Vec<SpendDescription>, pub sprout_bundle: Option<sprout::Bundle>,
pub shielded_outputs: Vec<OutputDescription>, pub sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>,
pub joinsplits: Vec<JsDescription>, pub orchard_bundle: Option<orchard::bundle::Bundle<A::OrchardAuth, Amount>>,
pub joinsplit_pubkey: Option<[u8; 32]>, #[cfg(feature = "zfuture")]
pub joinsplit_sig: Option<[u8; 64]>, pub tze_bundle: Option<tze::Bundle<A::TzeAuth>>,
pub binding_sig: Option<Signature>,
} }
impl std::fmt::Debug for TransactionData { impl<A: Authorization> std::fmt::Debug for TransactionData<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!( write!(
f, f,
"TransactionData( "TransactionData(
version = {:?}, version = {:?},
vin = {:?},
vout = {:?},{}
lock_time = {:?}, lock_time = {:?},
expiry_height = {:?}, expiry_height = {:?},
{}{}{}{}",
self.version,
self.lock_time,
self.expiry_height,
if let Some(b) = &self.transparent_bundle {
format!(
"
vin = {:?},
vout = {:?},",
b.vin, b.vout
)
} else {
"".to_string()
},
if let Some(b) = &self.sprout_bundle {
format!(
"
joinsplits = {:?},
joinsplit_pubkey = {:?},",
b.joinsplits, b.joinsplit_pubkey
)
} else {
"".to_string()
},
if let Some(b) = &self.sapling_bundle {
format!(
"
value_balance = {:?}, value_balance = {:?},
shielded_spends = {:?}, shielded_spends = {:?},
shielded_outputs = {:?}, shielded_outputs = {:?},
joinsplits = {:?}, binding_sig = {:?},",
joinsplit_pubkey = {:?}, b.value_balance, b.shielded_spends, b.shielded_outputs, b.authorization
binding_sig = {:?})", )
self.version, } else {
self.vin, "".to_string()
self.vout, },
{ {
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
{ if let Some(b) = &self.tze_bundle {
format!( format!(
" "
tze_inputs = {:?}, tze_inputs = {:?},
tze_outputs = {:?},", tze_outputs = {:?},",
self.tze_inputs, self.tze_outputs b.vin, b.vout
) )
} else {
"".to_string()
} }
#[cfg(not(feature = "zfuture"))] #[cfg(not(feature = "zfuture"))]
"" ""
}, }
self.lock_time,
self.expiry_height,
self.value_balance,
self.shielded_spends,
self.shielded_outputs,
self.joinsplits,
self.joinsplit_pubkey,
self.binding_sig
) )
} }
} }
impl Default for TransactionData { impl<A: Authorization> TransactionData<A> {
fn default() -> Self { pub fn sapling_value_balance(&self) -> Amount {
TransactionData::new() self.sapling_bundle
.as_ref()
.map_or(Amount::zero(), |b| b.value_balance)
} }
} }
impl TransactionData { impl Default for TransactionData<Unauthorized> {
fn default() -> Self {
Self::new()
}
}
impl TransactionData<Unauthorized> {
pub fn new() -> Self { pub fn new() -> Self {
TransactionData { TransactionData {
version: TxVersion::Sapling, version: TxVersion::Sapling,
vin: vec![],
vout: vec![],
#[cfg(feature = "zfuture")]
tze_inputs: vec![],
#[cfg(feature = "zfuture")]
tze_outputs: vec![],
lock_time: 0, lock_time: 0,
expiry_height: 0u32.into(), expiry_height: 0u32.into(),
value_balance: Amount::zero(), transparent_bundle: None,
shielded_spends: vec![], sprout_bundle: None,
shielded_outputs: vec![], sapling_bundle: None,
joinsplits: vec![], orchard_bundle: None,
joinsplit_pubkey: None, #[cfg(feature = "zfuture")]
joinsplit_sig: None, tze_bundle: None,
binding_sig: None,
} }
} }
@ -305,29 +362,28 @@ impl TransactionData {
pub fn zfuture() -> Self { pub fn zfuture() -> Self {
TransactionData { TransactionData {
version: TxVersion::ZFuture, version: TxVersion::ZFuture,
vin: vec![],
vout: vec![],
tze_inputs: vec![],
tze_outputs: vec![],
lock_time: 0, lock_time: 0,
expiry_height: 0u32.into(), expiry_height: 0u32.into(),
value_balance: Amount::zero(), transparent_bundle: None,
shielded_spends: vec![], sprout_bundle: None,
shielded_outputs: vec![], sapling_bundle: None,
joinsplits: vec![], orchard_bundle: None,
joinsplit_pubkey: None, tze_bundle: None,
joinsplit_sig: None,
binding_sig: None,
} }
} }
}
pub fn freeze(self) -> io::Result<Transaction> { impl TransactionData<Authorized> {
Transaction::from_data(self) pub fn freeze(self, consensus_branch_id: BranchId) -> io::Result<Transaction> {
Transaction::from_data(self, consensus_branch_id)
} }
} }
impl Transaction { impl Transaction {
fn from_data(data: TransactionData) -> io::Result<Self> { fn from_data(
data: TransactionData<Authorized>,
_consensus_branch_id: BranchId,
) -> io::Result<Self> {
let mut tx = Transaction { let mut tx = Transaction {
txid: TxId([0; 32]), txid: TxId([0; 32]),
data, data,
@ -349,70 +405,56 @@ impl Transaction {
let is_overwinter_v3 = version == TxVersion::Overwinter; let is_overwinter_v3 = version == TxVersion::Overwinter;
let is_sapling_v4 = version == TxVersion::Sapling; let is_sapling_v4 = version == TxVersion::Sapling;
#[cfg(feature = "zfuture")] let transparent_bundle = Self::read_transparent(&mut reader)?;
let has_tze = version == TxVersion::ZFuture;
#[cfg(not(feature = "zfuture"))]
let has_tze = false;
let vin = Vector::read(&mut reader, TxIn::read)?;
let vout = Vector::read(&mut reader, TxOut::read)?;
#[cfg(feature = "zfuture")]
let (tze_inputs, tze_outputs) = if has_tze {
let wi = Vector::read(&mut reader, TzeIn::read)?;
let wo = Vector::read(&mut reader, TzeOut::read)?;
(wi, wo)
} else {
(vec![], vec![])
};
let lock_time = reader.read_u32::<LittleEndian>()?; let lock_time = reader.read_u32::<LittleEndian>()?;
let expiry_height: BlockHeight = if is_overwinter_v3 || is_sapling_v4 || has_tze { let expiry_height: BlockHeight = if is_overwinter_v3 || is_sapling_v4 {
reader.read_u32::<LittleEndian>()?.into() reader.read_u32::<LittleEndian>()?.into()
} else { } else {
0u32.into() 0u32.into()
}; };
let (value_balance, shielded_spends, shielded_outputs) = if is_sapling_v4 || has_tze { let (value_balance, shielded_spends, shielded_outputs) = if version.has_sapling() {
let vb = { let vb = Self::read_amount(&mut reader)?;
let mut tmp = [0; 8]; #[allow(clippy::redundant_closure)]
reader.read_exact(&mut tmp)?; let ss: Vec<SpendDescription<sapling::Authorized>> =
Amount::from_i64_le_bytes(tmp) Vector::read(&mut reader, |r| SpendDescription::read(r))?;
} #[allow(clippy::redundant_closure)]
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range"))?; let so: Vec<OutputDescription<sapling::Authorized>> =
let ss = Vector::read(&mut reader, SpendDescription::read)?; Vector::read(&mut reader, |r| OutputDescription::read(r))?;
let so = Vector::read(&mut reader, OutputDescription::read)?;
(vb, ss, so) (vb, ss, so)
} else { } else {
(Amount::zero(), vec![], vec![]) (Amount::zero(), vec![], vec![])
}; };
let (joinsplits, joinsplit_pubkey, joinsplit_sig) = if version.has_sprout() { let sprout_bundle = if version.has_sprout() {
let jss = Vector::read(&mut reader, |r| { let joinsplits = Vector::read(&mut reader, |r| {
JsDescription::read(r, version.uses_groth_proofs()) JsDescription::read(r, version.has_sapling())
})?; })?;
let (pubkey, sig) = if !jss.is_empty() {
let mut joinsplit_pubkey = [0; 32];
let mut joinsplit_sig = [0; 64];
reader.read_exact(&mut joinsplit_pubkey)?;
reader.read_exact(&mut joinsplit_sig)?;
(Some(joinsplit_pubkey), Some(joinsplit_sig))
} else {
(None, None)
};
(jss, pubkey, sig)
} else {
(vec![], None, None)
};
let binding_sig = if (is_sapling_v4 || has_tze) if !joinsplits.is_empty() {
&& !(shielded_spends.is_empty() && shielded_outputs.is_empty()) let mut bundle = sprout::Bundle {
{ joinsplits,
Some(Signature::read(&mut reader)?) joinsplit_pubkey: [0; 32],
joinsplit_sig: [0; 64],
};
reader.read_exact(&mut bundle.joinsplit_pubkey)?;
reader.read_exact(&mut bundle.joinsplit_sig)?;
Some(bundle)
} else {
None
}
} else { } else {
None None
}; };
let binding_sig =
if is_sapling_v4 && !(shielded_spends.is_empty() && shielded_outputs.is_empty()) {
Some(redjubjub::Signature::read(&mut reader)?)
} else {
None
};
let mut txid = [0; 32]; let mut txid = [0; 32];
txid.copy_from_slice(&reader.into_hash()); txid.copy_from_slice(&reader.into_hash());
@ -420,214 +462,150 @@ impl Transaction {
txid: TxId(txid), txid: TxId(txid),
data: TransactionData { data: TransactionData {
version, version,
vin,
vout,
#[cfg(feature = "zfuture")]
tze_inputs,
#[cfg(feature = "zfuture")]
tze_outputs,
lock_time, lock_time,
expiry_height, expiry_height,
value_balance, transparent_bundle,
shielded_spends, sprout_bundle,
shielded_outputs, sapling_bundle: binding_sig.map(|binding_sig| sapling::Bundle {
joinsplits, value_balance,
joinsplit_pubkey, shielded_spends,
joinsplit_sig, shielded_outputs,
binding_sig, authorization: sapling::Authorized { binding_sig },
}),
orchard_bundle: None,
#[cfg(feature = "zfuture")]
tze_bundle: None,
}, },
}) })
} }
fn read_transparent<R: Read>(
mut reader: R,
) -> io::Result<Option<transparent::Bundle<transparent::Authorized>>> {
let vin = Vector::read(&mut reader, TxIn::read)?;
let vout = Vector::read(&mut reader, TxOut::read)?;
Ok(if vin.is_empty() && vout.is_empty() {
None
} else {
Some(transparent::Bundle { vin, vout })
})
}
fn read_amount<R: Read>(mut reader: R) -> io::Result<Amount> {
let mut tmp = [0; 8];
reader.read_exact(&mut tmp)?;
Amount::from_i64_le_bytes(tmp)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range"))
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> { pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
self.version.write(&mut writer)?; self.version.write(&mut writer)?;
let is_overwinter_v3 = self.version == TxVersion::Overwinter; let is_overwinter_v3 = self.version == TxVersion::Overwinter;
let is_sapling_v4 = self.version == TxVersion::Sapling; let is_sapling_v4 = self.version == TxVersion::Sapling;
#[cfg(feature = "zfuture")]
let has_tze = self.version == TxVersion::ZFuture;
#[cfg(not(feature = "zfuture"))]
let has_tze = false;
Vector::write(&mut writer, &self.vin, |w, e| e.write(w))?; self.write_transparent(&mut writer)?;
Vector::write(&mut writer, &self.vout, |w, e| e.write(w))?;
#[cfg(feature = "zfuture")]
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::<LittleEndian>(self.lock_time)?; writer.write_u32::<LittleEndian>(self.lock_time)?;
if is_overwinter_v3 || is_sapling_v4 || has_tze { if is_overwinter_v3 || is_sapling_v4 {
writer.write_u32::<LittleEndian>(u32::from(self.expiry_height))?; writer.write_u32::<LittleEndian>(u32::from(self.expiry_height))?;
} }
if is_sapling_v4 || has_tze { if self.version.has_sapling() {
writer.write_all(&self.value_balance.to_i64_le_bytes())?; writer.write_all(
Vector::write(&mut writer, &self.shielded_spends, |w, e| e.write(w))?; &self
Vector::write(&mut writer, &self.shielded_outputs, |w, e| e.write(w))?; .sapling_bundle
.as_ref()
.map_or(Amount::zero(), |b| b.value_balance)
.to_i64_le_bytes(),
)?;
Vector::write(
&mut writer,
self.sapling_bundle
.as_ref()
.map_or(&[], |b| &b.shielded_spends),
|w, e| e.write(w),
)?;
Vector::write(
&mut writer,
self.sapling_bundle
.as_ref()
.map_or(&[], |b| &b.shielded_outputs),
|w, e| e.write(w),
)?;
} else if self.sapling_bundle.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Sapling components may not be present if Sapling is not active.",
));
} }
if self.version.has_sprout() { if self.version.has_sprout() {
Vector::write(&mut writer, &self.joinsplits, |w, e| e.write(w))?; if let Some(bundle) = self.sprout_bundle.as_ref() {
if !self.joinsplits.is_empty() { Vector::write(&mut writer, &bundle.joinsplits, |w, e| e.write(w))?;
match self.joinsplit_pubkey { writer.write_all(&bundle.joinsplit_pubkey)?;
Some(pubkey) => writer.write_all(&pubkey)?, writer.write_all(&bundle.joinsplit_sig)?;
None => { } else {
return Err(io::Error::new( CompactSize::write(&mut writer, 0)?;
io::ErrorKind::InvalidInput,
"Missing JoinSplit pubkey",
));
}
}
match self.joinsplit_sig {
Some(sig) => writer.write_all(&sig)?,
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Missing JoinSplit signature",
));
}
}
} }
} }
if !self.version.has_sprout() || self.joinsplits.is_empty() { if self.version.has_sapling() {
if self.joinsplit_pubkey.is_some() { if let Some(bundle) = self.sapling_bundle.as_ref() {
return Err(io::Error::new( bundle.authorization.binding_sig.write(&mut writer)?;
io::ErrorKind::InvalidInput,
"JoinSplit pubkey should not be present",
));
} }
if self.joinsplit_sig.is_some() { } else if self.sapling_bundle.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"JoinSplit signature should not be present",
));
}
}
if (is_sapling_v4 || has_tze)
&& !(self.shielded_spends.is_empty() && self.shielded_outputs.is_empty())
{
match self.binding_sig {
Some(sig) => sig.write(&mut writer)?,
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Missing binding signature",
));
}
}
} else if self.binding_sig.is_some() {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Binding signature should not be present", "Sapling components may not be present if Sapling is not active.",
)); ));
} }
Ok(()) Ok(())
} }
pub fn write_transparent<W: Write>(&self, mut writer: W) -> io::Result<()> {
if let Some(bundle) = &self.transparent_bundle {
Vector::write(&mut writer, &bundle.vin, |w, e| e.write(w))?;
Vector::write(&mut writer, &bundle.vout, |w, e| e.write(w))?;
} else {
CompactSize::write(&mut writer, 0)?;
CompactSize::write(&mut writer, 0)?;
}
Ok(())
}
#[cfg(feature = "zfuture")]
pub fn write_tze<W: Write>(&self, mut writer: W) -> io::Result<()> {
if let Some(bundle) = &self.tze_bundle {
Vector::write(&mut writer, &bundle.vin, |w, e| e.write(w))?;
Vector::write(&mut writer, &bundle.vout, |w, e| e.write(w))?;
} else {
CompactSize::write(&mut writer, 0)?;
CompactSize::write(&mut writer, 0)?;
}
Ok(())
}
} }
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use proptest::collection::vec;
use proptest::prelude::*; use proptest::prelude::*;
use proptest::sample::select; use proptest::sample::select;
use crate::{consensus::BranchId, legacy::Script}; use crate::consensus::BranchId;
#[cfg(feature = "zfuture")]
use crate::extensions::transparent as tze;
use super::{ use super::{
components::{amount::MAX_MONEY, Amount, OutPoint, TxIn, TxOut}, components::transparent::testing as transparent, Authorized, Transaction, TransactionData,
Transaction, TransactionData, TxId, TxVersion, TxId, TxVersion,
}; };
#[cfg(feature = "zfuture")]
use super::components::{TzeIn, TzeOut, TzeOutPoint};
pub const VALID_OPCODES: [u8; 8] = [
0x00, // OP_FALSE,
0x51, // OP_1,
0x52, // OP_2,
0x53, // OP_3,
0xac, // OP_CHECKSIG,
0x63, // OP_IF,
0x65, // OP_VERIF,
0x6a, // OP_RETURN,
];
pub fn arb_txid() -> impl Strategy<Value = TxId> { pub fn arb_txid() -> impl Strategy<Value = TxId> {
prop::array::uniform32(any::<u8>()).prop_map(TxId::from_bytes) prop::array::uniform32(any::<u8>()).prop_map(TxId::from_bytes)
} }
prop_compose! {
pub fn arb_outpoint()(hash in prop::array::uniform32(1u8..), n in 1..100u32) -> OutPoint {
OutPoint::new(hash, n)
}
}
prop_compose! {
pub fn arb_script()(v in vec(select(&VALID_OPCODES[..]), 1..256)) -> Script {
Script(v)
}
}
prop_compose! {
pub fn arb_txin()(prevout in arb_outpoint(), script_sig in arb_script(), sequence in any::<u32>()) -> TxIn {
TxIn { prevout, script_sig, sequence }
}
}
prop_compose! {
pub fn arb_amount()(value in 0..MAX_MONEY) -> Amount {
Amount::from_i64(value).unwrap()
}
}
prop_compose! {
pub fn arb_txout()(value in arb_amount(), script_pubkey in arb_script()) -> TxOut {
TxOut { value, script_pubkey }
}
}
#[cfg(feature = "zfuture")]
prop_compose! {
pub fn arb_tzeoutpoint()(txid in arb_txid(), n in 1..100u32) -> TzeOutPoint {
TzeOutPoint::new(txid, n)
}
}
#[cfg(feature = "zfuture")]
prop_compose! {
pub fn arb_witness()(extension_id in 0..100u32, mode in 0..100u32, payload in vec(any::<u8>(), 32..256)) -> tze::Witness {
tze::Witness { extension_id, mode, payload }
}
}
#[cfg(feature = "zfuture")]
prop_compose! {
pub fn arb_tzein()(prevout in arb_tzeoutpoint(), witness in arb_witness()) -> TzeIn {
TzeIn { prevout, witness }
}
}
#[cfg(feature = "zfuture")]
prop_compose! {
pub fn arb_precondition()(extension_id in 0..100u32, mode in 0..100u32, payload in vec(any::<u8>(), 32..256)) -> tze::Precondition {
tze::Precondition { extension_id, mode, payload }
}
}
#[cfg(feature = "zfuture")]
prop_compose! {
fn arb_tzeout()(value in arb_amount(), precondition in arb_precondition()) -> TzeOut {
TzeOut { value, precondition }
}
}
pub fn arb_branch_id() -> impl Strategy<Value = BranchId> { pub fn arb_branch_id() -> impl Strategy<Value = BranchId> {
select(vec![ select(vec![
BranchId::Sprout, BranchId::Sprout,
@ -641,81 +619,61 @@ pub mod testing {
]) ])
} }
fn tx_versions(branch_id: BranchId) -> impl Strategy<Value = TxVersion> { pub fn arb_tx_version(branch_id: BranchId) -> impl Strategy<Value = TxVersion> {
match branch_id { match branch_id {
BranchId::Sprout => (1..=2u32).prop_map(TxVersion::Sprout).boxed(), BranchId::Sprout => (1..=2u32).prop_map(TxVersion::Sprout).boxed(),
BranchId::Overwinter => Just(TxVersion::Overwinter).boxed(), BranchId::Overwinter => Just(TxVersion::Overwinter).boxed(),
#[cfg(feature = "zfuture")] //#[cfg(feature = "zfuture")]
BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(), //BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(),
_otherwise => Just(TxVersion::Sapling).boxed(), _otherwise => Just(TxVersion::Sapling).boxed(),
} }
} }
#[cfg(feature = "zfuture")]
prop_compose! {
pub fn arb_txdata(branch_id: BranchId)(
version 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::<u32>(),
expiry_height in any::<u32>(),
value_balance in arb_amount(),
) -> TransactionData {
TransactionData {
version,
vin, vout,
tze_inputs: if branch_id == BranchId::ZFuture { tze_inputs } else { vec![] },
tze_outputs: if branch_id == BranchId::ZFuture { tze_outputs } else { vec![] },
lock_time,
expiry_height: expiry_height.into(),
value_balance: match version {
TxVersion::Sprout(_) | TxVersion::Overwinter => Amount::zero(),
_ => 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
}
}
}
#[cfg(not(feature = "zfuture"))] #[cfg(not(feature = "zfuture"))]
prop_compose! { prop_compose! {
pub fn arb_txdata(branch_id: BranchId)( pub fn arb_txdata(branch_id: BranchId)(
version in tx_versions(branch_id), version in arb_tx_version(branch_id),
vin in vec(arb_txin(), 0..10),
vout in vec(arb_txout(), 0..10),
lock_time in any::<u32>(), lock_time in any::<u32>(),
expiry_height in any::<u32>(), expiry_height in any::<u32>(),
value_balance in arb_amount(), transparent_bundle in transparent::arb_bundle(),
) -> TransactionData { ) -> TransactionData<Authorized> {
TransactionData { TransactionData {
version, version,
vin, vout,
lock_time, lock_time,
expiry_height: expiry_height.into(), expiry_height: expiry_height.into(),
value_balance: match version { transparent_bundle,
TxVersion::Sprout(_) | TxVersion::Overwinter => Amount::zero(), sprout_bundle: None,
_ => value_balance, sapling_bundle: None, //FIXME
}, orchard_bundle: None, //FIXME
shielded_spends: vec![], //FIXME }
shielded_outputs: vec![], //FIXME }
joinsplits: vec![], //FIXME }
joinsplit_pubkey: None, //FIXME
joinsplit_sig: None, //FIXME #[cfg(feature = "zfuture")]
binding_sig: None, //FIXME prop_compose! {
pub fn arb_txdata(branch_id: BranchId)(
version in arb_tx_version(branch_id),
lock_time in any::<u32>(),
expiry_height in any::<u32>(),
transparent_bundle in transparent::arb_bundle(),
//tze_bundle in tze::arb_bundle(branch_id),
) -> TransactionData<Authorized> {
TransactionData {
version,
lock_time,
expiry_height: expiry_height.into(),
transparent_bundle,
sprout_bundle: None,
sapling_bundle: None, //FIXME
orchard_bundle: None, //FIXME
tze_bundle: None
} }
} }
} }
prop_compose! { prop_compose! {
pub fn arb_tx(branch_id: BranchId)(tx_data in arb_txdata(branch_id)) -> Transaction { pub fn arb_tx(branch_id: BranchId)(tx_data in arb_txdata(branch_id)) -> Transaction {
Transaction::from_data(tx_data).unwrap() Transaction::from_data(tx_data, branch_id).unwrap()
} }
} }
} }

View File

@ -1,27 +1,26 @@
#[cfg(feature = "zfuture")]
use std::convert::TryInto;
use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams};
use byteorder::{LittleEndian, WriteBytesExt}; use byteorder::{LittleEndian, WriteBytesExt};
use ff::PrimeField; use ff::PrimeField;
use group::GroupEncoding; use group::GroupEncoding;
use crate::{consensus, legacy::Script}; use crate::{
consensus::{self, BranchId},
legacy::Script,
};
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
use crate::{ use crate::extensions::transparent::Precondition;
extensions::transparent::Precondition,
serialize::{CompactSize, Vector},
};
use super::{ use super::{
components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut}, components::{
Transaction, TransactionData, TxVersion, amount::Amount,
sapling::{self, GrothProofBytes, OutputDescription, SpendDescription},
sprout::JsDescription,
transparent::{self, TxIn, TxOut},
},
Authorization, Transaction, TransactionData, TxVersion,
}; };
#[cfg(feature = "zfuture")]
use super::components::{TzeIn, TzeOut};
const ZCASH_SIGHASH_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashSigHash"; const ZCASH_SIGHASH_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashSigHash";
const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashPrevoutHash"; const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashPrevoutHash";
const ZCASH_SEQUENCE_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSequencHash"; const ZCASH_SEQUENCE_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSequencHash";
@ -30,16 +29,6 @@ const ZCASH_JOINSPLITS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashJSplitsHash";
const ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSSpendsHash"; const ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSSpendsHash";
const ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSOutputHash"; const ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSOutputHash";
#[cfg(feature = "zfuture")]
const ZCASH_TZE_INPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash_TzeInsHash";
#[cfg(feature = "zfuture")]
const ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashTzeOutsHash";
#[cfg(feature = "zfuture")]
const ZCASH_TZE_SIGNED_INPUT_TAG: &[u8; 1] = &[0x00];
#[cfg(feature = "zfuture")]
const ZCASH_TRANSPARENT_SIGNED_INPUT_TAG: &[u8; 1] = &[0x01];
pub const SIGHASH_ALL: u32 = 1; pub const SIGHASH_ALL: u32 = 1;
const SIGHASH_NONE: u32 = 2; const SIGHASH_NONE: u32 = 2;
const SIGHASH_SINGLE: u32 = 3; const SIGHASH_SINGLE: u32 = 3;
@ -67,16 +56,7 @@ fn has_overwinter_components(version: &TxVersion) -> bool {
!matches!(version, TxVersion::Sprout(_)) !matches!(version, TxVersion::Sprout(_))
} }
fn has_sapling_components(version: &TxVersion) -> bool { fn prevout_hash<TA: transparent::Authorization>(vin: &[TxIn<TA>]) -> Blake2bHash {
!matches!(version, TxVersion::Sprout(_) | TxVersion::Overwinter)
}
#[cfg(feature = "zfuture")]
fn has_tze_components(version: &TxVersion) -> bool {
matches!(version, TxVersion::ZFuture)
}
fn prevout_hash(vin: &[TxIn]) -> Blake2bHash {
let mut data = Vec::with_capacity(vin.len() * 36); let mut data = Vec::with_capacity(vin.len() * 36);
for t_in in vin { for t_in in vin {
t_in.prevout.write(&mut data).unwrap(); t_in.prevout.write(&mut data).unwrap();
@ -87,7 +67,7 @@ fn prevout_hash(vin: &[TxIn]) -> Blake2bHash {
.hash(&data) .hash(&data)
} }
fn sequence_hash(vin: &[TxIn]) -> Blake2bHash { fn sequence_hash<TA: transparent::Authorization>(vin: &[TxIn<TA>]) -> Blake2bHash {
let mut data = Vec::with_capacity(vin.len() * 4); let mut data = Vec::with_capacity(vin.len() * 4);
for t_in in vin { for t_in in vin {
(&mut data) (&mut data)
@ -121,13 +101,13 @@ fn single_output_hash(tx_out: &TxOut) -> Blake2bHash {
} }
fn joinsplits_hash( fn joinsplits_hash(
txversion: TxVersion, consensus_branch_id: BranchId,
joinsplits: &[JsDescription], joinsplits: &[JsDescription],
joinsplit_pubkey: &[u8; 32], joinsplit_pubkey: &[u8; 32],
) -> Blake2bHash { ) -> Blake2bHash {
let mut data = Vec::with_capacity( let mut data = Vec::with_capacity(
joinsplits.len() joinsplits.len()
* if txversion.uses_groth_proofs() { * if consensus_branch_id.sprout_uses_groth_proofs() {
1698 // JSDescription with Groth16 proof 1698 // JSDescription with Groth16 proof
} else { } else {
1802 // JSDescription with PHGR13 proof 1802 // JSDescription with PHGR13 proof
@ -143,7 +123,9 @@ fn joinsplits_hash(
.hash(&data) .hash(&data)
} }
fn shielded_spends_hash(shielded_spends: &[SpendDescription]) -> Blake2bHash { fn shielded_spends_hash<A: sapling::Authorization<Proof = GrothProofBytes>>(
shielded_spends: &[SpendDescription<A>],
) -> Blake2bHash {
let mut data = Vec::with_capacity(shielded_spends.len() * 384); let mut data = Vec::with_capacity(shielded_spends.len() * 384);
for s_spend in shielded_spends { for s_spend in shielded_spends {
data.extend_from_slice(&s_spend.cv.to_bytes()); data.extend_from_slice(&s_spend.cv.to_bytes());
@ -158,7 +140,9 @@ fn shielded_spends_hash(shielded_spends: &[SpendDescription]) -> Blake2bHash {
.hash(&data) .hash(&data)
} }
fn shielded_outputs_hash(shielded_outputs: &[OutputDescription]) -> Blake2bHash { fn shielded_outputs_hash<A: sapling::Authorization<Proof = GrothProofBytes>>(
shielded_outputs: &[OutputDescription<A>],
) -> Blake2bHash {
let mut data = Vec::with_capacity(shielded_outputs.len() * 948); let mut data = Vec::with_capacity(shielded_outputs.len() * 948);
for s_out in shielded_outputs { for s_out in shielded_outputs {
s_out.write(&mut data).unwrap(); s_out.write(&mut data).unwrap();
@ -169,30 +153,6 @@ fn shielded_outputs_hash(shielded_outputs: &[OutputDescription]) -> Blake2bHash
.hash(&data) .hash(&data)
} }
#[cfg(feature = "zfuture")]
fn tze_inputs_hash(tze_inputs: &[TzeIn]) -> Blake2bHash {
let mut data = vec![];
for tzein in tze_inputs {
tzein.write_without_witness(&mut data).unwrap();
}
Blake2bParams::new()
.hash_length(32)
.personal(ZCASH_TZE_INPUTS_HASH_PERSONALIZATION)
.hash(&data)
}
#[cfg(feature = "zfuture")]
fn tze_outputs_hash(tze_outputs: &[TzeOut]) -> Blake2bHash {
let mut data = vec![];
for tzeout in tze_outputs {
tzeout.write(&mut data).unwrap();
}
Blake2bParams::new()
.hash_length(32)
.personal(ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION)
.hash(&data)
}
pub enum SignableInput<'a> { pub enum SignableInput<'a> {
Shielded, Shielded,
Transparent { Transparent {
@ -227,8 +187,12 @@ impl<'a> SignableInput<'a> {
} }
} }
pub fn signature_hash_data( pub fn signature_hash_data<
tx: &TransactionData, TA: transparent::Authorization,
SA: sapling::Authorization<Proof = GrothProofBytes>,
A: Authorization<SaplingAuth = SA, TransparentAuth = TA>,
>(
tx: &TransactionData<A>,
consensus_branch_id: consensus::BranchId, consensus_branch_id: consensus::BranchId,
hash_type: u32, hash_type: u32,
signable_input: SignableInput<'_>, signable_input: SignableInput<'_>,
@ -251,64 +215,91 @@ pub fn signature_hash_data(
update_hash!( update_hash!(
h, h,
hash_type & SIGHASH_ANYONECANPAY == 0, hash_type & SIGHASH_ANYONECANPAY == 0,
prevout_hash(&tx.vin) prevout_hash(
tx.transparent_bundle
.as_ref()
.map_or(&[], |b| b.vin.as_slice())
)
); );
update_hash!( update_hash!(
h, h,
hash_type & SIGHASH_ANYONECANPAY == 0 hash_type & SIGHASH_ANYONECANPAY == 0
&& (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE && (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE
&& (hash_type & SIGHASH_MASK) != SIGHASH_NONE, && (hash_type & SIGHASH_MASK) != SIGHASH_NONE,
sequence_hash(&tx.vin) sequence_hash(
tx.transparent_bundle
.as_ref()
.map_or(&[], |b| b.vin.as_slice())
)
); );
if (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE if (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE
&& (hash_type & SIGHASH_MASK) != SIGHASH_NONE && (hash_type & SIGHASH_MASK) != SIGHASH_NONE
{ {
h.update(outputs_hash(&tx.vout).as_ref()); h.update(
outputs_hash(
tx.transparent_bundle
.as_ref()
.map_or(&[], |b| b.vout.as_slice()),
)
.as_bytes(),
);
} else if (hash_type & SIGHASH_MASK) == SIGHASH_SINGLE { } else if (hash_type & SIGHASH_MASK) == SIGHASH_SINGLE {
match signable_input { match (tx.transparent_bundle.as_ref(), &signable_input) {
SignableInput::Transparent { index, .. } if index < tx.vout.len() => { (Some(b), SignableInput::Transparent { index, .. }) if *index < b.vout.len() => {
h.update(single_output_hash(&tx.vout[index]).as_ref()) h.update(single_output_hash(&b.vout[*index]).as_bytes())
} }
_ => h.update(&[0; 32]), _ => h.update(&[0; 32]),
}; };
} else { } else {
h.update(&[0; 32]); h.update(&[0; 32]);
}; };
#[cfg(feature = "zfuture")]
if has_tze_components(&tx.version) {
update_hash!(
h,
!tx.tze_inputs.is_empty(),
tze_inputs_hash(&tx.tze_inputs)
);
update_hash!(
h,
!tx.tze_outputs.is_empty(),
tze_outputs_hash(&tx.tze_outputs)
);
}
update_hash!( update_hash!(
h, h,
!tx.joinsplits.is_empty(), !tx.sprout_bundle
joinsplits_hash(tx.version, &tx.joinsplits, &tx.joinsplit_pubkey.unwrap()) .as_ref()
.map_or(true, |b| b.joinsplits.is_empty()),
{
let bundle = tx.sprout_bundle.as_ref().unwrap();
joinsplits_hash(
consensus_branch_id,
&bundle.joinsplits,
&bundle.joinsplit_pubkey,
)
}
); );
if has_sapling_components(&tx.version) { if tx.version.has_sapling() {
update_hash!( update_hash!(
h, h,
!tx.shielded_spends.is_empty(), !tx.sapling_bundle
shielded_spends_hash(&tx.shielded_spends) .as_ref()
.map_or(true, |b| b.shielded_spends.is_empty()),
shielded_spends_hash(
tx.sapling_bundle
.as_ref()
.unwrap()
.shielded_spends
.as_slice()
)
); );
update_hash!( update_hash!(
h, h,
!tx.shielded_outputs.is_empty(), !tx.sapling_bundle
shielded_outputs_hash(&tx.shielded_outputs) .as_ref()
.map_or(true, |b| b.shielded_outputs.is_empty()),
shielded_outputs_hash(
tx.sapling_bundle
.as_ref()
.unwrap()
.shielded_outputs
.as_slice()
)
); );
} }
update_u32!(h, tx.lock_time, tmp); update_u32!(h, tx.lock_time, tmp);
update_u32!(h, tx.expiry_height.into(), tmp); update_u32!(h, tx.expiry_height.into(), tmp);
if has_sapling_components(&tx.version) { if tx.version.has_sapling() {
h.update(&tx.value_balance.to_i64_le_bytes()); h.update(&tx.sapling_value_balance().to_i64_le_bytes());
} }
update_u32!(h, hash_type, tmp); update_u32!(h, hash_type, tmp);
@ -318,52 +309,28 @@ pub fn signature_hash_data(
script_code, script_code,
value, value,
} => { } => {
#[cfg(feature = "zfuture")] if let Some(bundle) = tx.transparent_bundle.as_ref() {
let mut data = if has_tze_components(&tx.version) { let mut data = vec![];
// domain separation here is to avoid collision attacks
// between transparent and TZE inputs. bundle.vin[index].prevout.write(&mut data).unwrap();
ZCASH_TRANSPARENT_SIGNED_INPUT_TAG.to_vec() script_code.write(&mut data).unwrap();
data.extend_from_slice(&value.to_i64_le_bytes());
(&mut data)
.write_u32::<LittleEndian>(bundle.vin[index].sequence)
.unwrap();
h.update(&data);
} else { } else {
vec![] panic!(
}; "A request has been made to sign a transparent input, but none are present."
);
#[cfg(not(feature = "zfuture"))] }
let mut data = vec![];
tx.vin[index].prevout.write(&mut data).unwrap();
script_code.write(&mut data).unwrap();
data.extend_from_slice(&value.to_i64_le_bytes());
(&mut data)
.write_u32::<LittleEndian>(tx.vin[index].sequence)
.unwrap();
h.update(&data);
} }
#[cfg(feature = "zfuture")]
SignableInput::Tze {
index,
precondition,
value,
} if has_tze_components(&tx.version) => {
// domain separation here is to avoid collision attacks
// between transparent and TZE inputs.
let mut data = ZCASH_TZE_SIGNED_INPUT_TAG.to_vec();
tx.tze_inputs[index].prevout.write(&mut data).unwrap();
CompactSize::write(&mut data, precondition.extension_id.try_into().unwrap())
.unwrap();
CompactSize::write(&mut data, precondition.mode.try_into().unwrap()).unwrap();
Vector::write(&mut data, &precondition.payload, |w, e| w.write_u8(*e)).unwrap();
data.extend_from_slice(&value.to_i64_le_bytes());
h.update(&data);
}
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
SignableInput::Tze { .. } => { SignableInput::Tze { .. } => {
panic!("A request has been made to sign a TZE input, but the signature hash version is not ZFuture"); panic!("A request has been made to sign a TZE input in a V4 transaction.");
} }
_ => (), SignableInput::Shielded => (),
} }
h.finalize().as_ref().to_vec() h.finalize().as_ref().to_vec()

View File

@ -1,14 +1,9 @@
use ff::Field;
use rand_core::OsRng;
use proptest::prelude::*; use proptest::prelude::*;
use crate::{constants::SPENDING_KEY_GENERATOR, sapling::redjubjub::PrivateKey};
use super::{ use super::{
components::Amount, components::Amount,
sighash::{signature_hash, SignableInput}, sighash::{signature_hash, SignableInput},
Transaction, TransactionData, Transaction,
}; };
use super::testing::{arb_branch_id, arb_tx}; use super::testing::{arb_branch_id, arb_tx};
@ -27,49 +22,6 @@ fn tx_read_write() {
assert_eq!(&data[..], &encoded[..]); assert_eq!(&data[..], &encoded[..]);
} }
#[test]
fn tx_write_rejects_unexpected_joinsplit_pubkey() {
// Succeeds without a JoinSplit pubkey
assert!(TransactionData::new().freeze().is_ok());
// Fails with an unexpected JoinSplit pubkey
{
let mut tx = TransactionData::new();
tx.joinsplit_pubkey = Some([0; 32]);
assert!(tx.freeze().is_err());
}
}
#[test]
fn tx_write_rejects_unexpected_joinsplit_sig() {
// Succeeds without a JoinSplit signature
assert!(TransactionData::new().freeze().is_ok());
// Fails with an unexpected JoinSplit signature
{
let mut tx = TransactionData::new();
tx.joinsplit_sig = Some([0; 64]);
assert!(tx.freeze().is_err());
}
}
#[test]
fn tx_write_rejects_unexpected_binding_sig() {
// Succeeds without a binding signature
assert!(TransactionData::new().freeze().is_ok());
// Fails with an unexpected binding signature
{
let mut rng = OsRng;
let sk = PrivateKey(jubjub::Fr::random(&mut rng));
let sig = sk.sign(b"Foo bar", &mut rng, SPENDING_KEY_GENERATOR);
let mut tx = TransactionData::new();
tx.binding_sig = Some(sig);
assert!(tx.freeze().is_err());
}
}
proptest! { proptest! {
#[test] #[test]
fn tx_serialization_roundtrip(tx in arb_branch_id().prop_flat_map(arb_tx)) { fn tx_serialization_roundtrip(tx in arb_branch_id().prop_flat_map(arb_tx)) {
@ -78,43 +30,12 @@ proptest! {
let txo = Transaction::read(&txn_bytes[..]).unwrap(); let txo = Transaction::read(&txn_bytes[..]).unwrap();
assert_eq!(tx.version, txo.version); prop_assert_eq!(tx.version, txo.version);
assert_eq!(tx.vin, txo.vin); prop_assert_eq!(tx.lock_time, txo.lock_time);
assert_eq!(tx.vout, txo.vout); prop_assert_eq!(tx.transparent_bundle.as_ref(), txo.transparent_bundle.as_ref());
prop_assert_eq!(tx.sapling_value_balance(), txo.sapling_value_balance());
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
assert_eq!(tx.tze_inputs, txo.tze_inputs); prop_assert_eq!(tx.tze_bundle.as_ref(), txo.tze_bundle.as_ref());
#[cfg(feature = "zfuture")]
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]
#[cfg(feature = "zfuture")]
fn test_tze_tx_parse() {
let txn_bytes = vec![
0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0x52, 0x52, 0x52, 0x52,
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52,
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x30, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x20, 0xd9, 0x81, 0x80, 0x87, 0xde, 0x72, 0x44, 0xab, 0xc1, 0xb5, 0xfc,
0xf2, 0x8e, 0x55, 0xe4, 0x2c, 0x7f, 0xf9, 0xc6, 0x78, 0xc0, 0x60, 0x51, 0x81, 0xf3, 0x7a,
0xc5, 0xd7, 0x41, 0x4a, 0x7b, 0x95, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let tx = Transaction::read(&txn_bytes[..]);
match tx {
Ok(tx) => assert!(!tx.tze_inputs.is_empty()),
Err(e) => panic!(
"An error occurred parsing a serialized TZE transaction: {}",
e
),
} }
} }
@ -133,7 +54,7 @@ fn zip_0143() {
}; };
assert_eq!( assert_eq!(
signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input), signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input).as_ref(),
tv.sighash tv.sighash
); );
} }
@ -153,7 +74,7 @@ fn zip_0243() {
}; };
assert_eq!( assert_eq!(
signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input), signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input).as_ref(),
tv.sighash tv.sighash
); );
} }