Implement V5 transaction serialization & roundtrip property tests.

This commit is contained in:
Kris Nuttycombe 2021-05-13 14:11:20 -06:00
parent e828dbf5d0
commit 38b864c100
6 changed files with 523 additions and 74 deletions

View File

@ -179,3 +179,39 @@ pub fn write_flags<W: Write>(mut writer: W, flags: &Flags) -> io::Result<()> {
pub fn write_anchor<W: Write>(mut writer: W, anchor: &Anchor) -> io::Result<()> {
writer.write_all(&anchor.0)
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::*;
use orchard::bundle::{
testing::{self as t_orch},
Authorized, Bundle,
};
use crate::transaction::{
components::amount::{testing::arb_amount, Amount},
TxVersion,
};
prop_compose! {
pub fn arb_bundle()(
orchard_value_balance in arb_amount(),
bundle in t_orch::arb_bundle()
) -> Bundle<Authorized, Amount> {
// overwrite the value balance, as we can't guarantee that the
// value doesn't exceed the MAX_MONEY bounds.
bundle.try_map_value_balance::<_, (), _>(|_| Ok(orchard_value_balance)).unwrap()
}
}
pub fn arb_bundle_for_version(
v: TxVersion,
) -> impl Strategy<Value = Option<Bundle<Authorized, Amount>>> {
if v.has_orchard() {
Strategy::boxed(prop::option::of(arb_bundle()))
} else {
Strategy::boxed(Just(None))
}
}
}

View File

@ -446,7 +446,10 @@ pub mod testing {
redjubjub::{PrivateKey, PublicKey},
Nullifier,
},
transaction::components::{amount::testing::arb_amount, GROTH_PROOF_SIZE},
transaction::{
components::{amount::testing::arb_amount, GROTH_PROOF_SIZE},
TxVersion,
},
};
use super::{Authorized, Bundle, OutputDescription, SpendDescription};
@ -524,12 +527,12 @@ pub mod testing {
rng_seed in prop::array::uniform32(prop::num::u8::ANY),
fake_bvk_bytes in prop::array::uniform32(prop::num::u8::ANY),
) -> Option<Bundle<Authorized>> {
let mut rng = StdRng::from_seed(rng_seed);
let bsk = PrivateKey(jubjub::Fr::random(&mut rng));
if shielded_spends.is_empty() && shielded_outputs.is_empty() {
None
} else {
let mut rng = StdRng::from_seed(rng_seed);
let bsk = PrivateKey(jubjub::Fr::random(&mut rng));
Some(
Bundle {
shielded_spends,
@ -541,4 +544,14 @@ pub mod testing {
}
}
}
pub fn arb_bundle_for_version(
v: TxVersion,
) -> impl Strategy<Value = Option<Bundle<Authorized>>> {
if v.has_sapling() {
Strategy::boxed(arb_bundle())
} else {
Strategy::boxed(Just(None))
}
}
}

View File

@ -10,11 +10,18 @@ pub mod util;
#[cfg(test)]
mod tests;
use blake2b_simd::Hash as Blake2bHash;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use ff::PrimeField;
use nonempty::NonEmpty;
use std::convert::TryFrom;
use std::fmt;
use std::fmt::Debug;
use std::io::{self, Read, Write};
use std::ops::Deref;
use orchard::{self, primitives::redpallas};
use crate::{
consensus::{BlockHeight, BranchId},
sapling::redjubjub,
@ -24,15 +31,19 @@ use crate::{
use self::{
components::{
amount::Amount,
sapling::{self, OutputDescription, SpendDescription},
orchard as orchard_serialization,
sapling::{
self, OutputDescription, OutputDescriptionV5, SpendDescription, SpendDescriptionV5,
},
sprout::{self, JsDescription},
transparent::{self, TxIn, TxOut},
},
txid::{to_txid, BlockTxCommitmentDigester, TxIdDigester},
util::sha256d::{HashReader, HashWriter},
};
#[cfg(feature = "zfuture")]
use self::components::tze;
use self::components::tze::{self, TzeIn, TzeOut};
const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270;
const OVERWINTER_TX_VERSION: u32 = 3;
@ -99,7 +110,7 @@ pub enum TxVersion {
Sprout(u32),
Overwinter,
Sapling,
ZcashTxV5,
Zip225,
#[cfg(feature = "zfuture")]
ZFuture,
}
@ -114,7 +125,7 @@ impl TxVersion {
match (version, reader.read_u32::<LittleEndian>()?) {
(OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID) => Ok(TxVersion::Overwinter),
(SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID) => Ok(TxVersion::Sapling),
(V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::ZcashTxV5),
(V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225),
#[cfg(feature = "zfuture")]
(ZFUTURE_TX_VERSION, ZFUTURE_VERSION_GROUP_ID) => Ok(TxVersion::ZFuture),
_ => Err(io::Error::new(
@ -144,7 +155,7 @@ impl TxVersion {
TxVersion::Sprout(v) => *v,
TxVersion::Overwinter => OVERWINTER_TX_VERSION,
TxVersion::Sapling => SAPLING_TX_VERSION,
TxVersion::ZcashTxV5 => V5_TX_VERSION,
TxVersion::Zip225 => V5_TX_VERSION,
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => ZFUTURE_TX_VERSION,
}
@ -155,7 +166,7 @@ impl TxVersion {
TxVersion::Sprout(_) => 0,
TxVersion::Overwinter => OVERWINTER_VERSION_GROUP_ID,
TxVersion::Sapling => SAPLING_VERSION_GROUP_ID,
TxVersion::ZcashTxV5 => V5_VERSION_GROUP_ID,
TxVersion::Zip225 => V5_VERSION_GROUP_ID,
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => ZFUTURE_VERSION_GROUP_ID,
}
@ -173,7 +184,7 @@ impl TxVersion {
match self {
TxVersion::Sprout(v) => *v >= 2u32,
TxVersion::Overwinter | TxVersion::Sapling => true,
TxVersion::ZcashTxV5 => false,
TxVersion::Zip225 => false,
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => true,
}
@ -187,7 +198,7 @@ impl TxVersion {
match self {
TxVersion::Sprout(_) | TxVersion::Overwinter => false,
TxVersion::Sapling => true,
TxVersion::ZcashTxV5 => true,
TxVersion::Zip225 => true,
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => true,
}
@ -196,7 +207,7 @@ impl TxVersion {
pub fn has_orchard(&self) -> bool {
match self {
TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => false,
TxVersion::ZcashTxV5 => true,
TxVersion::Zip225 => true,
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => true,
}
@ -214,7 +225,7 @@ impl TxVersion {
BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => {
TxVersion::Sapling
}
BranchId::Nu5 => TxVersion::Sapling, //TEMPORARY WORKAROUND
BranchId::Nu5 => TxVersion::Zip225, //TEMPORARY WORKAROUND
#[cfg(feature = "zfuture")]
BranchId::ZFuture => TxVersion::ZFuture,
}
@ -466,6 +477,17 @@ impl TransactionData<Authorized> {
impl Transaction {
fn from_data(data: TransactionData<Authorized>) -> io::Result<Self> {
match data.version {
TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => {
Self::from_data_v4(data)
}
TxVersion::Zip225 => Ok(Self::from_data_v5(data)),
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => Ok(Self::from_data_v5(data)),
}
}
fn from_data_v4(data: TransactionData<Authorized>) -> io::Result<Self> {
let mut tx = Transaction {
txid: TxId([0; 32]),
data,
@ -476,22 +498,44 @@ impl Transaction {
Ok(tx)
}
fn from_data_v5(data: TransactionData<Authorized>) -> Self {
let txid = to_txid(
data.version,
data.consensus_branch_id,
&data.digest(TxIdDigester),
);
Transaction { txid, data }
}
pub fn txid(&self) -> TxId {
self.txid
}
#[allow(clippy::redundant_closure)]
pub fn read<R: Read>(reader: R, consensus_branch_id: BranchId) -> io::Result<Self> {
let mut reader = HashReader::new(reader);
let version = TxVersion::read(&mut reader)?;
let is_overwinter_v3 = version == TxVersion::Overwinter;
let is_sapling_v4 = version == TxVersion::Sapling;
match version {
TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => {
Self::read_v4(reader, version, consensus_branch_id)
}
TxVersion::Zip225 => Self::read_v5(reader.into_base_reader(), version),
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => Self::read_v5(reader.into_base_reader(), version),
}
}
#[allow(clippy::redundant_closure)]
fn read_v4<R: Read>(
mut reader: HashReader<R>,
version: TxVersion,
consensus_branch_id: BranchId,
) -> io::Result<Self> {
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 {
let expiry_height: BlockHeight = if version.has_overwinter() {
reader.read_u32::<LittleEndian>()?.into()
} else {
0u32.into()
@ -531,15 +575,17 @@ impl Transaction {
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 binding_sig = if version.has_sapling()
&& !(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());
let hash_bytes = reader.into_hash();
txid.copy_from_slice(&hash_bytes);
Ok(Transaction {
txid: TxId(txid),
@ -582,16 +628,190 @@ impl Transaction {
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range"))
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
fn read_v5<R: Read>(mut reader: R, version: TxVersion) -> io::Result<Self> {
let (consensus_branch_id, lock_time, expiry_height) =
Self::read_v5_header_fragment(&mut reader)?;
let transparent_bundle = Self::read_transparent(&mut reader)?;
let sapling_bundle = Self::read_v5_sapling(&mut reader)?;
let orchard_bundle = Self::read_v5_orchard(&mut reader)?;
#[cfg(feature = "zfuture")]
let tze_bundle = Self::read_tze(&mut reader)?;
let data = TransactionData {
version,
consensus_branch_id,
lock_time,
expiry_height,
transparent_bundle,
sprout_bundle: None,
sapling_bundle,
orchard_bundle,
#[cfg(feature = "zfuture")]
tze_bundle,
};
Ok(Self::from_data_v5(data))
}
fn read_v5_header_fragment<R: Read>(mut reader: R) -> io::Result<(BranchId, u32, BlockHeight)> {
let consensus_branch_id = reader.read_u32::<LittleEndian>().and_then(|value| {
BranchId::try_from(value).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid consensus branch id: ".to_owned() + e,
)
})
})?;
let lock_time = reader.read_u32::<LittleEndian>()?;
let expiry_height: BlockHeight = reader.read_u32::<LittleEndian>()?.into();
Ok((consensus_branch_id, lock_time, expiry_height))
}
#[allow(clippy::redundant_closure)]
fn read_v5_sapling<R: Read>(
mut reader: R,
) -> io::Result<Option<sapling::Bundle<sapling::Authorized>>> {
let n_spends = CompactSize::read(&mut reader)?;
let sd_v5s = Vector::read_count(&mut reader, n_spends, SpendDescriptionV5::read)?;
let n_outputs = CompactSize::read(&mut reader)?;
let od_v5s = Vector::read_count(&mut reader, n_outputs, OutputDescriptionV5::read)?;
let value_balance = if n_spends > 0 || n_outputs > 0 {
Self::read_amount(&mut reader)?
} else {
Amount::zero()
};
let anchor = if n_spends > 0 {
Some(sapling::read_base(&mut reader, "anchor")?)
} else {
None
};
let v_spend_proofs =
Vector::read_count(&mut reader, n_spends, |r| sapling::read_zkproof(r))?;
let v_spend_auth_sigs = Vector::read_count(&mut reader, n_spends, |r| {
SpendDescription::read_spend_auth_sig(r)
})?;
let v_output_proofs =
Vector::read_count(&mut reader, n_outputs, |r| sapling::read_zkproof(r))?;
let binding_sig = if n_spends > 0 || n_outputs > 0 {
Some(redjubjub::Signature::read(&mut reader)?)
} else {
None
};
let shielded_spends = sd_v5s
.into_iter()
.zip(
v_spend_proofs
.into_iter()
.zip(v_spend_auth_sigs.into_iter()),
)
.map(|(sd_5, (zkproof, spend_auth_sig))| {
// the following `unwrap` is safe because we know n_spends > 0.
sd_5.into_spend_description(anchor.unwrap(), zkproof, spend_auth_sig)
})
.collect();
let shielded_outputs = od_v5s
.into_iter()
.zip(v_output_proofs.into_iter())
.map(|(od_5, zkproof)| od_5.into_output_description(zkproof))
.collect();
Ok(binding_sig.map(|binding_sig| sapling::Bundle {
value_balance,
shielded_spends,
shielded_outputs,
authorization: sapling::Authorized { binding_sig },
}))
}
fn read_v5_orchard<R: Read>(
mut reader: R,
) -> io::Result<Option<orchard::Bundle<orchard::bundle::Authorized, Amount>>> {
let n_actions = CompactSize::read(&mut reader)?;
if n_actions == 0 {
Ok(None)
} else {
let actions_without_auth = Vector::read_count(&mut reader, n_actions, |r| {
orchard_serialization::read_action_without_auth(r)
})?;
let flags = orchard_serialization::read_flags(&mut reader)?;
let value_balance = Self::read_amount(&mut reader)?;
let anchor = orchard_serialization::read_anchor(&mut reader)?;
let proof_size = CompactSize::read(&mut reader)?;
let mut proof_bytes = vec![0u8; proof_size];
reader.read_exact(&mut proof_bytes)?;
let spend_sigs = Vector::read_count(&mut reader, n_actions, |r| {
orchard_serialization::read_signature::<_, redpallas::SpendAuth>(r)
})?;
let binding_signature =
orchard_serialization::read_signature::<_, redpallas::Binding>(&mut reader)
.map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"An error occurred deserializing the Orchard binding signature: {}",
e
),
)
})?;
let actions = NonEmpty::from_vec(
actions_without_auth
.into_iter()
.zip(spend_sigs.into_iter())
.map(|(act, sig)| act.map(|_| sig))
.collect(),
)
.expect("A nonzero number of actions was read from the transaction data.");
let authorization = orchard::bundle::Authorized::from_parts(
orchard::Proof::new(proof_bytes),
binding_signature,
);
Ok(Some(orchard::Bundle::from_parts(
actions,
flags,
value_balance,
anchor,
authorization,
)))
}
}
#[cfg(feature = "zfuture")]
fn read_tze<R: Read>(mut reader: &mut R) -> io::Result<Option<tze::Bundle<tze::Authorized>>> {
let vin = Vector::read(&mut reader, TzeIn::read)?;
let vout = Vector::read(&mut reader, TzeOut::read)?;
Ok(if vin.is_empty() && vout.is_empty() {
None
} else {
Some(tze::Bundle { vin, vout })
})
}
pub fn write<W: Write>(&self, writer: W) -> io::Result<()> {
match self.version {
TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => {
self.write_v4(writer)
}
TxVersion::Zip225 => self.write_v5(writer),
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => self.write_v5(writer),
}
}
pub fn write_v4<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;
self.write_transparent(&mut writer)?;
writer.write_u32::<LittleEndian>(self.lock_time)?;
if is_overwinter_v3 || is_sapling_v4 {
if self.version.has_overwinter() {
writer.write_u32::<LittleEndian>(u32::from(self.expiry_height))?;
}
@ -638,10 +858,12 @@ impl Transaction {
if let Some(bundle) = self.sapling_bundle.as_ref() {
bundle.authorization.binding_sig.write(&mut writer)?;
}
} else if self.sapling_bundle.is_some() {
}
if self.orchard_bundle.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Sapling components may not be present if Sapling is not active.",
"Orchard components may not be present when serializing to the V4 transaction format."
));
}
@ -660,6 +882,101 @@ impl Transaction {
Ok(())
}
pub fn write_v5<W: Write>(&self, mut writer: W) -> io::Result<()> {
self.write_v5_header(&mut writer)?;
self.write_transparent(&mut writer)?;
self.write_v5_sapling(&mut writer)?;
self.write_v5_orchard(&mut writer)?;
#[cfg(feature = "zfuture")]
self.write_tze(&mut writer)?;
Ok(())
}
pub fn write_v5_header<W: Write>(&self, mut writer: W) -> io::Result<()> {
self.version.write(&mut writer)?;
writer.write_u32::<LittleEndian>(u32::from(self.consensus_branch_id))?;
writer.write_u32::<LittleEndian>(self.lock_time)?;
writer.write_u32::<LittleEndian>(u32::from(self.expiry_height))?;
Ok(())
}
pub fn write_v5_sapling<W: Write>(&self, mut writer: W) -> io::Result<()> {
if let Some(bundle) = &self.sapling_bundle {
CompactSize::write(&mut writer, bundle.shielded_spends.len())?;
Vector::write_items(&mut writer, &bundle.shielded_spends, |w, e| {
e.write_v5_without_witness_data(w)
})?;
CompactSize::write(&mut writer, bundle.shielded_outputs.len())?;
Vector::write_items(&mut writer, &bundle.shielded_outputs, |w, e| {
e.write_v5_without_proof(w)
})?;
if !(bundle.shielded_spends.is_empty() && bundle.shielded_outputs.is_empty()) {
writer.write_all(&bundle.value_balance.to_i64_le_bytes())?;
}
if !bundle.shielded_spends.is_empty() {
writer.write_all(bundle.shielded_spends[0].anchor.to_repr().as_ref())?;
}
Vector::write_items(
&mut writer,
bundle.shielded_spends.iter().map(|s| s.zkproof),
|w, e| w.write_all(e),
)?;
Vector::write_items(
&mut writer,
bundle.shielded_spends.iter().map(|s| s.spend_auth_sig),
|w, e| e.write(w),
)?;
Vector::write_items(
&mut writer,
bundle.shielded_outputs.iter().map(|s| s.zkproof),
|w, e| w.write_all(e),
)?;
if !bundle.shielded_spends.is_empty() || !bundle.shielded_outputs.is_empty() {
bundle.authorization.binding_sig.write(&mut writer)?;
}
} else {
CompactSize::write(&mut writer, 0)?;
CompactSize::write(&mut writer, 0)?;
}
Ok(())
}
pub fn write_v5_orchard<W: Write>(&self, mut writer: W) -> io::Result<()> {
if let Some(bundle) = &self.orchard_bundle {
CompactSize::write(&mut writer, bundle.actions().len())?;
Vector::write_items(&mut writer, bundle.actions().iter(), |w, a| {
orchard_serialization::write_action_without_auth(w, a)
})?;
if !bundle.actions().is_empty() {
orchard_serialization::write_flags(&mut writer, &bundle.flags())?;
writer.write_all(&bundle.value_balance().to_i64_le_bytes())?;
orchard_serialization::write_anchor(&mut writer, bundle.anchor())?;
let proof_bytes: &[u8] = bundle.authorization().proof().as_ref();
CompactSize::write(&mut writer, proof_bytes.len())?;
writer.write_all(&proof_bytes)?;
Vector::write_items(
&mut writer,
bundle.actions().iter().map(|a| a.authorization()),
|w, auth| w.write_all(&<[u8; 64]>::from(*auth)),
)?;
writer.write_all(&<[u8; 64]>::from(
bundle.authorization().binding_signature(),
))?;
}
} else {
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 {
@ -672,6 +989,11 @@ impl Transaction {
Ok(())
}
// TODO: should this be moved to `from_data` and stored?
pub fn auth_commitment(&self) -> Result<Blake2bHash, DigestError> {
Ok(self.data.digest(BlockTxCommitmentDigester))
}
}
#[derive(Clone)]
@ -753,39 +1075,35 @@ pub enum DigestError {
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::*;
use proptest::sample::select;
use crate::consensus::BranchId;
use super::{
components::transparent::testing as transparent, Authorized, Transaction, TransactionData,
TxId, TxVersion,
components::{
orchard::testing::{self as orchard},
sapling::testing::{self as sapling},
transparent::testing::{self as transparent},
},
Authorized, Transaction, TransactionData, TxId, TxVersion,
};
#[cfg(feature = "zfuture")]
use super::components::tze::testing::{self as tze};
pub fn arb_txid() -> impl Strategy<Value = TxId> {
prop::array::uniform32(any::<u8>()).prop_map(TxId::from_bytes)
}
pub fn arb_branch_id() -> impl Strategy<Value = BranchId> {
select(vec![
BranchId::Sprout,
BranchId::Overwinter,
BranchId::Sapling,
BranchId::Blossom,
BranchId::Heartwood,
BranchId::Canopy,
#[cfg(feature = "zfuture")]
BranchId::ZFuture,
])
}
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(),
_otherwise => Just(TxVersion::Sapling).boxed(),
BranchId::Sapling | BranchId::Blossom | BranchId::Heartwood | BranchId::Canopy => {
Just(TxVersion::Sapling).boxed()
}
BranchId::Nu5 => Just(TxVersion::Zip225).boxed(),
#[cfg(feature = "zfuture")]
BranchId::ZFuture => Just(TxVersion::ZFuture).boxed(),
}
}
@ -793,9 +1111,13 @@ pub mod testing {
prop_compose! {
pub fn arb_txdata(consensus_branch_id: BranchId)(
version in arb_tx_version(consensus_branch_id),
)(
lock_time in any::<u32>(),
expiry_height in any::<u32>(),
transparent_bundle in transparent::arb_bundle(),
sapling_bundle in sapling::arb_bundle_for_version(version),
orchard_bundle in orchard::arb_bundle_for_version(version),
version in Just(version)
) -> TransactionData<Authorized> {
TransactionData {
version,
@ -804,8 +1126,8 @@ pub mod testing {
expiry_height: expiry_height.into(),
transparent_bundle,
sprout_bundle: None,
sapling_bundle: None, //FIXME
orchard_bundle: None, //FIXME
sapling_bundle,
orchard_bundle
}
}
}
@ -814,10 +1136,14 @@ pub mod testing {
prop_compose! {
pub fn arb_txdata(consensus_branch_id: BranchId)(
version in arb_tx_version(consensus_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),
sapling_bundle in sapling::arb_bundle_for_version(version),
orchard_bundle in orchard::arb_bundle_for_version(version),
tze_bundle in tze::arb_bundle(consensus_branch_id),
version in Just(version)
) -> TransactionData<Authorized> {
TransactionData {
version,
@ -826,9 +1152,9 @@ pub mod testing {
expiry_height: expiry_height.into(),
transparent_bundle,
sprout_bundle: None,
sapling_bundle: None, //FIXME
orchard_bundle: None, //FIXME
tze_bundle: None
sapling_bundle,
orchard_bundle,
tze_bundle
}
}
}

View File

@ -128,7 +128,7 @@ pub fn signature_hash<
v4_signature_hash(tx, signable_input, hash_type)
}
TxVersion::ZcashTxV5 => v5_signature_hash(tx, txid_parts, signable_input, hash_type),
TxVersion::Zip225 => v5_signature_hash(tx, txid_parts, signable_input, hash_type),
#[cfg(feature = "zfuture")]
TxVersion::ZFuture => v5_signature_hash(tx, txid_parts, signable_input, hash_type),

View File

@ -1,13 +1,14 @@
use std::ops::Deref;
use proptest::prelude::*;
use crate::consensus::BranchId;
use super::{
components::Amount, sighash::SignableInput, sighash_v4::v4_signature_hash, Transaction,
components::Amount, sighash::SignableInput, sighash_v4::v4_signature_hash, testing::arb_tx,
Transaction,
};
use super::testing::{arb_branch_id, arb_tx};
#[test]
fn tx_read_write() {
let data = &self::data::tx_read_write::TX_READ_WRITE;
@ -22,20 +23,89 @@ fn tx_read_write() {
assert_eq!(&data[..], &encoded[..]);
}
fn check_roundtrip(branch_id: BranchId, tx: Transaction) -> Result<(), TestCaseError> {
let mut txn_bytes = vec![];
tx.write(&mut txn_bytes).unwrap();
let txo = Transaction::read(&txn_bytes[..], branch_id).unwrap();
prop_assert_eq!(tx.version, txo.version);
#[cfg(feature = "zfuture")]
prop_assert_eq!(tx.tze_bundle.as_ref(), txo.tze_bundle.as_ref());
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());
prop_assert_eq!(
tx.orchard_bundle.as_ref().map(|v| *v.value_balance()),
txo.orchard_bundle.as_ref().map(|v| *v.value_balance())
);
Ok(())
}
proptest! {
#[test]
fn tx_serialization_roundtrip(tx in arb_branch_id().prop_flat_map(arb_tx)) {
let mut txn_bytes = vec![];
tx.write(&mut txn_bytes).unwrap();
#[ignore]
fn tx_serialization_roundtrip_sprout(tx in arb_tx(BranchId::Sprout)) {
check_roundtrip(BranchId::Sprout, tx)?;
}
}
let txo = Transaction::read(&txn_bytes[..], BranchId::Canopy).unwrap();
proptest! {
#[test]
#[ignore]
fn tx_serialization_roundtrip_overwinter(tx in arb_tx(BranchId::Overwinter)) {
check_roundtrip(BranchId::Overwinter, tx)?;
}
}
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")]
prop_assert_eq!(tx.tze_bundle.as_ref(), txo.tze_bundle.as_ref());
proptest! {
#[test]
#[ignore]
fn tx_serialization_roundtrip_sapling(tx in arb_tx(BranchId::Sapling)) {
check_roundtrip(BranchId::Sapling, tx)?;
}
}
proptest! {
#[test]
#[ignore]
fn tx_serialization_roundtrip_blossom(tx in arb_tx(BranchId::Blossom)) {
check_roundtrip(BranchId::Blossom, tx)?;
}
}
proptest! {
#[test]
#[ignore]
fn tx_serialization_roundtrip_heartwood(tx in arb_tx(BranchId::Heartwood)) {
check_roundtrip(BranchId::Heartwood, tx)?;
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(20))]
#[test]
fn tx_serialization_roundtrip_canopy(tx in arb_tx(BranchId::Canopy)) {
check_roundtrip(BranchId::Canopy, tx)?;
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(20))]
#[test]
fn tx_serialization_roundtrip_nu5(tx in arb_tx(BranchId::Nu5)) {
check_roundtrip(BranchId::Nu5, tx)?;
}
}
#[cfg(feature = "zfuture")]
proptest! {
#[test]
#[ignore]
fn tx_serialization_roundtrip_future(tx in arb_tx(BranchId::ZFuture)) {
check_roundtrip(BranchId::ZFuture, tx)?;
}
}
@ -54,7 +124,7 @@ fn zip_0143() {
};
assert_eq!(
v4_signature_hash(&tx, signable_input, tv.hash_type).as_ref(),
v4_signature_hash(tx.deref(), signable_input, tv.hash_type).as_ref(),
tv.sighash
);
}
@ -74,7 +144,7 @@ fn zip_0243() {
};
assert_eq!(
v4_signature_hash(&tx, signable_input, tv.hash_type).as_ref(),
v4_signature_hash(tx.deref(), signable_input, tv.hash_type).as_ref(),
tv.sighash
);
}

View File

@ -16,6 +16,10 @@ impl<R: Read> HashReader<R> {
}
}
pub fn into_base_reader(self) -> R {
self.reader
}
/// Destroy this reader and return the hash of what was read.
pub fn into_hash(self) -> Output<Sha256> {
Sha256::digest(&self.hasher.finalize())