Move Sapling components to a bundle within TransactionData

This commit is contained in:
Kris Nuttycombe 2021-04-07 15:58:30 -06:00
parent 488d02aad3
commit 84e8952ec3
16 changed files with 607 additions and 393 deletions

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

@ -78,8 +78,7 @@ 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.sapling_bundle.is_none()
&& self.tx.joinsplits.is_empty()
&& self.tx.orchard_bundle.is_none()
}

View File

@ -472,8 +472,7 @@ mod tests {
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::{
@ -606,8 +605,7 @@ mod tests {
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.sapling_bundle.is_none()
&& self.tx.joinsplits.is_empty()
&& self.tx.orchard_bundle.is_none()
}
@ -659,7 +657,8 @@ mod tests {
};
fn auth_and_freeze(tx: TransactionData<Unauthorized>) -> Transaction {
Builder::<FutureNetwork, OsRng>::apply_authorization(
Builder::<FutureNetwork, OsRng>::apply_signatures(
BranchId::ZFuture,
tx,
#[cfg(feature = "transparent-inputs")]
None,
@ -667,7 +666,6 @@ mod tests {
None,
None,
)
.freeze(BranchId::ZFuture)
.unwrap()
}

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

@ -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

@ -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

@ -3,6 +3,7 @@
use core::array;
use std::error;
use std::fmt;
use std::io;
use std::sync::mpsc::Sender;
use rand::{rngs::OsRng, CryptoRng, RngCore};
@ -21,11 +22,14 @@ use crate::{
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, TxIn},
},
signature_hash_data, Authorized, SignableInput, Transaction, TransactionData, TxVersion,
Unauthorized, SIGHASH_ALL,
signature_hash_data, SignableInput, Transaction, TransactionData, TxVersion, Unauthorized,
SIGHASH_ALL,
},
zip32::ExtendedSpendingKey,
};
@ -34,17 +38,14 @@ use crate::{
use std::marker::PhantomData;
#[cfg(feature = "transparent-inputs")]
use crate::{
legacy::Script,
transaction::components::{OutPoint, TxOut},
};
use crate::{legacy::Script, transaction::components::transparent::TxOut};
#[cfg(feature = "zfuture")]
use crate::{
extensions::transparent::{ExtensionTxBuilder, ToPayload},
transaction::components::{
tze::builder::{self as tzebuilder, TzeBuilder, WitnessData},
TzeOut, TzeOutPoint,
tze::builder::{TzeBuilder, WitnessData},
tze::{self, TzeIn, TzeOut, TzeOutPoint},
},
};
@ -58,10 +59,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 {
@ -175,7 +176,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(),
@ -220,7 +221,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
@ -326,7 +327,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
let (vin, vout) = 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,
@ -350,13 +351,10 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
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,
sapling_bundle,
orchard_bundle: None,
};
@ -373,9 +371,10 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
));
#[cfg(feature = "transparent-inputs")]
let transparent_sigs = self
.transparent_builder
.create_signatures(&unauthed_tx, consensus_branch_id);
let transparent_sigs = Some(
self.transparent_builder
.create_signatures(&unauthed_tx, consensus_branch_id),
);
let sapling_sigs = self
.sapling_builder
@ -389,26 +388,25 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
.map_err(Error::TzeBuild)?,
);
let authorized_tx = Self::apply_authorization(
unauthed_tx,
#[cfg(feature = "transparent-inputs")]
Some(transparent_sigs),
sapling_sigs,
None,
#[cfg(feature = "zfuture")]
tze_witnesses,
);
Ok((
authorized_tx
.freeze(consensus_branch_id)
.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,
))
}
pub fn apply_authorization(
mut mtx: TransactionData<Unauthorized>,
pub 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<(
@ -416,46 +414,59 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
orchard::Authorized,
)>,
#[cfg(feature = "zfuture")] tze_witnesses: Option<Vec<WitnessData>>,
) -> TransactionData<Authorized> {
#[cfg(feature = "transparent-inputs")]
if let Some(script_sigs) = transparent_sigs {
for (i, sig) in script_sigs.into_iter().enumerate() {
mtx.vin[i].script_sig = sig;
) -> io::Result<Transaction> {
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))
}
}
if let Some((sapling_spend_auth_sigs, sapling_binding_sig)) = sapling_sigs {
for (i, spend_auth_sig) in sapling_spend_auth_sigs.into_iter().enumerate() {
mtx.shielded_spends[i].spend_auth_sig = Some(spend_auth_sig);
(None, None) => None,
_ => {
panic!("Mismatch between sapling bundle and signatures.");
}
mtx.binding_sig = Some(sapling_binding_sig);
}
};
#[cfg(feature = "zfuture")]
if let Some(tze_witnesses) = tze_witnesses {
for (i, payload) in tze_witnesses.into_iter().enumerate() {
mtx.tze_inputs[i].witness.payload = payload.0;
}
}
TransactionData {
version: mtx.version,
vin: mtx.vin,
vout: mtx.vout,
let mut authorized_tx = TransactionData {
version: unauthed_tx.version,
vin: unauthed_tx.vin,
vout: unauthed_tx.vout,
#[cfg(feature = "zfuture")]
tze_inputs: mtx.tze_inputs,
tze_inputs: unauthed_tx.tze_inputs,
#[cfg(feature = "zfuture")]
tze_outputs: mtx.tze_outputs,
lock_time: mtx.lock_time,
expiry_height: mtx.expiry_height,
value_balance: mtx.value_balance,
shielded_spends: mtx.shielded_spends,
shielded_outputs: mtx.shielded_outputs,
joinsplits: mtx.joinsplits,
joinsplit_pubkey: mtx.joinsplit_pubkey,
joinsplit_sig: mtx.joinsplit_sig,
binding_sig: mtx.binding_sig,
tze_outputs: unauthed_tx.tze_outputs,
lock_time: unauthed_tx.lock_time,
expiry_height: unauthed_tx.expiry_height,
joinsplits: unauthed_tx.joinsplits,
joinsplit_pubkey: unauthed_tx.joinsplit_pubkey,
joinsplit_sig: unauthed_tx.joinsplit_sig,
sapling_bundle: signed_sapling_bundle,
orchard_bundle: None,
};
#[cfg(feature = "transparent-inputs")]
Self::apply_transparent_sigs(&mut authorized_tx.vin, transparent_sigs);
#[cfg(feature = "zfuture")]
Self::apply_tze_sigs(&mut authorized_tx.tze_inputs, tze_witnesses);
authorized_tx.freeze(consensus_branch_id)
}
#[cfg(feature = "transparent-inputs")]
pub fn apply_transparent_sigs(vin: &mut [TxIn], transparent_sigs: Option<Vec<Script>>) {
if let Some(script_sigs) = transparent_sigs {
assert!(vin.len() == script_sigs.len());
for (i, sig) in script_sigs.into_iter().enumerate() {
vin[i].script_sig = sig;
}
}
}
#[cfg(feature = "zfuture")]
pub fn apply_tze_sigs(vtzein: &mut [TzeIn], tze_witnesses: Option<Vec<WitnessData>>) {
if let Some(tze_witnesses) = tze_witnesses {
assert!(vtzein.len() == tze_witnesses.len());
for (i, payload) in tze_witnesses.into_iter().enumerate() {
vtzein[i].witness.payload = payload.0;
}
}
}
}
@ -465,7 +476,7 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a
for Builder<'a, P, R>
{
type BuildCtx = TransactionData<Unauthorized>;
type BuildError = tzebuilder::Error;
type BuildError = tze::builder::Error;
fn add_tze_input<WBuilder, W: ToPayload>(
&mut self,
@ -475,7 +486,7 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a
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);
@ -552,7 +563,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},
@ -580,7 +591,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))
);
}
@ -601,7 +612,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(),
@ -617,7 +628,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]
@ -654,7 +665,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))
);
}
@ -804,7 +815,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,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,
"invalid ".to_owned() + field,
))
} else {
Ok(point.unwrap())
}
}
/// Consensus rules (§7.3) & (§7.4):
/// - Canonical encoding is enforced here
pub fn read_scalar<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,
field.to_owned() + " not in 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_scalar(&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>(
@ -523,10 +506,91 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
.unwrap_or_default();
let binding_sig = prover
.binding_sig(ctx, self.value_balance, &sighash_bytes)
.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

@ -9,12 +9,14 @@ use orchard;
use crate::{
consensus::{BlockHeight, BranchId},
sapling::redjubjub::Signature,
sapling::redjubjub,
serialize::Vector,
};
use self::{
components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut},
components::{
sapling, Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut,
},
sighash::{signature_hash_data, SignableInput, SIGHASH_ALL},
util::sha256d::{HashReader, HashWriter},
};
@ -167,7 +169,7 @@ impl TxVersion {
}
}
pub fn uses_groth_proofs(&self) -> bool {
pub fn has_sapling(&self) -> bool {
match self {
TxVersion::Sprout(_) | TxVersion::Overwinter => false,
TxVersion::Sapling => true,
@ -192,18 +194,21 @@ impl TxVersion {
/// Authorization state for a bundle of transaction data.
pub trait Authorization {
type SaplingAuth: sapling::Authorization;
type OrchardAuth: orchard::bundle::Authorization;
}
pub struct Authorized;
impl Authorization for Authorized {
type SaplingAuth = sapling::Authorized;
type OrchardAuth = orchard::bundle::Authorized;
}
pub struct Unauthorized;
impl Authorization for Unauthorized {
type SaplingAuth = sapling::Unauthorized;
type OrchardAuth = orchard::builder::Unauthorized;
}
@ -238,13 +243,10 @@ pub struct TransactionData<A: Authorization> {
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 sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>,
pub orchard_bundle: Option<orchard::Bundle<A::OrchardAuth, Amount>>,
}
@ -282,19 +284,33 @@ impl<A: Authorization> std::fmt::Debug for TransactionData<A> {
},
self.lock_time,
self.expiry_height,
self.value_balance,
self.shielded_spends,
self.shielded_outputs,
self.sapling_bundle
.as_ref()
.map_or(Amount::zero(), |b| b.value_balance),
self.sapling_bundle
.as_ref()
.map_or(&vec![], |b| &b.shielded_spends),
self.sapling_bundle
.as_ref()
.map_or(&vec![], |b| &b.shielded_outputs),
self.joinsplits,
self.joinsplit_pubkey,
self.binding_sig
self.sapling_bundle.as_ref().map(|b| &b.authorization)
)
}
}
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 Default for TransactionData<Unauthorized> {
fn default() -> Self {
TransactionData::new()
Self::new()
}
}
@ -310,13 +326,10 @@ impl TransactionData<Unauthorized> {
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,
sapling_bundle: None,
orchard_bundle: None,
}
}
@ -331,13 +344,10 @@ impl TransactionData<Unauthorized> {
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,
sapling_bundle: None,
orchard_bundle: None,
}
}
@ -365,6 +375,13 @@ impl Transaction {
self.txid
}
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 read<R: Read>(reader: R) -> io::Result<Self> {
let mut reader = HashReader::new(reader);
@ -396,15 +413,14 @@ impl Transaction {
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![])
@ -412,7 +428,7 @@ impl Transaction {
let (joinsplits, joinsplit_pubkey, joinsplit_sig) = if version.has_sprout() {
let jss = Vector::read(&mut reader, |r| {
JsDescription::read(r, version.uses_groth_proofs())
JsDescription::read(r, version.has_sapling())
})?;
let (pubkey, sig) = if !jss.is_empty() {
let mut joinsplit_pubkey = [0; 32];
@ -431,7 +447,7 @@ impl Transaction {
let binding_sig = if (is_sapling_v4 || has_tze)
&& !(shielded_spends.is_empty() && shielded_outputs.is_empty())
{
Some(Signature::read(&mut reader)?)
Some(redjubjub::Signature::read(&mut reader)?)
} else {
None
};
@ -451,13 +467,15 @@ impl Transaction {
tze_outputs,
lock_time,
expiry_height,
value_balance,
shielded_spends,
shielded_outputs,
joinsplits,
joinsplit_pubkey,
joinsplit_sig,
binding_sig,
sapling_bundle: binding_sig.map(|binding_sig| sapling::Bundle {
value_balance,
shielded_spends,
shielded_outputs,
authorization: sapling::Authorized { binding_sig },
}),
orchard_bundle: None,
},
})
@ -486,10 +504,28 @@ impl Transaction {
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),
)?;
}
if self.version.has_sprout() {
@ -531,19 +567,16 @@ impl Transaction {
}
}
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",
));
}
if self.version.has_sapling() {
if let Some(bundle) = self.sapling_bundle.as_ref() {
bundle.authorization.binding_sig.write(&mut writer)?;
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Missing binding signature",
));
}
} else if self.binding_sig.is_some() {
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Binding signature should not be present",
@ -686,7 +719,6 @@ pub mod testing {
tze_outputs in vec(arb_tzeout(), 0..10),
lock_time in any::<u32>(),
expiry_height in any::<u32>(),
value_balance in arb_amount(),
) -> TransactionData<Authorized> {
TransactionData {
version,
@ -695,16 +727,10 @@ pub mod testing {
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
sapling_bundle: None, //FIXME
orchard_bundle: None, //FIXME
}
}
@ -718,23 +744,16 @@ pub mod testing {
vout in vec(arb_txout(), 0..10),
lock_time in any::<u32>(),
expiry_height in any::<u32>(),
value_balance in arb_amount(),
) -> 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
sapling_bundle: None, //FIXME
orchard_bundle: None, //FIXME
}
}

View File

@ -6,7 +6,10 @@ 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::{
@ -15,7 +18,10 @@ use crate::{
};
use super::{
components::{Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut},
components::{
sapling::{self, GrothProofBytes},
Amount, JsDescription, OutputDescription, SpendDescription, TxIn, TxOut,
},
Authorization, Transaction, TransactionData, TxVersion,
};
@ -67,10 +73,6 @@ 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)
@ -121,13 +123,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 +145,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 +162,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();
@ -227,7 +233,10 @@ impl<'a> SignableInput<'a> {
}
}
pub fn signature_hash_data<A: Authorization>(
pub fn signature_hash_data<
SA: sapling::Authorization<Proof = GrothProofBytes>,
A: Authorization<SaplingAuth = SA>,
>(
tx: &TransactionData<A>,
consensus_branch_id: consensus::BranchId,
hash_type: u32,
@ -291,24 +300,44 @@ pub fn signature_hash_data<A: Authorization>(
update_hash!(
h,
!tx.joinsplits.is_empty(),
joinsplits_hash(tx.version, &tx.joinsplits, &tx.joinsplit_pubkey.unwrap())
joinsplits_hash(
consensus_branch_id,
&tx.joinsplits,
&tx.joinsplit_pubkey.unwrap()
)
);
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);

View File

@ -1,41 +1,13 @@
use ff::Field;
use rand_core::OsRng;
use std::io;
use proptest::prelude::*;
use crate::{
consensus::{BranchId, TestNetwork},
constants::SPENDING_KEY_GENERATOR,
sapling::redjubjub::PrivateKey,
};
use super::{
builder::Builder,
components::Amount,
sighash::{signature_hash, SignableInput},
Transaction, TransactionData, Unauthorized,
Transaction,
};
use super::testing::{arb_branch_id, arb_tx};
fn auth_and_freeze(
txdata: TransactionData<Unauthorized>,
branch_id: BranchId,
) -> io::Result<Transaction> {
Builder::<TestNetwork, OsRng>::apply_authorization(
txdata,
#[cfg(feature = "transparent-inputs")]
None,
None,
#[cfg(any(feature = "nu5", feature = "zfuture"))]
None,
#[cfg(feature = "zfuture")]
None,
)
.freeze(branch_id)
}
#[test]
fn tx_read_write() {
let data = &self::data::tx_read_write::TX_READ_WRITE;
@ -50,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!(auth_and_freeze(TransactionData::new(), BranchId::Canopy).is_ok());
// Fails with an unexpected JoinSplit pubkey
{
let mut tx = TransactionData::new();
tx.joinsplit_pubkey = Some([0; 32]);
assert!(auth_and_freeze(tx, BranchId::Canopy).is_err());
}
}
#[test]
fn tx_write_rejects_unexpected_joinsplit_sig() {
// Succeeds without a JoinSplit signature
assert!(auth_and_freeze(TransactionData::new(), BranchId::Canopy).is_ok());
// Fails with an unexpected JoinSplit signature
{
let mut tx = TransactionData::new();
tx.joinsplit_sig = Some([0; 64]);
assert!(auth_and_freeze(tx, BranchId::Canopy).is_err());
}
}
#[test]
fn tx_write_rejects_unexpected_binding_sig() {
// Succeeds without a binding signature
assert!(auth_and_freeze(TransactionData::new(), BranchId::Canopy).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!(auth_and_freeze(tx, BranchId::Canopy).is_err());
}
}
proptest! {
#[test]
fn tx_serialization_roundtrip(tx in arb_branch_id().prop_flat_map(arb_tx)) {
@ -109,7 +38,7 @@ proptest! {
#[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);
assert_eq!(tx.sapling_value_balance(), txo.sapling_value_balance());
}
}
@ -156,7 +85,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
);
}
@ -176,7 +105,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
);
}