Implement Sapling serialization in Transaction V5 (#2020)

* serialize/deserialize spaling shielded data in v5 transaction

* fix serialize/deserialize fields order according to spec

* remove extra clone calls

* more serialize fixes

* clippy: fix empty array

* tidy comments

* Add v4 and v5 transaction tests

Also make sure that serialized bytes match if structs match.

* Test fake v5 blocks made out of pre-NU5 block test vectors

* Add outputs-only tests for v5 shared anchor serialization

* Refactor sapling::ShieldedData V5 serialization into its own impl

* Fix spec name typos

* Simplify sapling shielded data parsing

* Delete redundant V5 transaction wrappers in tests

And split out sapling ShieldedData serialization.

Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
This commit is contained in:
teor 2021-04-19 08:09:57 +10:00 committed by GitHub
parent 32285faf56
commit b9ac221ad4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 831 additions and 43 deletions

10
Cargo.lock generated
View File

@ -1579,6 +1579,15 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135"
[[package]]
name = "itertools"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.6"
@ -4044,6 +4053,7 @@ dependencies = [
"equihash",
"futures 0.3.14",
"hex",
"itertools",
"jubjub",
"lazy_static",
"primitive-types",

View File

@ -46,9 +46,13 @@ redjubjub = "0.4"
[dev-dependencies]
bincode = "1"
color-eyre = "0.5.11"
spandoc = "0.2"
tracing = "0.1.25"
itertools = "0.10.0"
proptest = "0.10"
proptest-derive = "0.3"

View File

@ -19,7 +19,12 @@ proptest! {
let bytes = hash.zcash_serialize_to_vec()?;
let other_hash: Hash = bytes.zcash_deserialize_into()?;
prop_assert_eq![hash, other_hash];
prop_assert_eq![&hash, &other_hash];
let bytes2 = other_hash
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![bytes, bytes2, "bytes must be equal if structs are equal"];
}
#[test]
@ -38,7 +43,12 @@ proptest! {
let bytes = header.zcash_serialize_to_vec()?;
let other_header = bytes.zcash_deserialize_into()?;
prop_assert_eq![header, other_header];
prop_assert_eq![&header, &other_header];
let bytes2 = other_header
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![bytes, bytes2, "bytes must be equal if structs are equal"];
}
#[test]
@ -72,7 +82,6 @@ proptest! {
zebra_test::init();
let bytes = block.zcash_serialize_to_vec()?;
let bytes = &mut bytes.as_slice();
// Check the block commitment
let commitment = block.commitment(network);
@ -86,7 +95,12 @@ proptest! {
// Check deserialization
let other_block = bytes.zcash_deserialize_into()?;
prop_assert_eq![block, other_block];
prop_assert_eq![&block, &other_block];
let bytes2 = other_block
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![bytes, bytes2, "bytes must be equal if structs are equal"];
} else {
let serialization_err = bytes.zcash_deserialize_into::<Block>()
.expect_err("blocks larger than the maximum size should fail");

View File

@ -72,14 +72,15 @@ fn deserialize_blockheader() {
}
#[test]
fn deserialize_block() {
fn round_trip_blocks() {
zebra_test::init();
// this one has a bad version field
// this one has a bad version field, but it is still valid
zebra_test::vectors::BLOCK_MAINNET_434873_BYTES
.zcash_deserialize_into::<Block>()
.expect("block test vector should deserialize");
.expect("bad version block test vector should deserialize");
// now do a round-trip test on all the block test vectors
for block_bytes in zebra_test::vectors::BLOCKS.iter() {
let block = block_bytes
.zcash_deserialize_into::<Block>()

View File

@ -5,23 +5,23 @@ mod address;
mod arbitrary;
mod commitment;
mod note;
mod output;
mod spend;
#[cfg(test)]
mod tests;
// XXX clean up these modules
pub mod keys;
pub mod output;
pub mod shielded_data;
pub mod spend;
pub mod tree;
pub use address::Address;
pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment};
pub use keys::Diversifier;
pub use note::{EncryptedNote, Note, Nullifier, WrappedNoteKey};
pub use output::{Output, OutputInTransactionV4};
pub use output::{Output, OutputInTransactionV4, OutputPrefixInTransactionV5};
pub use shielded_data::{
AnchorVariant, FieldNotPresent, PerSpendAnchor, SharedAnchor, ShieldedData,
};
pub use spend::Spend;
pub use spend::{Spend, SpendPrefixInTransactionV5};

View File

@ -20,7 +20,12 @@ proptest! {
let data = spend.zcash_serialize_to_vec().expect("spend should serialize");
let spend_parsed = data.zcash_deserialize_into().expect("randomized spend should deserialize");
prop_assert_eq![spend, spend_parsed];
prop_assert_eq![&spend, &spend_parsed];
let data2 = spend_parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
}
/// Serialize and deserialize `Spend<SharedAnchor>`
@ -34,15 +39,30 @@ proptest! {
let data = prefix.zcash_serialize_to_vec().expect("spend prefix should serialize");
let parsed = data.zcash_deserialize_into().expect("randomized spend prefix should deserialize");
prop_assert_eq![prefix, parsed];
prop_assert_eq![&prefix, &parsed];
let data2 = parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
let data = zkproof.zcash_serialize_to_vec().expect("spend zkproof should serialize");
let parsed = data.zcash_deserialize_into().expect("randomized spend zkproof should deserialize");
prop_assert_eq![zkproof, parsed];
prop_assert_eq![&zkproof, &parsed];
let data2 = parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
let data = spend_auth_sig.zcash_serialize_to_vec().expect("spend auth sig should serialize");
let parsed = data.zcash_deserialize_into().expect("randomized spend auth sig should deserialize");
prop_assert_eq![spend_auth_sig, parsed];
prop_assert_eq![&spend_auth_sig, &parsed];
let data2 = parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
}
/// Serialize and deserialize `Output`
@ -57,25 +77,38 @@ proptest! {
let output_parsed = data.zcash_deserialize_into::<OutputInTransactionV4>().expect("randomized output should deserialize").into_output();
prop_assert_eq![&output, &output_parsed];
let data2 = output_parsed
.into_v4()
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
// v5 format
let (prefix, zkproof) = output.into_v5_parts();
let data = prefix.zcash_serialize_to_vec().expect("output prefix should serialize");
let parsed = data.zcash_deserialize_into().expect("randomized output prefix should deserialize");
prop_assert_eq![prefix, parsed];
prop_assert_eq![&prefix, &parsed];
let data2 = parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
let data = zkproof.zcash_serialize_to_vec().expect("output zkproof should serialize");
let parsed = data.zcash_deserialize_into().expect("randomized output zkproof should deserialize");
prop_assert_eq![zkproof, parsed];
prop_assert_eq![&zkproof, &parsed];
let data2 = parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
}
}
proptest! {
/// Serialize and deserialize `PerSpendAnchor` shielded data by including it
/// in a V4 transaction
//
// TODO: write a similar test for `ShieldedData<SharedAnchor>` (#1829)
#[test]
fn shielded_data_v4_roundtrip(
shielded_v4 in any::<sapling::ShieldedData<PerSpendAnchor>>(),
@ -96,15 +129,121 @@ proptest! {
};
let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
let tx_parsed = data.zcash_deserialize_into().expect("randomized tx should deserialize");
prop_assert_eq![tx, tx_parsed];
prop_assert_eq![&tx, &tx_parsed];
let data2 = tx_parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
}
/// Serialize and deserialize `SharedAnchor` shielded data
#[test]
fn shielded_data_v5_roundtrip(
shielded_v5 in any::<sapling::ShieldedData<SharedAnchor>>(),
) {
zebra_test::init();
let data = shielded_v5.zcash_serialize_to_vec().expect("shielded_v5 should serialize");
let shielded_v5_parsed = data.zcash_deserialize_into().expect("randomized shielded_v5 should deserialize");
if let Some(shielded_v5_parsed) = shielded_v5_parsed {
prop_assert_eq![&shielded_v5,
&shielded_v5_parsed];
let data2 = shielded_v5_parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
} else {
panic!("unexpected parsing error: ShieldedData should be Some(_)");
}
}
/// Test v4 with empty spends, but some outputs
#[test]
fn shielded_data_v4_outputs_only(
shielded_v4 in any::<sapling::ShieldedData<PerSpendAnchor>>(),
) {
use Either::*;
zebra_test::init();
// we need at least one output to delete all the spends
prop_assume!(shielded_v4.outputs().count() > 0);
// TODO: modify the strategy, rather than the shielded data
let mut shielded_v4 = shielded_v4;
let mut outputs: Vec<_> = shielded_v4.outputs().cloned().collect();
shielded_v4.rest_spends = Vec::new();
shielded_v4.first = Right(outputs.remove(0));
shielded_v4.rest_outputs = outputs;
// shielded data doesn't serialize by itself, so we have to stick it in
// a transaction
// stick `PerSpendAnchor` shielded data into a v4 transaction
let tx = Transaction::V4 {
inputs: Vec::new(),
outputs: Vec::new(),
lock_time: LockTime::min_lock_time(),
expiry_height: block::Height(0),
joinsplit_data: None,
sapling_shielded_data: Some(shielded_v4),
};
let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
let tx_parsed = data.zcash_deserialize_into().expect("randomized tx should deserialize");
prop_assert_eq![&tx, &tx_parsed];
let data2 = tx_parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
}
/// Test the v5 shared anchor serialization condition: empty spends, but some outputs
#[test]
fn shielded_data_v5_outputs_only(
shielded_v5 in any::<sapling::ShieldedData<SharedAnchor>>(),
) {
use Either::*;
zebra_test::init();
// we need at least one output to delete all the spends
prop_assume!(shielded_v5.outputs().count() > 0);
// TODO: modify the strategy, rather than the shielded data
let mut shielded_v5 = shielded_v5;
let mut outputs: Vec<_> = shielded_v5.outputs().cloned().collect();
shielded_v5.rest_spends = Vec::new();
shielded_v5.first = Right(outputs.remove(0));
shielded_v5.rest_outputs = outputs;
// TODO: delete the shared anchor when there are no spends
shielded_v5.shared_anchor = Default::default();
let data = shielded_v5.zcash_serialize_to_vec().expect("shielded_v5 should serialize");
let shielded_v5_parsed = data.zcash_deserialize_into().expect("randomized shielded_v5 should deserialize");
if let Some(shielded_v5_parsed) = shielded_v5_parsed {
prop_assert_eq![&shielded_v5,
&shielded_v5_parsed];
let data2 = shielded_v5_parsed
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
} else {
panic!("unexpected parsing error: ShieldedData should be Some(_)");
}
}
/// Check that ShieldedData<PerSpendAnchor> is equal when `first` is swapped
/// between a spend and an output
//
// TODO: write a similar test for `ShieldedData<SharedAnchor>` (#1829)
#[test]
fn shielded_data_per_spend_swap_first_eq(shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>()) {
fn shielded_data_per_spend_swap_first_eq(
shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>()
) {
use Either::*;
zebra_test::init();
@ -157,12 +296,50 @@ proptest! {
prop_assert_eq![data1, data2];
}
/// Check that ShieldedData<SharedAnchor> is equal when `first` is swapped
/// between a spend and an output
#[test]
fn shielded_data_shared_swap_first_eq(
shielded1 in any::<sapling::ShieldedData<SharedAnchor>>()
) {
use Either::*;
zebra_test::init();
// we need at least one spend and one output to swap them
prop_assume!(shielded1.spends().count() > 0 && shielded1.outputs().count() > 0);
let mut shielded2 = shielded1.clone();
let mut spends: Vec<_> = shielded2.spends().cloned().collect();
let mut outputs: Vec<_> = shielded2.outputs().cloned().collect();
match shielded2.first {
Left(_spend) => {
shielded2.first = Right(outputs.remove(0));
shielded2.rest_outputs = outputs;
shielded2.rest_spends = spends;
}
Right(_output) => {
shielded2.first = Left(spends.remove(0));
shielded2.rest_spends = spends;
shielded2.rest_outputs = outputs;
}
}
prop_assert_eq![&shielded1, &shielded2];
let data1 = shielded1.zcash_serialize_to_vec().expect("shielded1 should serialize");
let data2 = shielded2.zcash_serialize_to_vec().expect("shielded2 should serialize");
prop_assert_eq![data1, data2];
}
/// Check that ShieldedData<PerSpendAnchor> serialization is equal if
/// `shielded1 == shielded2`
//
// TODO: write a similar test for `ShieldedData<SharedAnchor>` (#1829)
#[test]
fn shielded_data_per_spend_serialize_eq(shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>(), shielded2 in any::<sapling::ShieldedData<PerSpendAnchor>>()) {
fn shielded_data_per_spend_serialize_eq(
shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>(),
shielded2 in any::<sapling::ShieldedData<PerSpendAnchor>>()
) {
zebra_test::init();
let shielded_eq = shielded1 == shielded2;
@ -202,14 +379,36 @@ proptest! {
}
}
/// Check that ShieldedData<SharedAnchor> serialization is equal if
/// `shielded1 == shielded2`
#[test]
fn shielded_data_shared_serialize_eq(
shielded1 in any::<sapling::ShieldedData<SharedAnchor>>(),
shielded2 in any::<sapling::ShieldedData<SharedAnchor>>()
) {
zebra_test::init();
let shielded_eq = shielded1 == shielded2;
let data1 = shielded1.zcash_serialize_to_vec().expect("shielded1 should serialize");
let data2 = shielded2.zcash_serialize_to_vec().expect("shielded2 should serialize");
if shielded_eq {
prop_assert_eq![data1, data2];
} else {
prop_assert_ne![data1, data2];
}
}
/// Check that ShieldedData<PerSpendAnchor> serialization is equal when we
/// replace all the known fields.
///
/// This test checks for extra fields that are not in `ShieldedData::eq`.
//
// TODO: write a similar test for `ShieldedData<SharedAnchor>` (#1829)
#[test]
fn shielded_data_per_spend_field_assign_eq(shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>(), shielded2 in any::<sapling::ShieldedData<PerSpendAnchor>>()) {
fn shielded_data_per_spend_field_assign_eq(
shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>(),
shielded2 in any::<sapling::ShieldedData<PerSpendAnchor>>()
) {
zebra_test::init();
let mut shielded2 = shielded2;
@ -252,4 +451,37 @@ proptest! {
prop_assert_eq![data1, data2];
}
/// Check that ShieldedData<SharedAnchor> serialization is equal when we
/// replace all the known fields.
///
/// This test checks for extra fields that are not in `ShieldedData::eq`.
#[test]
fn shielded_data_shared_field_assign_eq(
shielded1 in any::<sapling::ShieldedData<SharedAnchor>>(),
shielded2 in any::<sapling::ShieldedData<SharedAnchor>>()
) {
zebra_test::init();
let mut shielded2 = shielded2;
// TODO: modify the strategy, rather than the shielded data
//
// these fields must match ShieldedData::eq
// the spends() and outputs() checks cover first, rest_spends, and rest_outputs
shielded2.first = shielded1.first.clone();
shielded2.rest_spends = shielded1.rest_spends.clone();
shielded2.rest_outputs = shielded1.rest_outputs.clone();
// now for the fields that are checked literally
shielded2.value_balance = shielded1.value_balance;
shielded2.shared_anchor = shielded1.shared_anchor;
shielded2.binding_sig = shielded1.binding_sig;
prop_assert_eq![&shielded1, &shielded2];
let data1 = shielded1.zcash_serialize_to_vec().expect("shielded1 should serialize");
let data2 = shielded2.zcash_serialize_to_vec().expect("shielded2 should serialize");
prop_assert_eq![data1, data2];
}
}

View File

@ -257,7 +257,7 @@ impl Arbitrary for sapling::ShieldedData<sapling::SharedAnchor> {
)
.prop_map(
|(value_balance, shared_anchor, first, rest_spends, rest_outputs, sig_bytes)| {
Self {
let mut shielded_data = Self {
value_balance,
shared_anchor,
first,
@ -268,7 +268,12 @@ impl Arbitrary for sapling::ShieldedData<sapling::SharedAnchor> {
b.copy_from_slice(sig_bytes.as_slice());
b
}),
};
if shielded_data.spends().count() == 0 {
// Todo: delete field when there is no spend
shielded_data.shared_anchor = Default::default();
}
shielded_data
},
)
.boxed()

View File

@ -8,16 +8,17 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crate::{
block::MAX_BLOCK_BYTES,
parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID},
primitives::ZkSnarkProof,
primitives::{Groth16Proof, ZkSnarkProof},
serialization::{
ReadZcashExt, SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize,
zcash_deserialize_external_count, zcash_serialize_external_count, ReadZcashExt,
SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize,
ZcashDeserializeInto, ZcashSerialize,
},
sprout,
};
use super::*;
use sapling::Output;
use sapling::{Output, SharedAnchor, Spend};
impl ZcashDeserialize for jubjub::Fq {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
@ -32,6 +33,11 @@ impl ZcashDeserialize for jubjub::Fq {
}
}
}
// Transaction V3 and V4 serialize sprout JoinSplitData in a single continuous
// byte range, so we can implement its serialization and deserialization
// separately.
impl<P: ZkSnarkProof> ZcashSerialize for JoinSplitData<P> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_compactsize(self.joinsplits().count() as u64)?;
@ -68,6 +74,160 @@ impl<P: ZkSnarkProof> ZcashDeserialize for Option<JoinSplitData<P>> {
}
}
// Transaction::V5 serializes sapling ShieldedData in a single continuous byte
// range, so we can implement its serialization and deserialization separately.
// (Unlike V4, where it must be serialized as part of the transaction.)
impl ZcashSerialize for Option<sapling::ShieldedData<SharedAnchor>> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
match self {
None => {
// nSpendsSapling
writer.write_compactsize(0)?;
// nOutputsSapling
writer.write_compactsize(0)?;
}
Some(shielded_data) => {
shielded_data.zcash_serialize(&mut writer)?;
}
}
Ok(())
}
}
impl ZcashSerialize for sapling::ShieldedData<SharedAnchor> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
// Collect arrays for Spends
// There's no unzip3, so we have to unzip twice.
let (spend_prefixes, spend_proofs_sigs): (Vec<_>, Vec<_>) = self
.spends()
.cloned()
.map(sapling::Spend::<SharedAnchor>::into_v5_parts)
.map(|(prefix, proof, sig)| (prefix, (proof, sig)))
.unzip();
let (spend_proofs, spend_sigs) = spend_proofs_sigs.into_iter().unzip();
// Collect arrays for Outputs
let (output_prefixes, output_proofs): (Vec<_>, _) =
self.outputs().cloned().map(Output::into_v5_parts).unzip();
// nSpendsSapling and vSpendsSapling
spend_prefixes.zcash_serialize(&mut writer)?;
// nOutputsSapling and vOutputsSapling
output_prefixes.zcash_serialize(&mut writer)?;
// valueBalanceSapling
self.value_balance.zcash_serialize(&mut writer)?;
// anchorSapling
if !spend_prefixes.is_empty() {
writer.write_all(&<[u8; 32]>::from(self.shared_anchor)[..])?;
}
// vSpendProofsSapling
zcash_serialize_external_count(&spend_proofs, &mut writer)?;
// vSpendAuthSigsSapling
zcash_serialize_external_count(&spend_sigs, &mut writer)?;
// vOutputProofsSapling
zcash_serialize_external_count(&output_proofs, &mut writer)?;
// bindingSigSapling
writer.write_all(&<[u8; 64]>::from(self.binding_sig)[..])?;
Ok(())
}
}
// we can't split ShieldedData out of Option<ShieldedData> deserialization,
// because the counts are read along with the arrays.
impl ZcashDeserialize for Option<sapling::ShieldedData<SharedAnchor>> {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
// nSpendsSapling and vSpendsSapling
let spend_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?;
// nOutputsSapling and vOutputsSapling
let output_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?;
// nSpendsSapling and nOutputsSapling as variables
let spends_count = spend_prefixes.len();
let outputs_count = output_prefixes.len();
// All the other fields depend on having spends or outputs
if spend_prefixes.is_empty() && output_prefixes.is_empty() {
return Ok(None);
}
// valueBalanceSapling
let value_balance = (&mut reader).zcash_deserialize_into()?;
// anchorSapling
let mut shared_anchor = None;
if spends_count > 0 {
shared_anchor = Some(reader.read_32_bytes()?.into());
}
// vSpendProofsSapling
let spend_proofs = zcash_deserialize_external_count(spends_count, &mut reader)?;
// vSpendAuthSigsSapling
let spend_sigs = zcash_deserialize_external_count(spends_count, &mut reader)?;
// vOutputProofsSapling
let output_proofs = zcash_deserialize_external_count(outputs_count, &mut reader)?;
// bindingSigSapling
let binding_sig = reader.read_64_bytes()?.into();
// Create shielded spends from deserialized parts
let mut spends: Vec<_> = spend_prefixes
.into_iter()
.zip(spend_proofs.into_iter())
.zip(spend_sigs.into_iter())
.map(|((prefix, proof), sig)| Spend::<SharedAnchor>::from_v5_parts(prefix, proof, sig))
.collect();
// Create shielded outputs from deserialized parts
let mut outputs = output_prefixes
.into_iter()
.zip(output_proofs.into_iter())
.map(|(prefix, proof)| Output::from_v5_parts(prefix, proof))
.collect();
// Create shielded data
use futures::future::Either::*;
// TODO: Use a Spend for first if both are present, because the first
// spend activates the shared anchor.
if spends_count > 0 {
Ok(Some(sapling::ShieldedData {
value_balance,
// TODO: cleanup shared anchor parsing
shared_anchor: shared_anchor.expect("present when spends_count > 0"),
first: Left(spends.remove(0)),
rest_spends: spends,
rest_outputs: outputs,
binding_sig,
}))
} else {
assert!(
outputs_count > 0,
"parsing returns early when there are no spends and no outputs"
);
Ok(Some(sapling::ShieldedData {
value_balance,
// TODO: delete shared anchor when there are no spends
shared_anchor: shared_anchor.unwrap_or_default(),
first: Right(outputs.remove(0)),
// the spends are actually empty here, but we use the
// vec for consistency and readability
rest_spends: spends,
rest_outputs: outputs,
binding_sig,
}))
}
}
}
impl ZcashSerialize for Transaction {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
// Post-Sapling, transaction size is limited to MAX_BLOCK_BYTES.
@ -187,8 +347,7 @@ impl ZcashSerialize for Transaction {
None => {}
}
}
// TODO: serialize sapling shielded data according to the V5 transaction spec
#[allow(unused_variables)]
Transaction::V5 {
lock_time,
expiry_height,
@ -197,17 +356,26 @@ impl ZcashSerialize for Transaction {
sapling_shielded_data,
rest,
} => {
// Write version 5 and set the fOverwintered bit.
// Transaction V5 spec:
// https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus
// header: Write version 5 and set the fOverwintered bit
writer.write_u32::<LittleEndian>(5 | (1 << 31))?;
writer.write_u32::<LittleEndian>(TX_V5_VERSION_GROUP_ID)?;
// transaction validity time and height limits
lock_time.zcash_serialize(&mut writer)?;
writer.write_u32::<LittleEndian>(expiry_height.0)?;
// transparent
inputs.zcash_serialize(&mut writer)?;
outputs.zcash_serialize(&mut writer)?;
// TODO: serialize sapling shielded data according to the V5 transaction spec
// sapling
sapling_shielded_data.zcash_serialize(&mut writer)?;
// write the rest
// orchard
// TODO: parse orchard into structs
writer.write_all(rest)?;
}
}
@ -326,17 +494,25 @@ impl ZcashDeserialize for Transaction {
})
}
(5, true) => {
// header
let id = reader.read_u32::<LittleEndian>()?;
if id != TX_V5_VERSION_GROUP_ID {
return Err(SerializationError::Parse("expected TX_V5_VERSION_GROUP_ID"));
}
// transaction validity time and height limits
let lock_time = LockTime::zcash_deserialize(&mut reader)?;
let expiry_height = block::Height(reader.read_u32::<LittleEndian>()?);
// transparent
let inputs = Vec::zcash_deserialize(&mut reader)?;
let outputs = Vec::zcash_deserialize(&mut reader)?;
// TODO: deserialize sapling shielded data according to the V5 transaction spec
// sapling
let sapling_shielded_data = (&mut reader).zcash_deserialize_into()?;
// orchard
// TODO: parse orchard into structs
let mut rest = Vec::new();
reader.read_to_end(&mut rest)?;
@ -345,8 +521,7 @@ impl ZcashDeserialize for Transaction {
expiry_height,
inputs,
outputs,
// TODO: use deserialized sapling shielded data
sapling_shielded_data: None,
sapling_shielded_data,
rest,
})
}

View File

@ -13,7 +13,13 @@ proptest! {
let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
let tx2 = data.zcash_deserialize_into().expect("randomized tx should deserialize");
prop_assert_eq![tx, tx2];
prop_assert_eq![&tx, &tx2];
let data2 = tx2
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
prop_assert_eq![data, data2, "data must be equal if structs are equal"];
}
#[test]

View File

@ -1,6 +1,14 @@
use super::super::*;
use crate::serialization::{ZcashDeserialize, ZcashSerialize};
use crate::{
block::Block,
sapling::{PerSpendAnchor, SharedAnchor},
serialization::{WriteZcashExt, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
};
use block::MAX_BLOCK_BYTES;
use itertools::Itertools;
use std::convert::TryInto;
#[test]
fn librustzcash_tx_deserialize_and_round_trip() {
@ -85,3 +93,336 @@ fn zip243_deserialize_and_round_trip() {
assert_eq!(&zebra_test::vectors::ZIP243_3[..], &data3[..]);
}
// Transaction V5 test vectors
/// An empty transaction v5, with no Orchard, Sapling, or Transparent data
///
/// empty transaction are invalid, but Zebra only checks this rule in
/// zebra_consensus::transaction::Verifier
#[test]
fn empty_v5_round_trip() {
zebra_test::init();
let tx = Transaction::V5 {
lock_time: LockTime::min_lock_time(),
expiry_height: block::Height(0),
inputs: Vec::new(),
outputs: Vec::new(),
sapling_shielded_data: None,
rest: empty_v5_orchard_data(),
};
let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
let tx2 = data
.zcash_deserialize_into()
.expect("tx should deserialize");
assert_eq!(tx, tx2);
let data2 = tx2
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_eq!(data, data2, "data must be equal if structs are equal");
}
/// An empty transaction v4, with no Sapling, Sprout, or Transparent data
///
/// empty transaction are invalid, but Zebra only checks this rule in
/// zebra_consensus::transaction::Verifier
#[test]
fn empty_v4_round_trip() {
zebra_test::init();
let tx = Transaction::V4 {
inputs: Vec::new(),
outputs: Vec::new(),
lock_time: LockTime::min_lock_time(),
expiry_height: block::Height(0),
joinsplit_data: None,
sapling_shielded_data: None,
};
let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
let tx2 = data
.zcash_deserialize_into()
.expect("tx should deserialize");
assert_eq!(tx, tx2);
let data2 = tx2
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_eq!(data, data2, "data must be equal if structs are equal");
}
/// Do a round-trip test on fake v5 transactions created from v4 transactions
/// in the block test vectors.
///
/// Covers Sapling only, Transparent only, and Sapling/Transparent v5
/// transactions.
#[test]
fn fake_v5_round_trip() {
zebra_test::init();
for original_bytes in zebra_test::vectors::BLOCKS.iter() {
let original_block = original_bytes
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
// skip this block if it only contains v5 transactions,
// the block round-trip test covers it already
if original_block
.transactions
.iter()
.all(|trans| matches!(trans.as_ref(), &Transaction::V5 { .. }))
{
continue;
}
let mut fake_block = original_block.clone();
fake_block.transactions = fake_block
.transactions
.iter()
.map(AsRef::as_ref)
.map(transaction_to_fake_v5)
.map(Into::into)
.collect();
// test each transaction
for (original_tx, fake_tx) in original_block
.transactions
.iter()
.zip(fake_block.transactions.iter())
{
assert_ne!(
&original_tx, &fake_tx,
"v1-v4 transactions must change when converted to fake v5"
);
let fake_bytes = fake_tx
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_ne!(
&original_bytes[..],
fake_bytes,
"v1-v4 transaction data must change when converted to fake v5"
);
let fake_tx2 = fake_bytes
.zcash_deserialize_into::<Transaction>()
.expect("tx is structurally valid");
assert_eq!(fake_tx.as_ref(), &fake_tx2);
let fake_bytes2 = fake_tx2
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_eq!(
fake_bytes, fake_bytes2,
"data must be equal if structs are equal"
);
}
// test full blocks
assert_ne!(
&original_block, &fake_block,
"v1-v4 transactions must change when converted to fake v5"
);
let fake_bytes = fake_block
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_ne!(
&original_bytes[..],
fake_bytes,
"v1-v4 transaction data must change when converted to fake v5"
);
// skip fake blocks which exceed the block size limit
// because of the changes we made
if fake_bytes.len() > MAX_BLOCK_BYTES.try_into().unwrap() {
continue;
}
let fake_block2 = match fake_bytes.zcash_deserialize_into::<Block>() {
Ok(fake_block2) => fake_block2,
Err(err) => {
// TODO: work out why transaction parsing succeeds,
// but block parsing doesn't
tracing::info!(
?err,
?original_block,
?fake_block,
hex_original_bytes = ?hex::encode(&original_bytes),
hex_fake_bytes = ?hex::encode(&fake_bytes),
original_bytes_len = %original_bytes.len(),
fake_bytes_len = %fake_bytes.len(),
%MAX_BLOCK_BYTES,
"unexpected structurally invalid block during deserialization"
);
continue;
}
};
assert_eq!(fake_block, fake_block2);
let fake_bytes2 = fake_block2
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_eq!(
fake_bytes, fake_bytes2,
"data must be equal if structs are equal"
);
}
}
// Utility functions
/// Return serialized empty Transaction::V5 Orchard data.
///
/// TODO: replace with orchard::ShieldedData (#1979)
fn empty_v5_orchard_data() -> Vec<u8> {
let mut buf = Vec::new();
// nActionsOrchard
buf.write_compactsize(0)
.expect("serialize to Vec always succeeds");
// all other orchard fields are only present when `nActionsOrchard > 0`
buf
}
/// Convert `trans` into a fake v5 transaction,
/// converting sapling shielded data from v4 to v5 if possible.
fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
use Transaction::*;
match trans {
V1 {
inputs,
outputs,
lock_time,
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: block::Height(0),
sapling_shielded_data: None,
rest: empty_v5_orchard_data(),
},
V2 {
inputs,
outputs,
lock_time,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: block::Height(0),
sapling_shielded_data: None,
rest: empty_v5_orchard_data(),
},
V3 {
inputs,
outputs,
lock_time,
expiry_height,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: *expiry_height,
sapling_shielded_data: None,
rest: empty_v5_orchard_data(),
},
V4 {
inputs,
outputs,
lock_time,
expiry_height,
sapling_shielded_data,
..
} => V5 {
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
lock_time: *lock_time,
expiry_height: *expiry_height,
sapling_shielded_data: sapling_shielded_data
.clone()
.map(sapling_shielded_v4_to_fake_v5)
.flatten(),
rest: empty_v5_orchard_data(),
},
v5 @ V5 { .. } => v5.clone(),
}
}
/// Convert a v4 sapling shielded data into a fake v5 sapling shielded data,
/// if possible.
fn sapling_shielded_v4_to_fake_v5(
v4_shielded: sapling::ShieldedData<PerSpendAnchor>,
) -> Option<sapling::ShieldedData<SharedAnchor>> {
use futures::future::Either::*;
use sapling::ShieldedData;
let unique_anchors: Vec<_> = v4_shielded
.spends()
.map(|spend| spend.per_spend_anchor)
.unique()
.collect();
let shared_anchor = match unique_anchors.as_slice() {
[unique_anchor] => *unique_anchor,
// TODO: remove shared anchor when there are no spends
[] => Default::default(),
// Multiple different anchors, can't convert to v5
_ => return None,
};
let first = match v4_shielded.first {
Left(spend) => Left(sapling_spend_v4_to_fake_v5(spend)),
Right(output) => Right(output),
};
let fake_shielded_v5 = ShieldedData::<SharedAnchor> {
value_balance: v4_shielded.value_balance,
shared_anchor,
first,
rest_spends: v4_shielded
.rest_spends
.iter()
.cloned()
.map(sapling_spend_v4_to_fake_v5)
.collect(),
rest_outputs: v4_shielded.rest_outputs,
binding_sig: v4_shielded.binding_sig,
};
Some(fake_shielded_v5)
}
/// Convert a v4 sapling spend into a fake v5 sapling spend.
fn sapling_spend_v4_to_fake_v5(
v4_spend: sapling::Spend<PerSpendAnchor>,
) -> sapling::Spend<SharedAnchor> {
use sapling::Spend;
Spend::<SharedAnchor> {
cv: v4_spend.cv,
per_spend_anchor: FieldNotPresent,
nullifier: v4_spend.nullifier,
rk: v4_spend.rk,
zkproof: v4_spend.zkproof,
spend_auth_sig: v4_spend.spend_auth_sig,
}
}