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) => {
let script = addr.script();
tx.vout
.iter()
.enumerate()
.find(|(_, tx_out)| tx_out.script_pubkey == script)
tx.transparent_bundle
.as_ref()
.and_then(|b| {
b.vout
.iter()
.enumerate()
.find(|(_, tx_out)| tx_out.script_pubkey == script)
})
.map(|(index, _)| index)
.expect("we sent to this address")
}

View File

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

View File

@ -9,7 +9,7 @@ use zcash_primitives::{
consensus::BlockHeight,
sapling::Nullifier,
transaction::{
components::sapling::{CompactOutputDescription, OutputDescription},
components::sapling::{self, CompactOutputDescription, OutputDescription},
TxId,
},
};
@ -113,8 +113,8 @@ impl compact_formats::CompactOutput {
}
}
impl From<OutputDescription> for compact_formats::CompactOutput {
fn from(out: OutputDescription) -> compact_formats::CompactOutput {
impl<A: sapling::Authorization> From<OutputDescription<A>> for compact_formats::CompactOutput {
fn from(out: OutputDescription<A>) -> compact_formats::CompactOutput {
let mut result = compact_formats::CompactOutput::new();
result.set_cmu(out.cmu.to_repr().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
// reasonable assumption for a light client such as a mobile phone.
for spend in &sent_tx.tx.shielded_spends {
wallet::mark_spent(up, tx_ref, &spend.nullifier)?;
if let Some(bundle) = sent_tx.tx.sapling_bundle.as_ref() {
for spend in &bundle.shielded_spends {
wallet::mark_spent(up, tx_ref, &spend.nullifier)?;
}
}
wallet::insert_sent_note(

View File

@ -631,7 +631,8 @@ mod tests {
)
.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(
&network,

View File

@ -17,3 +17,9 @@ ff = "0.9"
jubjub = "0.6"
rand_core = "0.6"
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 zcash_primitives::consensus::{BlockHeight, BranchId};
use zcash_primitives::extensions::transparent::{Error, Extension, Precondition, Witness};
use zcash_primitives::transaction::{components::TzeOut, Transaction};
use zcash_primitives::extensions::transparent::{
AuthData, Error, Extension, Precondition, Witness,
};
use zcash_primitives::transaction::{components::tze::TzeOut, Transaction};
use crate::transparent::demo;
@ -67,7 +69,7 @@ pub trait Epoch {
fn verify<'a>(
&self,
precondition: &Precondition,
witness: &Witness,
witness: &Witness<AuthData>,
ctx: &Context<'a>,
) -> Result<(), Error<Self::Error>>;
}
@ -76,15 +78,18 @@ pub trait Epoch {
/// by the context.
impl<'a> demo::Context for Context<'a> {
fn is_tze_only(&self) -> bool {
self.tx.vin.is_empty()
&& self.tx.vout.is_empty()
&& self.tx.shielded_spends.is_empty()
&& self.tx.shielded_outputs.is_empty()
&& self.tx.joinsplits.is_empty()
self.tx.transparent_bundle.is_none()
&& self.tx.sapling_bundle.is_none()
&& self.tx.sprout_bundle.is_none()
&& self.tx.orchard_bundle.is_none()
}
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>(
&self,
precondition: &Precondition,
witness: &Witness,
witness: &Witness<AuthData>,
ctx: &Context<'a>,
) -> Result<(), Error<Self::Error>> {
let ext_id = ExtensionId::try_from(precondition.extension_id)

View File

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

View File

@ -12,7 +12,7 @@ and this library adheres to Rust's notion of
sent.
- `zcash_primitives::transaction::Txid::{read, write, from_bytes}`
- `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.
- `zcash_primitives::consensus::NetworkUpgrade::Nu5` value representing the Nu5 upgrade.
- `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.
- `transparent::builder` for transparent 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
- 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 now selects the correct consensus branch ID for the given target
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
### Added
@ -150,7 +192,7 @@ and this library adheres to Rust's notion of
- `try_sapling_output_recovery`
- `try_sapling_output_recovery_with_ock`
- `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.
- `zcash_primitives::primitives::ViewingKey::ivk` now returns `SaplingIvk`
- `zcash_primitives::primitives::Note::nf` now returns `Nullifier`.

View File

@ -9,7 +9,10 @@ use zcash_primitives::{
util::generate_random_rseed,
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) {
@ -20,7 +23,7 @@ fn bench_note_decryption(c: &mut Criterion) {
let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
// 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 pk_d = diversifier.g_d().unwrap() * valid_ivk.0;
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
# 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.
///
/// [`signature_hash`]: crate::transaction::signature_hash
/// [`signature_hash`]: crate::transaction::sighash::signature_hash
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BranchId {
/// The consensus rules at the launch of Zcash.
@ -554,6 +554,10 @@ impl BranchId {
.map(|lower| (lower, None)),
}
}
pub fn sprout_uses_groth_proofs(&self) -> bool {
!matches!(self, BranchId::Sprout | BranchId::Overwinter)
}
}
#[cfg(any(test, feature = "test-dependencies"))]

View File

@ -1,8 +1,16 @@
//! Core traits and structs for Transparent Zcash Extensions.
use crate::transaction::components::{Amount, TzeOut, TzeOutPoint};
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.
///
/// 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`
/// value.
#[derive(Clone, Debug, PartialEq)]
pub struct Witness {
pub struct Witness<T> {
pub extension_id: 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
/// 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();
Witness {
extension_id,
mode,
payload,
payload: AuthData(payload),
}
}
/// Attempt to parse an extension-specific witness value from the
/// intermediate representation.
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(
&self,
precondition: &Precondition,
witness: &Witness,
witness: &Witness<AuthData>,
context: &C,
) -> Result<(), Self::Error>
where
@ -146,7 +154,7 @@ pub trait Extension<C> {
{
self.verify_inner(
&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,
)
}
@ -178,7 +186,7 @@ pub trait ExtensionTxBuilder<'a> {
&mut self,
extension_id: u32,
mode: u32,
prevout: (TzeOutPoint, TzeOut),
prevout: (tze::OutPoint, TzeOut),
witness_builder: WBuilder,
) -> Result<(), Self::BuildError>
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
/// commitment tree.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct CommitmentTree<Node: Hashable> {
left: 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},
memo::MemoBytes,
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";
@ -383,7 +386,7 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
params: &P,
height: BlockHeight,
ock: &OutgoingCipherKey,
output: &OutputDescription,
output: &OutputDescription<sapling::Authorized>,
) -> Option<(Note, PaymentAddress, MemoBytes)> {
let domain = SaplingDomain {
params: params.clone(),
@ -405,7 +408,7 @@ pub fn try_sapling_output_recovery<P: consensus::Parameters>(
params: &P,
height: BlockHeight,
ovk: &OutgoingViewingKey,
output: &OutputDescription,
output: &OutputDescription<sapling::Authorized>,
) -> Option<(Note, PaymentAddress, MemoBytes)> {
let domain = SaplingDomain {
params: params.clone(),
@ -450,7 +453,7 @@ mod tests {
},
transaction::components::{
amount::Amount,
sapling::{CompactOutputDescription, OutputDescription},
sapling::{self, CompactOutputDescription, OutputDescription},
GROTH_PROOF_SIZE,
},
};
@ -462,7 +465,7 @@ mod tests {
OutgoingViewingKey,
OutgoingCipherKey,
SaplingIvk,
OutputDescription,
OutputDescription<sapling::Authorized>,
) {
let ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
@ -492,7 +495,11 @@ mod tests {
height: BlockHeight,
ivk: &SaplingIvk,
mut rng: &mut R,
) -> (OutgoingViewingKey, OutgoingCipherKey, OutputDescription) {
) -> (
OutgoingViewingKey,
OutgoingCipherKey,
OutputDescription<sapling::Authorized>,
) {
let diversifier = Diversifier([0; 11]);
let pk_d = diversifier.g_d().unwrap() * ivk.0;
let pa = PaymentAddress::from_parts_unchecked(diversifier, pk_d);

View File

@ -1,11 +1,13 @@
//! Structs for building transactions.
use core::array;
use rand::{rngs::OsRng, CryptoRng, RngCore};
use std::array;
use std::error;
use std::fmt;
use std::io;
use std::sync::mpsc::Sender;
use rand::{rngs::OsRng, CryptoRng, RngCore};
use orchard::bundle::{self as orchard};
use crate::{
consensus::{self, BlockHeight, BranchId},
@ -13,15 +15,20 @@ use crate::{
memo::MemoBytes,
merkle_tree::MerklePath,
sapling::{
keys::OutgoingViewingKey, prover::TxProver, Diversifier, Node, Note, PaymentAddress,
keys::OutgoingViewingKey, prover::TxProver, redjubjub, Diversifier, Node, Note,
PaymentAddress,
},
transaction::{
components::{
amount::{Amount, DEFAULT_FEE},
sapling::builder::{self as sapling, SaplingBuilder, SaplingMetadata},
transparent::builder::{self as transparent, TransparentBuilder},
sapling::{
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,
};
@ -30,14 +37,14 @@ use crate::{
use std::marker::PhantomData;
#[cfg(feature = "transparent-inputs")]
use crate::transaction::components::{OutPoint, TxOut};
use crate::{legacy::Script, transaction::components::transparent::TxOut};
#[cfg(feature = "zfuture")]
use crate::{
extensions::transparent::{ExtensionTxBuilder, ToPayload},
extensions::transparent::{AuthData, ExtensionTxBuilder, ToPayload},
transaction::components::{
tze::builder::{self as tzebuilder, TzeBuilder},
TzeOut, TzeOutPoint,
tze::builder::TzeBuilder,
tze::{self, TzeOut},
},
};
@ -51,10 +58,10 @@ pub enum Error {
ChangeIsNegative(Amount),
InvalidAmount,
NoChangeAddress,
TransparentBuild(transparent::Error),
SaplingBuild(sapling::Error),
TransparentBuild(transparent::builder::Error),
SaplingBuild(sapling::builder::Error),
#[cfg(feature = "zfuture")]
TzeBuild(tzebuilder::Error),
TzeBuild(tze::builder::Error),
}
impl fmt::Display for Error {
@ -109,7 +116,7 @@ enum ChangeAddress {
}
/// 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,
rng: R,
target_height: BlockHeight,
@ -119,7 +126,7 @@ pub struct Builder<'a, P: consensus::Parameters, R: RngCore> {
sapling_builder: SaplingBuilder<P>,
change_address: Option<ChangeAddress>,
#[cfg(feature = "zfuture")]
tze_builder: TzeBuilder<'a, TransactionData>,
tze_builder: TzeBuilder<'a, TransactionData<Unauthorized>>,
#[cfg(not(feature = "zfuture"))]
tze_builder: PhantomData<&'a ()>,
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,
fee: DEFAULT_FEE,
transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::empty(params, target_height),
sapling_builder: SaplingBuilder::new(params, target_height),
change_address: None,
#[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::empty(),
@ -213,7 +220,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
pub fn add_transparent_input(
&mut self,
sk: secp256k1::SecretKey,
utxo: OutPoint,
utxo: transparent::OutPoint,
coin: TxOut,
) -> Result<(), Error> {
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 (spend_descs, output_descs, tx_metadata) = self
let (sapling_bundle, tx_metadata) = self
.sapling_builder
.build(
prover,
@ -331,25 +338,18 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
.map_err(Error::SaplingBuild)?;
#[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,
vin,
vout,
#[cfg(feature = "zfuture")]
tze_inputs,
#[cfg(feature = "zfuture")]
tze_outputs,
lock_time: 0,
expiry_height: self.expiry_height,
value_balance: self.sapling_builder.value_balance(),
shielded_spends: spend_descs,
shielded_outputs: output_descs,
joinsplits: vec![],
joinsplit_pubkey: None,
joinsplit_sig: None,
binding_sig: None,
transparent_bundle,
sprout_bundle: None,
sapling_bundle,
orchard_bundle: None,
#[cfg(feature = "zfuture")]
tze_bundle,
};
//
@ -358,68 +358,126 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
let mut sighash = [0u8; 32];
sighash.copy_from_slice(&signature_hash_data(
&mtx,
&unauthed_tx,
consensus_branch_id,
SIGHASH_ALL,
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
.create_signatures(prover, &mut ctx, &mut self.rng, &sighash, &tx_metadata)
.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")]
{
// Create TZE input witnesses
let tze_payloads = self
.tze_builder
.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;
}
}
let tze_witnesses = self
.tze_builder
.create_witnesses(&unauthed_tx)
.map_err(Error::TzeBuild)?;
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,
))
}
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")]
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a>
for Builder<'a, P, R>
{
type BuildCtx = TransactionData;
type BuildError = tzebuilder::Error;
type BuildCtx = TransactionData<Unauthorized>;
type BuildError = tze::builder::Error;
fn add_tze_input<WBuilder, W: ToPayload>(
&mut self,
extension_id: u32,
mode: u32,
prevout: (TzeOutPoint, TzeOut),
prevout: (tze::OutPoint, TzeOut),
witness_builder: WBuilder,
) -> Result<(), Self::BuildError>
where
WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result<W, tzebuilder::Error>),
WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result<W, tze::builder::Error>),
{
self.tze_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)
}
/// 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> {
self.build(&MockTxProver)
}
@ -496,7 +529,7 @@ mod tests {
sapling::{prover::mock::MockTxProver, Node, Rseed},
transaction::components::{
amount::{Amount, DEFAULT_FEE},
sapling::builder::{self as sapling},
sapling::builder::{self as build_s},
transparent::builder::{self as transparent},
},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
@ -524,7 +557,7 @@ mod tests {
let mut builder = Builder::new(TEST_NETWORK, sapling_activation_height);
assert_eq!(
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,
fee: Amount::zero(),
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,
#[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::empty(),
@ -561,7 +594,7 @@ mod tests {
let (tx, _) = builder.build(&MockTxProver).unwrap();
// No binding signature, because only t input and outputs
assert!(tx.binding_sig.is_none());
assert!(tx.sapling_bundle.is_none());
}
#[test]
@ -598,7 +631,7 @@ mod tests {
// that a binding signature was attempted
assert_eq!(
builder.build(&MockTxProver),
Err(Error::SaplingBuild(sapling::Error::BindingSig))
Err(Error::SaplingBuild(build_s::Error::BindingSig))
);
}
@ -748,7 +781,7 @@ mod tests {
.unwrap();
assert_eq!(
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.
pub mod amount;
pub mod orchard;
pub mod sapling;
pub mod sprout;
pub mod transparent;
@ -13,7 +14,7 @@ pub use self::{
};
#[cfg(feature = "zfuture")]
pub use self::tze::{TzeIn, TzeOut, TzeOutPoint};
pub use self::tze::{TzeIn, TzeOut};
// π_A + π_B + π_C
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 group::GroupEncoding;
use std::io::{self, Read, Write};
use zcash_note_encryption::ShieldedOutput;
use zcash_note_encryption::{ShieldedOutput, COMPACT_NOTE_SIZE};
use crate::{
consensus,
sapling::{
note_encryption::SaplingDomain,
redjubjub::{PublicKey, Signature},
redjubjub::{self, PublicKey, Signature},
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 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)]
pub struct SpendDescription {
pub struct SpendDescription<A: Authorization> {
pub cv: jubjub::ExtendedPoint,
pub anchor: bls12_381::Scalar,
pub nullifier: Nullifier,
pub rk: PublicKey,
pub zkproof: [u8; GROTH_PROOF_SIZE],
pub spend_auth_sig: Option<Signature>,
pub zkproof: A::Proof,
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> {
write!(
f,
@ -40,49 +117,84 @@ impl std::fmt::Debug for SpendDescription {
}
}
impl SpendDescription {
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
// Consensus rules (§4.4):
// - Canonical encoding is enforced here.
// - "Not small order" is enforced in SaplingVerificationContext::check_spend()
// (located in zcash_proofs::sapling::verifier).
let cv = {
let mut bytes = [0u8; 32];
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 rules (§4.4) & (§4.5):
/// - Canonical encoding is enforced here.
/// - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output())
/// (located in zcash_proofs::sapling::verifier).
pub fn read_point<R: Read>(mut reader: R, field: &str) -> io::Result<jubjub::ExtendedPoint> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
let point = jubjub::ExtendedPoint::from_bytes(&bytes);
// Consensus rule (§7.3): Canonical encoding is enforced here
let anchor = {
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, "anchor not in field"))?
};
if point.is_none().into() {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid {}", 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]);
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.
// - "Not small order" is enforced in SaplingVerificationContext::check_spend()
let rk = PublicKey::read(&mut reader)?;
// Consensus rules (§4.4):
// - Canonical encoding is enforced by the API of SaplingVerificationContext::check_spend()
// due to the need to parse this into a bellman::groth16::Proof.
// - Proof validity is enforced in SaplingVerificationContext::check_spend()
let mut zkproof = [0u8; GROTH_PROOF_SIZE];
reader.read_exact(&mut zkproof)?;
// 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)?);
// - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output())
// (located in zcash_proofs::sapling::verifier).
let cv = read_point(&mut reader, "cv")?;
// Consensus rules (§7.3) & (§7.4):
// - Canonical encoding is enforced here
let anchor = read_base(&mut reader, "anchor")?;
let nullifier = Self::read_nullifier(&mut reader)?;
let rk = Self::read_rk(&mut reader)?;
let zkproof = read_zkproof(&mut reader)?;
let spend_auth_sig = Self::read_spend_auth_sig(&mut reader)?;
Ok(SpendDescription {
cv,
@ -100,27 +212,23 @@ impl SpendDescription {
writer.write_all(&self.nullifier.0)?;
self.rk.write(&mut writer)?;
writer.write_all(&self.zkproof)?;
match self.spend_auth_sig {
Some(sig) => sig.write(&mut writer),
None => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Missing spend auth signature",
)),
}
self.spend_auth_sig.write(&mut writer)
}
}
#[derive(Clone)]
pub struct OutputDescription {
pub struct OutputDescription<A: Authorization> {
pub cv: jubjub::ExtendedPoint,
pub cmu: bls12_381::Scalar,
pub ephemeral_key: jubjub::ExtendedPoint,
pub enc_ciphertext: [u8; 580],
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 {
&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> {
write!(
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> {
// Consensus rules (§4.5):
// - Canonical encoding is enforced here.
@ -205,7 +313,9 @@ impl OutputDescription {
zkproof,
})
}
}
impl<A: Authorization<Proof = GrothProofBytes>> OutputDescription<A> {
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(&self.cv.to_bytes())?;
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 epk: jubjub::ExtendedPoint,
pub cmu: bls12_381::Scalar,
pub enc_ciphertext: Vec<u8>,
}
impl From<OutputDescription> for CompactOutputDescription {
fn from(out: OutputDescription) -> CompactOutputDescription {
impl<A: Authorization> From<OutputDescription<A>> for CompactOutputDescription {
fn from(out: OutputDescription<A>) -> CompactOutputDescription {
CompactOutputDescription {
epk: out.ephemeral_key,
cmu: out.cmu,

View File

@ -1,11 +1,10 @@
//! Types and functions for building Sapling transaction components.
use std::fmt;
use std::marker::PhantomData;
use std::sync::mpsc::Sender;
use ff::Field;
use rand::{seq::SliceRandom, CryptoRng, RngCore};
use rand::{seq::SliceRandom, RngCore};
use crate::{
consensus::{self, BlockHeight},
@ -22,7 +21,10 @@ use crate::{
},
transaction::{
builder::Progress,
components::{amount::Amount, OutputDescription, SpendDescription},
components::{
amount::Amount,
sapling::{Bundle, OutputDescription, SpendDescription, Unauthorized},
},
},
zip32::ExtendedSpendingKey,
};
@ -63,32 +65,19 @@ struct SpendDescriptionInfo {
}
#[derive(Clone)]
pub struct SaplingOutput<P: consensus::Parameters> {
struct SaplingOutput {
/// `None` represents the `ovk = ⊥` case.
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
note: Note,
memo: MemoBytes,
_params: PhantomData<P>,
}
impl<P: consensus::Parameters> SaplingOutput<P> {
pub fn new<R: RngCore + CryptoRng>(
impl SaplingOutput {
fn new_internal<P: consensus::Parameters, R: RngCore>(
params: &P,
target_height: BlockHeight,
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,
rng: &mut R,
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
value: Amount,
@ -113,25 +102,15 @@ impl<P: consensus::Parameters> SaplingOutput<P> {
to,
note,
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,
prover: &Pr,
ctx: &mut Pr::SaplingProvingContext,
rng: &mut R,
) -> OutputDescription {
self.build_internal(prover, ctx, rng)
}
fn build_internal<Pr: TxProver, R: RngCore>(
self,
prover: &Pr,
ctx: &mut Pr::SaplingProvingContext,
rng: &mut R,
) -> OutputDescription {
) -> OutputDescription<Unauthorized> {
let encryptor = sapling_note_encryption::<R, P>(
self.ovk,
self.note.clone(),
@ -204,17 +183,17 @@ impl SaplingMetadata {
}
}
pub struct SaplingBuilder<P: consensus::Parameters> {
pub struct SaplingBuilder<P> {
params: P,
anchor: Option<bls12_381::Scalar>,
target_height: BlockHeight,
value_balance: Amount,
spends: Vec<SpendDescriptionInfo>,
outputs: Vec<SaplingOutput<P>>,
outputs: Vec<SaplingOutput>,
}
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 {
params,
anchor: None,
@ -280,8 +259,8 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
) -> Result<(), Error> {
let output = SaplingOutput::new_internal(
&self.params,
self.target_height,
&mut rng,
self.target_height,
ovk,
to,
value,
@ -311,14 +290,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
mut rng: R,
target_height: BlockHeight,
progress_notifier: Option<&Sender<Progress>>,
) -> Result<
(
Vec<SpendDescription>,
Vec<OutputDescription>,
SaplingMetadata,
),
Error,
> {
) -> Result<(Option<Bundle<Unauthorized>>, SaplingMetadata), Error> {
// Record initial positions of spends and outputs
let mut indexed_spends: Vec<_> = self.spends.iter().enumerate().collect();
let mut indexed_outputs: Vec<_> = self
@ -350,7 +322,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
let mut progress = 0u32;
// 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
.anchor
.expect("Sapling anchor must be set if Sapling spends are present.");
@ -397,7 +369,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
nullifier,
rk,
zkproof,
spend_auth_sig: None,
spend_auth_sig: (),
})
})
.collect::<Result<Vec<_>, Error>>()?
@ -406,7 +378,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
};
// Create Sapling OutputDescriptions
let output_descs = indexed_outputs
let shielded_outputs: Vec<OutputDescription<Unauthorized>> = indexed_outputs
.into_iter()
.enumerate()
.map(|(i, output)| {
@ -414,7 +386,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
// Record the post-randomized output location
tx_metadata.output_indices[pos] = i;
output.clone().build_internal(prover, ctx, &mut rng)
output.clone().build::<P, _, _>(prover, ctx, &mut rng)
} else {
// This is a dummy output
let (dummy_to, dummy_note) = {
@ -491,7 +463,18 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
})
.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>(
@ -501,8 +484,9 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
rng: &mut R,
sighash_bytes: &[u8; 32],
tx_metadata: &SaplingMetadata,
) -> Result<(Vec<Option<Signature>>, Option<Signature>), Error> {
// Create Sapling spendAuth and binding signatures
) -> Result<Option<(Vec<Signature>, Signature)>, Error> {
// 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()];
for (i, spend) in self.spends.into_iter().enumerate() {
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
let binding_sig =
if tx_metadata.spend_indices.is_empty() && tx_metadata.output_indices.is_empty() {
None
} else {
Some(
prover
.binding_sig(ctx, self.value_balance, &sighash_bytes)
.map_err(|_| Error::BindingSig)?,
)
};
if tx_metadata.spend_indices.is_empty() && tx_metadata.output_indices.is_empty() {
Ok(None)
} else {
let spend_sigs = spend_sigs
.into_iter()
.collect::<Option<Vec<Signature>>>()
.unwrap_or_default();
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_OUTPUTS: usize = 2;
#[derive(Debug)]
pub struct Bundle {
pub joinsplits: Vec<JsDescription>,
pub joinsplit_pubkey: [u8; 32],
pub joinsplit_sig: [u8; 64],
}
#[derive(Clone)]
pub(crate) enum SproutProof {
Groth([u8; GROTH_PROOF_SIZE]),

View File

@ -2,6 +2,7 @@
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::fmt::Debug;
use std::io::{self, Read, Write};
use crate::legacy::Script;
@ -10,6 +11,49 @@ use super::amount::Amount;
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)]
pub struct OutPoint {
hash: [u8; 32],
@ -43,23 +87,25 @@ impl OutPoint {
}
#[derive(Debug, Clone, PartialEq)]
pub struct TxIn {
pub struct TxIn<A: Authorization> {
pub prevout: OutPoint,
pub script_sig: Script,
pub script_sig: A::ScriptSig,
pub sequence: u32,
}
impl TxIn {
impl TxIn<Unauthorized> {
#[cfg(feature = "transparent-inputs")]
#[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))]
pub fn new(prevout: OutPoint) -> Self {
TxIn {
prevout,
script_sig: Script::default(),
script_sig: (),
sequence: std::u32::MAX,
}
}
}
impl TxIn<Authorized> {
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
let prevout = OutPoint::read(&mut reader)?;
let script_sig = Script::read(&mut reader)?;
@ -106,3 +152,66 @@ impl TxOut {
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::{
legacy::TransparentAddress,
transaction::components::{amount::Amount, TxIn, TxOut},
transaction::components::{
amount::Amount,
transparent::{self, TxIn, TxOut},
},
};
#[cfg(feature = "transparent-inputs")]
@ -13,7 +16,7 @@ use crate::{
legacy::Script,
transaction::{
components::OutPoint, sighash::signature_hash_data, SignableInput, TransactionData,
SIGHASH_ALL,
Unauthorized, SIGHASH_ALL,
},
};
@ -128,48 +131,66 @@ impl TransparentBuilder {
.sum::<Option<Amount>>()?
}
pub fn build(&self) -> (Vec<TxIn>, Vec<TxOut>) {
pub fn build(&self) -> Option<transparent::Bundle<transparent::Unauthorized>> {
#[cfg(feature = "transparent-inputs")]
let vin = self
let vin: Vec<TxIn<transparent::Unauthorized>> = self
.inputs
.iter()
.map(|i| TxIn::new(i.utxo.clone()))
.collect();
#[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")]
pub fn create_signatures(
self,
mtx: &TransactionData,
mtx: &TransactionData<Unauthorized>,
consensus_branch_id: consensus::BranchId,
) -> Vec<Script> {
self.inputs
.iter()
.enumerate()
.map(|(i, info)| {
let mut sighash = [0u8; 32];
sighash.copy_from_slice(&signature_hash_data(
mtx,
consensus_branch_id,
SIGHASH_ALL,
SignableInput::transparent(i, &info.coin.script_pubkey, info.coin.value),
));
) -> Option<Vec<Script>> {
if self.inputs.is_empty() && self.vout.is_empty() {
None
} else {
Some(
self.inputs
.iter()
.enumerate()
.map(|(i, info)| {
let mut sighash = [0u8; 32];
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 sig = self.secp.sign(&msg, &info.sk);
let msg =
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
let mut sig_bytes: Vec<u8> = sig.serialize_der()[..].to_vec();
sig_bytes.extend(&[SIGHASH_ALL as u8]);
// Signature has to have "SIGHASH_ALL" appended to it
let mut sig_bytes: Vec<u8> = sig.serialize_der()[..].to_vec();
sig_bytes.extend(&[SIGHASH_ALL as u8]);
// P2PKH scriptSig
Script::default() << &sig_bytes[..] << &info.pubkey[..]
})
.collect()
// P2PKH scriptSig
Script::default() << &sig_bytes[..] << &info.pubkey[..]
})
.collect(),
)
}
}
}

View File

@ -3,9 +3,9 @@
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::io::{self, Read, Write};
use std::convert::TryFrom;
use std::fmt::Debug;
use std::io::{self, Read, Write};
use crate::{
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")
}
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)]
pub struct TzeOutPoint {
pub struct OutPoint {
txid: TxId,
n: u32,
}
impl TzeOutPoint {
impl OutPoint {
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> {
let txid = TxId::read(&mut reader)?;
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<()> {
@ -53,48 +99,12 @@ impl TzeOutPoint {
}
#[derive(Clone, Debug, PartialEq)]
pub struct TzeIn {
pub prevout: TzeOutPoint,
pub witness: tze::Witness,
pub struct TzeIn<A: Authorization> {
pub prevout: OutPoint,
pub witness: tze::Witness<A::Witness>,
}
/// Transaction encoding and decoding functions conforming to [ZIP 222].
///
/// [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,
},
})
}
impl<A: Authorization> TzeIn<A> {
/// Write without witness data (for signature hashing)
///
/// 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)?,
)
}
}
/// 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.
///
@ -122,7 +172,7 @@ impl TzeIn {
/// [`write_without_witness`]: TzeIn::write_without_witness
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
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::{
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)]
@ -32,16 +35,16 @@ struct TzeSigner<'a, BuildCtx> {
pub struct TzeBuilder<'a, BuildCtx> {
signers: Vec<TzeSigner<'a, BuildCtx>>,
tze_inputs: Vec<TzeIn>,
tze_outputs: Vec<TzeOut>,
vin: Vec<TzeIn<Unauthorized>>,
vout: Vec<TzeOut>,
}
impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> {
pub fn empty() -> Self {
TzeBuilder {
signers: vec![],
tze_inputs: vec![],
tze_outputs: vec![],
vin: vec![],
vout: vec![],
}
}
@ -49,13 +52,12 @@ impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> {
&mut self,
extension_id: u32,
mode: u32,
(outpoint, prevout): (TzeOutPoint, TzeOut),
(outpoint, prevout): (OutPoint, TzeOut),
witness_builder: WBuilder,
) where
WBuilder: 'a + FnOnce(&BuildCtx) -> Result<W, Error>,
{
self.tze_inputs
.push(TzeIn::new(outpoint, extension_id, mode));
self.vin.push(TzeIn::new(outpoint, extension_id, mode));
self.signers.push(TzeSigner {
prevout,
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();
self.tze_outputs.push(TzeOut {
self.vout.push(TzeOut {
value,
precondition: tze::Precondition {
extension_id,
@ -91,36 +93,49 @@ impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> {
.map(|s| s.prevout.value)
.sum::<Option<Amount>>()?
- self
.tze_outputs
.vout
.iter()
.map(|tzo| tzo.value)
.sum::<Option<Amount>>()?
}
pub fn build(&self) -> (Vec<TzeIn>, Vec<TzeOut>) {
(self.tze_inputs.clone(), self.tze_outputs.clone())
pub fn build(&self) -> Option<Bundle<Unauthorized>> {
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
let payloads = self
.signers
.into_iter()
.zip(self.tze_inputs.into_iter())
.map(|(signer, tzein)| {
// The witness builder function should have cached/closed over whatever data was
// 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));
}
if self.vin.is_empty() && self.vout.is_empty() {
Ok(None)
} else {
// Create TZE input witnesses
let payloads = self
.signers
.into_iter()
.zip(self.vin.into_iter())
.into_iter()
.map(|(signer, tzein)| {
// The witness builder function should have cached/closed over whatever data was
// 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)
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(tze::AuthData(payload))
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(payloads)
Ok(Some(payloads))
}
}
}

View File

@ -7,32 +7,40 @@ use std::ops::Deref;
use crate::{
consensus::{BlockHeight, BranchId},
sapling::redjubjub::Signature,
serialize::Vector,
sapling::redjubjub,
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 components;
mod sighash;
pub mod sighash;
pub mod util;
#[cfg(test)]
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_TX_VERSION: u32 = 3;
const SAPLING_VERSION_GROUP_ID: u32 = 0x892F2085;
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
/// serialization, and will never be active under the consensus rules.
/// When new consensus transaction versions are added, all call sites
@ -90,6 +98,7 @@ pub enum TxVersion {
Sprout(u32),
Overwinter,
Sapling,
ZcashTxV5,
#[cfg(feature = "zfuture")]
ZFuture,
}
@ -133,6 +142,7 @@ impl TxVersion {
TxVersion::Sprout(v) => *v,
TxVersion::Overwinter => OVERWINTER_TX_VERSION,
TxVersion::Sapling => SAPLING_TX_VERSION,
TxVersion::ZcashTxV5 => V5_TX_VERSION,
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => ZFUTURE_TX_VERSION,
}
@ -143,6 +153,7 @@ impl TxVersion {
TxVersion::Sprout(_) => 0,
TxVersion::Overwinter => OVERWINTER_VERSION_GROUP_ID,
TxVersion::Sapling => SAPLING_VERSION_GROUP_ID,
TxVersion::ZcashTxV5 => V5_VERSION_GROUP_ID,
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => ZFUTURE_VERSION_GROUP_ID,
}
@ -160,15 +171,17 @@ impl TxVersion {
match self {
TxVersion::Sprout(v) => *v >= 2u32,
TxVersion::Overwinter | TxVersion::Sapling => true,
TxVersion::ZcashTxV5 => false,
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => true,
}
}
pub fn uses_groth_proofs(&self) -> bool {
pub fn has_sapling(&self) -> bool {
match self {
TxVersion::Sprout(_) | TxVersion::Overwinter => false,
TxVersion::Sapling => true,
TxVersion::ZcashTxV5 => true,
#[cfg(feature = "zfuture")]
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.
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Transaction {
txid: TxId,
data: TransactionData,
data: TransactionData<Authorized>,
}
impl Deref for Transaction {
type Target = TransactionData;
type Target = TransactionData<Authorized>;
fn deref(&self) -> &TransactionData {
fn deref(&self) -> &TransactionData<Authorized> {
&self.data
}
}
@ -209,95 +254,107 @@ impl PartialEq for Transaction {
}
}
#[derive(Clone)]
pub struct TransactionData {
pub struct TransactionData<A: Authorization> {
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 expiry_height: BlockHeight,
pub value_balance: Amount,
pub shielded_spends: Vec<SpendDescription>,
pub shielded_outputs: Vec<OutputDescription>,
pub joinsplits: Vec<JsDescription>,
pub joinsplit_pubkey: Option<[u8; 32]>,
pub joinsplit_sig: Option<[u8; 64]>,
pub binding_sig: Option<Signature>,
pub transparent_bundle: Option<transparent::Bundle<A::TransparentAuth>>,
pub sprout_bundle: Option<sprout::Bundle>,
pub sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>,
pub orchard_bundle: Option<orchard::bundle::Bundle<A::OrchardAuth, Amount>>,
#[cfg(feature = "zfuture")]
pub tze_bundle: Option<tze::Bundle<A::TzeAuth>>,
}
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> {
write!(
f,
"TransactionData(
version = {:?},
vin = {:?},
vout = {:?},{}
lock_time = {:?},
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 = {:?},
shielded_spends = {:?},
shielded_outputs = {:?},
joinsplits = {:?},
joinsplit_pubkey = {:?},
binding_sig = {:?})",
self.version,
self.vin,
self.vout,
binding_sig = {:?},",
b.value_balance, b.shielded_spends, b.shielded_outputs, b.authorization
)
} else {
"".to_string()
},
{
#[cfg(feature = "zfuture")]
{
if let Some(b) = &self.tze_bundle {
format!(
"
tze_inputs = {:?},
tze_outputs = {:?},",
self.tze_inputs, self.tze_outputs
b.vin, b.vout
)
} else {
"".to_string()
}
#[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 {
fn default() -> Self {
TransactionData::new()
impl<A: Authorization> TransactionData<A> {
pub fn sapling_value_balance(&self) -> Amount {
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 {
TransactionData {
version: TxVersion::Sapling,
vin: vec![],
vout: vec![],
#[cfg(feature = "zfuture")]
tze_inputs: vec![],
#[cfg(feature = "zfuture")]
tze_outputs: vec![],
lock_time: 0,
expiry_height: 0u32.into(),
value_balance: Amount::zero(),
shielded_spends: vec![],
shielded_outputs: vec![],
joinsplits: vec![],
joinsplit_pubkey: None,
joinsplit_sig: None,
binding_sig: None,
transparent_bundle: None,
sprout_bundle: None,
sapling_bundle: None,
orchard_bundle: None,
#[cfg(feature = "zfuture")]
tze_bundle: None,
}
}
@ -305,29 +362,28 @@ impl TransactionData {
pub fn zfuture() -> Self {
TransactionData {
version: TxVersion::ZFuture,
vin: vec![],
vout: vec![],
tze_inputs: vec![],
tze_outputs: vec![],
lock_time: 0,
expiry_height: 0u32.into(),
value_balance: Amount::zero(),
shielded_spends: vec![],
shielded_outputs: vec![],
joinsplits: vec![],
joinsplit_pubkey: None,
joinsplit_sig: None,
binding_sig: None,
transparent_bundle: None,
sprout_bundle: None,
sapling_bundle: None,
orchard_bundle: None,
tze_bundle: None,
}
}
}
pub fn freeze(self) -> io::Result<Transaction> {
Transaction::from_data(self)
impl TransactionData<Authorized> {
pub fn freeze(self, consensus_branch_id: BranchId) -> io::Result<Transaction> {
Transaction::from_data(self, consensus_branch_id)
}
}
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 {
txid: TxId([0; 32]),
data,
@ -349,70 +405,56 @@ impl Transaction {
let is_overwinter_v3 = version == TxVersion::Overwinter;
let is_sapling_v4 = version == TxVersion::Sapling;
#[cfg(feature = "zfuture")]
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 transparent_bundle = Self::read_transparent(&mut reader)?;
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()
} else {
0u32.into()
};
let (value_balance, shielded_spends, shielded_outputs) = if is_sapling_v4 || has_tze {
let vb = {
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"))?;
let ss = Vector::read(&mut reader, SpendDescription::read)?;
let so = Vector::read(&mut reader, OutputDescription::read)?;
let (value_balance, shielded_spends, shielded_outputs) = if version.has_sapling() {
let vb = Self::read_amount(&mut reader)?;
#[allow(clippy::redundant_closure)]
let ss: Vec<SpendDescription<sapling::Authorized>> =
Vector::read(&mut reader, |r| SpendDescription::read(r))?;
#[allow(clippy::redundant_closure)]
let so: Vec<OutputDescription<sapling::Authorized>> =
Vector::read(&mut reader, |r| OutputDescription::read(r))?;
(vb, ss, so)
} else {
(Amount::zero(), vec![], vec![])
};
let (joinsplits, joinsplit_pubkey, joinsplit_sig) = if version.has_sprout() {
let jss = Vector::read(&mut reader, |r| {
JsDescription::read(r, version.uses_groth_proofs())
let sprout_bundle = if version.has_sprout() {
let joinsplits = Vector::read(&mut reader, |r| {
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)
&& !(shielded_spends.is_empty() && shielded_outputs.is_empty())
{
Some(Signature::read(&mut reader)?)
if !joinsplits.is_empty() {
let mut bundle = sprout::Bundle {
joinsplits,
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 {
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];
txid.copy_from_slice(&reader.into_hash());
@ -420,214 +462,150 @@ impl Transaction {
txid: TxId(txid),
data: TransactionData {
version,
vin,
vout,
#[cfg(feature = "zfuture")]
tze_inputs,
#[cfg(feature = "zfuture")]
tze_outputs,
lock_time,
expiry_height,
value_balance,
shielded_spends,
shielded_outputs,
joinsplits,
joinsplit_pubkey,
joinsplit_sig,
binding_sig,
transparent_bundle,
sprout_bundle,
sapling_bundle: binding_sig.map(|binding_sig| sapling::Bundle {
value_balance,
shielded_spends,
shielded_outputs,
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<()> {
self.version.write(&mut writer)?;
let is_overwinter_v3 = self.version == TxVersion::Overwinter;
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))?;
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))?;
}
self.write_transparent(&mut writer)?;
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))?;
}
if is_sapling_v4 || has_tze {
writer.write_all(&self.value_balance.to_i64_le_bytes())?;
Vector::write(&mut writer, &self.shielded_spends, |w, e| e.write(w))?;
Vector::write(&mut writer, &self.shielded_outputs, |w, e| e.write(w))?;
if self.version.has_sapling() {
writer.write_all(
&self
.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() {
Vector::write(&mut writer, &self.joinsplits, |w, e| e.write(w))?;
if !self.joinsplits.is_empty() {
match self.joinsplit_pubkey {
Some(pubkey) => writer.write_all(&pubkey)?,
None => {
return Err(io::Error::new(
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 let Some(bundle) = self.sprout_bundle.as_ref() {
Vector::write(&mut writer, &bundle.joinsplits, |w, e| e.write(w))?;
writer.write_all(&bundle.joinsplit_pubkey)?;
writer.write_all(&bundle.joinsplit_sig)?;
} else {
CompactSize::write(&mut writer, 0)?;
}
}
if !self.version.has_sprout() || self.joinsplits.is_empty() {
if self.joinsplit_pubkey.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"JoinSplit pubkey should not be present",
));
if self.version.has_sapling() {
if let Some(bundle) = self.sapling_bundle.as_ref() {
bundle.authorization.binding_sig.write(&mut writer)?;
}
if self.joinsplit_sig.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() {
} else if self.sapling_bundle.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Binding signature should not be present",
"Sapling components may not be present if Sapling is not active.",
));
}
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"))]
pub mod testing {
use proptest::collection::vec;
use proptest::prelude::*;
use proptest::sample::select;
use crate::{consensus::BranchId, legacy::Script};
#[cfg(feature = "zfuture")]
use crate::extensions::transparent as tze;
use crate::consensus::BranchId;
use super::{
components::{amount::MAX_MONEY, Amount, OutPoint, TxIn, TxOut},
Transaction, TransactionData, TxId, TxVersion,
components::transparent::testing as transparent, Authorized, Transaction, TransactionData,
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> {
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> {
select(vec![
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 {
BranchId::Sprout => (1..=2u32).prop_map(TxVersion::Sprout).boxed(),
BranchId::Overwinter => Just(TxVersion::Overwinter).boxed(),
#[cfg(feature = "zfuture")]
BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(),
//#[cfg(feature = "zfuture")]
//BranchId::ZFuture => Just(TxVersion::ZFuture).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"))]
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),
version in arb_tx_version(branch_id),
lock_time in any::<u32>(),
expiry_height in any::<u32>(),
value_balance in arb_amount(),
) -> TransactionData {
transparent_bundle in transparent::arb_bundle(),
) -> TransactionData<Authorized> {
TransactionData {
version,
vin, vout,
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
transparent_bundle,
sprout_bundle: None,
sapling_bundle: None, //FIXME
orchard_bundle: None, //FIXME
}
}
}
#[cfg(feature = "zfuture")]
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! {
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 byteorder::{LittleEndian, WriteBytesExt};
use ff::PrimeField;
use group::GroupEncoding;
use crate::{consensus, legacy::Script};
use crate::{
consensus::{self, BranchId},
legacy::Script,
};
#[cfg(feature = "zfuture")]
use crate::{
extensions::transparent::Precondition,
serialize::{CompactSize, Vector},
};
use crate::extensions::transparent::Precondition;
use super::{
components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut},
Transaction, TransactionData, TxVersion,
components::{
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_PREVOUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashPrevoutHash";
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_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;
const SIGHASH_NONE: u32 = 2;
const SIGHASH_SINGLE: u32 = 3;
@ -67,16 +56,7 @@ fn has_overwinter_components(version: &TxVersion) -> bool {
!matches!(version, TxVersion::Sprout(_))
}
fn has_sapling_components(version: &TxVersion) -> bool {
!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 {
fn prevout_hash<TA: transparent::Authorization>(vin: &[TxIn<TA>]) -> Blake2bHash {
let mut data = Vec::with_capacity(vin.len() * 36);
for t_in in vin {
t_in.prevout.write(&mut data).unwrap();
@ -87,7 +67,7 @@ fn prevout_hash(vin: &[TxIn]) -> Blake2bHash {
.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);
for t_in in vin {
(&mut data)
@ -121,13 +101,13 @@ fn single_output_hash(tx_out: &TxOut) -> Blake2bHash {
}
fn joinsplits_hash(
txversion: TxVersion,
consensus_branch_id: BranchId,
joinsplits: &[JsDescription],
joinsplit_pubkey: &[u8; 32],
) -> Blake2bHash {
let mut data = Vec::with_capacity(
joinsplits.len()
* if txversion.uses_groth_proofs() {
* if consensus_branch_id.sprout_uses_groth_proofs() {
1698 // JSDescription with Groth16 proof
} else {
1802 // JSDescription with PHGR13 proof
@ -143,7 +123,9 @@ fn joinsplits_hash(
.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);
for s_spend in shielded_spends {
data.extend_from_slice(&s_spend.cv.to_bytes());
@ -158,7 +140,9 @@ fn shielded_spends_hash(shielded_spends: &[SpendDescription]) -> Blake2bHash {
.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);
for s_out in shielded_outputs {
s_out.write(&mut data).unwrap();
@ -169,30 +153,6 @@ fn shielded_outputs_hash(shielded_outputs: &[OutputDescription]) -> Blake2bHash
.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> {
Shielded,
Transparent {
@ -227,8 +187,12 @@ impl<'a> SignableInput<'a> {
}
}
pub fn signature_hash_data(
tx: &TransactionData,
pub fn signature_hash_data<
TA: transparent::Authorization,
SA: sapling::Authorization<Proof = GrothProofBytes>,
A: Authorization<SaplingAuth = SA, TransparentAuth = TA>,
>(
tx: &TransactionData<A>,
consensus_branch_id: consensus::BranchId,
hash_type: u32,
signable_input: SignableInput<'_>,
@ -251,64 +215,91 @@ pub fn signature_hash_data(
update_hash!(
h,
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!(
h,
hash_type & SIGHASH_ANYONECANPAY == 0
&& (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE
&& (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
&& (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 {
match signable_input {
SignableInput::Transparent { index, .. } if index < tx.vout.len() => {
h.update(single_output_hash(&tx.vout[index]).as_ref())
match (tx.transparent_bundle.as_ref(), &signable_input) {
(Some(b), SignableInput::Transparent { index, .. }) if *index < b.vout.len() => {
h.update(single_output_hash(&b.vout[*index]).as_bytes())
}
_ => h.update(&[0; 32]),
};
} else {
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!(
h,
!tx.joinsplits.is_empty(),
joinsplits_hash(tx.version, &tx.joinsplits, &tx.joinsplit_pubkey.unwrap())
!tx.sprout_bundle
.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!(
h,
!tx.shielded_spends.is_empty(),
shielded_spends_hash(&tx.shielded_spends)
!tx.sapling_bundle
.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!(
h,
!tx.shielded_outputs.is_empty(),
shielded_outputs_hash(&tx.shielded_outputs)
!tx.sapling_bundle
.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.expiry_height.into(), tmp);
if has_sapling_components(&tx.version) {
h.update(&tx.value_balance.to_i64_le_bytes());
if tx.version.has_sapling() {
h.update(&tx.sapling_value_balance().to_i64_le_bytes());
}
update_u32!(h, hash_type, tmp);
@ -318,52 +309,28 @@ pub fn signature_hash_data(
script_code,
value,
} => {
#[cfg(feature = "zfuture")]
let mut data = if has_tze_components(&tx.version) {
// domain separation here is to avoid collision attacks
// between transparent and TZE inputs.
ZCASH_TRANSPARENT_SIGNED_INPUT_TAG.to_vec()
if let Some(bundle) = tx.transparent_bundle.as_ref() {
let mut data = vec![];
bundle.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>(bundle.vin[index].sequence)
.unwrap();
h.update(&data);
} else {
vec![]
};
#[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);
panic!(
"A request has been made to sign a transparent input, but none are present."
);
}
}
#[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")]
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()

View File

@ -1,14 +1,9 @@
use ff::Field;
use rand_core::OsRng;
use proptest::prelude::*;
use crate::{constants::SPENDING_KEY_GENERATOR, sapling::redjubjub::PrivateKey};
use super::{
components::Amount,
sighash::{signature_hash, SignableInput},
Transaction, TransactionData,
Transaction,
};
use super::testing::{arb_branch_id, arb_tx};
@ -27,49 +22,6 @@ fn tx_read_write() {
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! {
#[test]
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();
assert_eq!(tx.version, txo.version);
assert_eq!(tx.vin, txo.vin);
assert_eq!(tx.vout, txo.vout);
prop_assert_eq!(tx.version, txo.version);
prop_assert_eq!(tx.lock_time, txo.lock_time);
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")]
assert_eq!(tx.tze_inputs, txo.tze_inputs);
#[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
),
prop_assert_eq!(tx.tze_bundle.as_ref(), txo.tze_bundle.as_ref());
}
}
@ -133,7 +54,7 @@ fn zip_0143() {
};
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
);
}
@ -153,7 +74,7 @@ fn zip_0243() {
};
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
);
}