Orchard data in V5 parsing (#2116)

* initialize the work on parsing orchard data in V5
* add the rest of orchard serialization
* fix serialization according to spec
* fix arbitrary for Signature<SpendAuth>
* move deserialization of AuthorizedAction to shielded_data module
* use `from_bits_truncate` to generate valid arbitrary flags
* change panic message
* fix serialization/deserialization when nActionsOrchard is empty
* fix Halo2Proof deserialization
* implement ZcashSerialize and ZcashDeserialize for flags
* implement ZcashSerialize and ZcashDeserialize for orchard::tree::Root
* use ZcashSerialize and ZcashDeserialize for binding_sig
* implement from_parts()
* implement Arbitrary for  Signature<Binding>
* add trusted preallocate with tests
* fix Arbitrary for orchard Nullifier
* Use zcash_serialize_bytes instead of write_compactsize

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Alfredo Garcia 2021-05-20 21:42:06 -03:00 committed by GitHub
parent 40d06657b3
commit a57c09a3b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 552 additions and 79 deletions

View File

@ -18,6 +18,9 @@
#[macro_use]
extern crate serde;
#[macro_use]
extern crate bitflags;
pub mod amount;
pub mod block;
pub mod fmt;

View File

@ -9,8 +9,11 @@ mod arbitrary;
mod commitment;
mod note;
mod sinsemilla;
#[cfg(test)]
mod tests;
pub mod keys;
pub mod shielded_data;
pub mod tree;
pub use action::Action;
@ -18,3 +21,4 @@ pub use address::Address;
pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment};
pub use keys::Diversifier;
pub use note::{EncryptedNote, Note, Nullifier};
pub use shielded_data::{AuthorizedAction, Flags, ShieldedData};

View File

@ -2,7 +2,11 @@ use group::prime::PrimeCurveAffine;
use halo2::pasta::pallas;
use proptest::{arbitrary::any, array, prelude::*};
use super::{keys, note, Action, NoteCommitment, ValueCommitment};
use crate::primitives::redpallas::{Signature, SpendAuth, VerificationKeyBytes};
use super::{keys, note, Action, AuthorizedAction, Flags, NoteCommitment, ValueCommitment};
use std::marker::PhantomData;
impl Arbitrary for Action {
type Parameters = ();
@ -18,7 +22,7 @@ impl Arbitrary for Action {
|(nullifier, rpk_bytes, enc_ciphertext, out_ciphertext)| Self {
cv: ValueCommitment(pallas::Affine::identity()),
nullifier,
rk: crate::primitives::redpallas::VerificationKeyBytes::from(rpk_bytes),
rk: VerificationKeyBytes::from(rpk_bytes),
cm_x: NoteCommitment(pallas::Affine::identity()).extract_x(),
ephemeral_key: keys::EphemeralPublicKey(pallas::Affine::identity()),
enc_ciphertext,
@ -35,7 +39,52 @@ impl Arbitrary for note::Nullifier {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
array::uniform32(any::<u8>()).prop_map(Self::from).boxed()
use halo2::arithmetic::FieldExt;
(any::<u64>())
.prop_map(|number| Self::from(pallas::Scalar::from_u64(number).to_bytes()))
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for AuthorizedAction {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
(any::<Action>(), any::<Signature<SpendAuth>>())
.prop_map(|(action, spend_auth_sig)| Self {
action,
spend_auth_sig,
})
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for Signature<SpendAuth> {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
(array::uniform32(any::<u8>()), array::uniform32(any::<u8>()))
.prop_map(|(r_bytes, s_bytes)| Self {
r_bytes,
s_bytes,
_marker: PhantomData,
})
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for Flags {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
(any::<u8>()).prop_map(Self::from_bits_truncate).boxed()
}
type Strategy = BoxedStrategy<Self>;

View File

@ -0,0 +1,142 @@
//! Orchard shielded data for `V5` `Transaction`s.
use crate::{
amount::Amount,
block::MAX_BLOCK_BYTES,
orchard::{tree, Action},
primitives::{
redpallas::{Binding, Signature, SpendAuth},
Halo2Proof,
},
serialization::{
AtLeastOne, SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashSerialize,
},
};
use byteorder::{ReadBytesExt, WriteBytesExt};
use std::{
cmp::{Eq, PartialEq},
fmt::Debug,
io,
};
/// A bundle of [`Action`] descriptions and signature data.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct ShieldedData {
/// The orchard flags for this transaction.
pub flags: Flags,
/// The net value of Orchard spends minus outputs.
pub value_balance: Amount,
/// The shared anchor for all `Spend`s in this transaction.
pub shared_anchor: tree::Root,
/// The aggregated zk-SNARK proof for all the actions in this transaction.
pub proof: Halo2Proof,
/// The Orchard Actions.
pub actions: AtLeastOne<AuthorizedAction>,
/// A signature on the transaction `sighash`.
pub binding_sig: Signature<Binding>,
}
/// An authorized action description.
///
/// Every authorized Orchard `Action` must have a corresponding `SpendAuth` signature.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct AuthorizedAction {
/// The action description of this Action.
pub action: Action,
/// The spend signature.
pub spend_auth_sig: Signature<SpendAuth>,
}
impl AuthorizedAction {
/// Split out the action and the signature for V5 transaction
/// serialization.
pub fn into_parts(self) -> (Action, Signature<SpendAuth>) {
(self.action, self.spend_auth_sig)
}
// Combine the action and the spend auth sig from V5 transaction
/// deserialization.
pub fn from_parts(action: Action, spend_auth_sig: Signature<SpendAuth>) -> AuthorizedAction {
AuthorizedAction {
action,
spend_auth_sig,
}
}
}
/// The size of a single Action
///
/// Actions are 5 * 32 + 580 + 80 bytes so the total size of each Action is 820 bytes.
/// [7.5 Action Description Encoding and Consensus][ps]
///
/// [ps] https://zips.z.cash/protocol/nu5.pdf#actionencodingandconsensus
pub const ACTION_SIZE: u64 = 5 * 32 + 580 + 80;
/// The size of a single Signature<SpendAuth>
///
/// Each Signature is 64 bytes.
/// [7.1 Transaction Encoding and Consensus][ps]
///
/// [ps] https://zips.z.cash/protocol/nu5.pdf#actionencodingandconsensus
pub const SPEND_AUTH_SIG_SIZE: u64 = 64;
/// The size of a single AuthorizedAction
///
/// Each serialized `Action` has a corresponding `Signature<SpendAuth>`.
pub const AUTHORIZED_ACTION_SIZE: u64 = ACTION_SIZE + SPEND_AUTH_SIG_SIZE;
/// The maximum number of orchard actions in a valid Zcash on-chain transaction V5.
///
/// If a transaction contains more actions than can fit in maximally large block, it might be
/// valid on the network and in the mempool, but it can never be mined into a block. So
/// rejecting these large edge-case transactions can never break consensus.
impl TrustedPreallocate for Action {
fn max_allocation() -> u64 {
// Since a serialized Vec<AuthorizedAction> uses at least one byte for its length,
// and the signature is required,
// a valid max allocation can never exceed this size
(MAX_BLOCK_BYTES - 1) / AUTHORIZED_ACTION_SIZE
}
}
impl TrustedPreallocate for Signature<SpendAuth> {
fn max_allocation() -> u64 {
// Each signature must have a corresponding action.
Action::max_allocation()
}
}
bitflags! {
/// Per-Transaction flags for Orchard.
///
/// The spend and output flags are passed to the `Halo2Proof` verifier, which verifies
/// the relevant note spending and creation consensus rules.
#[derive(Deserialize, Serialize)]
pub struct Flags: u8 {
/// Enable spending non-zero valued Orchard notes.
///
/// "the `enableSpendsOrchard` flag, if present, MUST be 0 for coinbase transactions"
const ENABLE_SPENDS = 0b00000001;
/// Enable creating new non-zero valued Orchard notes.
const ENABLE_OUTPUTS = 0b00000010;
// Reserved, zeros (bits 2 .. 7)
}
}
impl ZcashSerialize for Flags {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_u8(self.bits())?;
Ok(())
}
}
impl ZcashDeserialize for Flags {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let flags = Flags::from_bits(reader.read_u8()?).unwrap();
Ok(flags)
}
}

View File

@ -0,0 +1 @@
mod preallocate;

View File

@ -0,0 +1,59 @@
//! Tests for trusted preallocation during deserialization.
use crate::{
block::MAX_BLOCK_BYTES,
orchard::{
shielded_data::{ACTION_SIZE, AUTHORIZED_ACTION_SIZE},
Action, AuthorizedAction,
},
primitives::redpallas::{Signature, SpendAuth},
serialization::{tests::max_allocation_is_big_enough, TrustedPreallocate, ZcashSerialize},
};
use proptest::{prelude::*, proptest};
proptest! {
/// Confirm that each `AuthorizedAction` takes exactly AUTHORIZED_ACTION_SIZE
/// bytes when serialized.
#[test]
fn authorized_action_size_is_small_enough(authorized_action in <AuthorizedAction>::arbitrary_with(())) {
let (action, spend_auth_sig) = authorized_action.into_parts();
let mut serialized_len = action.zcash_serialize_to_vec().expect("Serialization to vec must succeed").len();
serialized_len += spend_auth_sig.zcash_serialize_to_vec().expect("Serialization to vec must succeed").len();
prop_assert!(serialized_len as u64 == AUTHORIZED_ACTION_SIZE)
}
/// Verify trusted preallocation for `AuthorizedAction` and its split fields
#[test]
fn authorized_action_max_allocation_is_big_enough(authorized_action in <AuthorizedAction>::arbitrary_with(())) {
let (action, spend_auth_sig) = authorized_action.into_parts();
let (
smallest_disallowed_vec_len,
smallest_disallowed_serialized_len,
largest_allowed_vec_len,
largest_allowed_serialized_len,
) = max_allocation_is_big_enough(action);
// Calculate the actual size of all required Action fields
prop_assert!((smallest_disallowed_serialized_len as u64)/ACTION_SIZE*AUTHORIZED_ACTION_SIZE >= MAX_BLOCK_BYTES);
prop_assert!((largest_allowed_serialized_len as u64)/ACTION_SIZE*AUTHORIZED_ACTION_SIZE <= MAX_BLOCK_BYTES);
// Check the serialization limits for `Action`
prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Action::max_allocation());
prop_assert!((largest_allowed_vec_len as u64) == Action::max_allocation());
prop_assert!((largest_allowed_serialized_len as u64) <= MAX_BLOCK_BYTES);
let (
smallest_disallowed_vec_len,
_smallest_disallowed_serialized_len,
largest_allowed_vec_len,
largest_allowed_serialized_len,
) = max_allocation_is_big_enough(spend_auth_sig);
// Check the serialization limits for `Signature::<SpendAuth>`
prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Signature::<SpendAuth>::max_allocation());
prop_assert!((largest_allowed_vec_len as u64) == Signature::<SpendAuth>::max_allocation());
prop_assert!((largest_allowed_serialized_len as u64) <= MAX_BLOCK_BYTES);
}
}

View File

@ -13,7 +13,7 @@
#![allow(clippy::unit_arg)]
#![allow(dead_code)]
use std::{collections::VecDeque, fmt};
use std::{collections::VecDeque, fmt, io};
use bitvec::prelude::*;
use halo2::arithmetic::FieldExt;
@ -23,6 +23,8 @@ use proptest_derive::Arbitrary;
use super::{commitment::NoteCommitment, sinsemilla::*};
use crate::serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize};
const MERKLE_DEPTH: usize = 32;
/// MerkleCRH^Orchard Hash Function
@ -88,6 +90,34 @@ impl fmt::Debug for Root {
}
}
impl From<[u8; 32]> for Root {
fn from(bytes: [u8; 32]) -> Root {
Self(bytes)
}
}
impl From<Root> for [u8; 32] {
fn from(root: Root) -> Self {
root.0
}
}
impl ZcashSerialize for Root {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&<[u8; 32]>::from(*self)[..])?;
Ok(())
}
}
impl ZcashDeserialize for Root {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let anchor = reader.read_32_bytes()?.into();
Ok(anchor)
}
}
/// Orchard Note Commitment Tree
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct NoteCommitmentTree {

View File

@ -1,7 +1,9 @@
use serde::{Deserialize, Serialize};
use std::{fmt, io};
use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize};
use crate::serialization::{
zcash_serialize_bytes, SerializationError, ZcashDeserialize, ZcashSerialize,
};
/// An encoding of a Halo2 proof, as used in [Zcash][halo2].
///
@ -21,18 +23,16 @@ impl fmt::Debug for Halo2Proof {
}
impl ZcashSerialize for Halo2Proof {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&self.0[..])?;
Ok(())
fn zcash_serialize<W: io::Write>(&self, writer: W) -> Result<(), io::Error> {
zcash_serialize_bytes(&self.0, writer)
}
}
impl ZcashDeserialize for Halo2Proof {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes)?;
let proof = Vec::zcash_deserialize(&mut reader)?;
Ok(Self(bytes))
Ok(Self(proof))
}
}
#[cfg(any(test, feature = "proptest-impl"))]

View File

@ -45,12 +45,12 @@ pub type Randomizer = pallas::Scalar;
pub trait SigType: private::Sealed {}
/// A type variable corresponding to Zcash's `BindingSig`.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub enum Binding {}
impl SigType for Binding {}
/// A type variable corresponding to Zcash's `SpendAuthSig`.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub enum SpendAuth {}
impl SigType for SpendAuth {}

View File

@ -8,13 +8,14 @@
// - Henry de Valence <hdevalence@hdevalence.ca>
// - Deirdre Connolly <deirdre@zfnd.org>
use std::marker::PhantomData;
use std::{io, marker::PhantomData};
use super::SigType;
use crate::serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize};
/// A RedPallas signature.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct Signature<T: SigType> {
pub(crate) r_bytes: [u8; 32],
pub(crate) s_bytes: [u8; 32],
@ -43,3 +44,16 @@ impl<T: SigType> From<Signature<T>> for [u8; 64] {
bytes
}
}
impl<T: SigType> ZcashSerialize for Signature<T> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&<[u8; 64]>::from(*self)[..])?;
Ok(())
}
}
impl<T: SigType> ZcashDeserialize for Signature<T> {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
Ok(reader.read_64_bytes()?.into())
}
}

View File

@ -14,11 +14,11 @@ use crate::{
},
PerSpendAnchor, SharedAnchor,
},
serialization::{TrustedPreallocate, ZcashSerialize},
serialization::{tests::max_allocation_is_big_enough, TrustedPreallocate, ZcashSerialize},
};
use proptest::prelude::*;
use std::{cmp::max, convert::TryInto};
use std::cmp::max;
proptest! {
/// Confirm that each `Spend<PerSpendAnchor>` takes exactly
@ -208,37 +208,3 @@ proptest! {
prop_assert!((largest_allowed_vec_len as u64) <= max(SpendPrefixInTransactionV5::max_allocation(), OutputPrefixInTransactionV5::max_allocation()));
}
}
/// Return the following calculations on `item`:
/// smallest_disallowed_vec_len
/// smallest_disallowed_serialized_len
/// largest_allowed_vec_len
/// largest_allowed_serialized_len
fn max_allocation_is_big_enough<T>(item: T) -> (usize, usize, usize, usize)
where
T: TrustedPreallocate + ZcashSerialize + Clone,
{
let max_allocation: usize = T::max_allocation().try_into().unwrap();
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
for _ in 0..(max_allocation + 1) {
smallest_disallowed_vec.push(item.clone());
}
let smallest_disallowed_serialized = smallest_disallowed_vec
.zcash_serialize_to_vec()
.expect("Serialization to vec must succeed");
let smallest_disallowed_vec_len = smallest_disallowed_vec.len();
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
smallest_disallowed_vec.pop();
let largest_allowed_vec = smallest_disallowed_vec;
let largest_allowed_serialized = largest_allowed_vec
.zcash_serialize_to_vec()
.expect("Serialization to vec must succeed");
(
smallest_disallowed_vec_len,
smallest_disallowed_serialized.len(),
largest_allowed_vec.len(),
largest_allowed_serialized.len(),
)
}

View File

@ -26,9 +26,9 @@ pub use zcash_deserialize::{
ZcashDeserialize, ZcashDeserializeInto,
};
pub use zcash_serialize::{
zcash_serialize_bytes_external_count, zcash_serialize_external_count, ZcashSerialize,
MAX_PROTOCOL_MESSAGE_LEN,
zcash_serialize_bytes, zcash_serialize_bytes_external_count, zcash_serialize_external_count,
ZcashSerialize, MAX_PROTOCOL_MESSAGE_LEN,
};
#[cfg(test)]
mod tests;
pub mod tests;

View File

@ -1,2 +1,4 @@
mod preallocate;
mod prop;
pub use preallocate::max_allocation_is_big_enough;

View File

@ -2,11 +2,11 @@
use proptest::{collection::size_range, prelude::*};
use std::matches;
use std::{convert::TryInto, matches};
use crate::serialization::{
zcash_deserialize::MAX_U8_ALLOCATION, SerializationError, ZcashDeserialize, ZcashSerialize,
MAX_PROTOCOL_MESSAGE_LEN,
zcash_deserialize::MAX_U8_ALLOCATION, SerializationError, TrustedPreallocate, ZcashDeserialize,
ZcashSerialize, MAX_PROTOCOL_MESSAGE_LEN,
};
// Allow direct serialization of Vec<u8> for these tests. We don't usually
@ -19,6 +19,40 @@ impl ZcashSerialize for u8 {
}
}
/// Return the following calculations on `item`:
/// smallest_disallowed_vec_len
/// smallest_disallowed_serialized_len
/// largest_allowed_vec_len
/// largest_allowed_serialized_len
pub fn max_allocation_is_big_enough<T>(item: T) -> (usize, usize, usize, usize)
where
T: TrustedPreallocate + ZcashSerialize + Clone,
{
let max_allocation: usize = T::max_allocation().try_into().unwrap();
let mut smallest_disallowed_vec = Vec::with_capacity(max_allocation + 1);
for _ in 0..(max_allocation + 1) {
smallest_disallowed_vec.push(item.clone());
}
let smallest_disallowed_serialized = smallest_disallowed_vec
.zcash_serialize_to_vec()
.expect("Serialization to vec must succeed");
let smallest_disallowed_vec_len = smallest_disallowed_vec.len();
// Create largest_allowed_vec by removing one element from smallest_disallowed_vec without copying (for efficiency)
smallest_disallowed_vec.pop();
let largest_allowed_vec = smallest_disallowed_vec;
let largest_allowed_serialized = largest_allowed_vec
.zcash_serialize_to_vec()
.expect("Serialization to vec must succeed");
(
smallest_disallowed_vec_len,
smallest_disallowed_serialized.len(),
largest_allowed_vec.len(),
largest_allowed_serialized.len(),
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(4))]

View File

@ -22,7 +22,7 @@ pub use sapling::FieldNotPresent;
pub use sighash::HashType;
use crate::{
block,
block, orchard,
parameters::NetworkUpgrade,
primitives::{Bctv14Proof, Groth16Proof},
sapling, sprout, transparent,
@ -112,7 +112,8 @@ pub enum Transaction {
outputs: Vec<transparent::Output>,
/// The sapling shielded data for this transaction, if any.
sapling_shielded_data: Option<sapling::ShieldedData<sapling::SharedAnchor>>,
// TODO: The orchard data for this transaction, if any.
// The orchard data for this transaction, if any.
orchard_shielded_data: Option<orchard::ShieldedData>,
},
}

View File

@ -5,9 +5,12 @@ use proptest::{arbitrary::any, array, collection::vec, option, prelude::*};
use crate::{
amount::Amount,
block,
block, orchard,
parameters::{Network, NetworkUpgrade},
primitives::{Bctv14Proof, Groth16Proof, ZkSnarkProof},
primitives::{
redpallas::{Binding, Signature},
Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof,
},
sapling, sprout, transparent, LedgerState,
};
@ -79,8 +82,8 @@ impl Transaction {
vec(any::<transparent::Output>(), 0..10),
any::<LockTime>(),
any::<block::Height>(),
option::of(any::<sapling::ShieldedData<sapling::PerSpendAnchor>>()),
option::of(any::<JoinSplitData<Groth16Proof>>()),
option::of(any::<sapling::ShieldedData<sapling::PerSpendAnchor>>()),
)
.prop_map(
|(
@ -88,15 +91,15 @@ impl Transaction {
outputs,
lock_time,
expiry_height,
sapling_shielded_data,
joinsplit_data,
sapling_shielded_data,
)| Transaction::V4 {
inputs,
outputs,
lock_time,
expiry_height,
sapling_shielded_data,
joinsplit_data,
sapling_shielded_data,
},
)
.boxed()
@ -111,6 +114,7 @@ impl Transaction {
transparent::Input::vec_strategy(ledger_state, 10),
vec(any::<transparent::Output>(), 0..10),
option::of(any::<sapling::ShieldedData<sapling::SharedAnchor>>()),
option::of(any::<orchard::ShieldedData>()),
)
.prop_map(
|(
@ -120,6 +124,7 @@ impl Transaction {
inputs,
outputs,
sapling_shielded_data,
orchard_shielded_data,
)| {
Transaction::V5 {
network_upgrade,
@ -128,6 +133,7 @@ impl Transaction {
inputs,
outputs,
sapling_shielded_data,
orchard_shielded_data,
}
},
)
@ -323,6 +329,58 @@ impl Arbitrary for sapling::TransferData<SharedAnchor> {
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for orchard::ShieldedData {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
(
any::<orchard::shielded_data::Flags>(),
any::<Amount>(),
any::<orchard::tree::Root>(),
any::<Halo2Proof>(),
vec(any::<orchard::shielded_data::AuthorizedAction>(), 1..10),
any::<Signature<Binding>>(),
)
.prop_map(
|(flags, value_balance, shared_anchor, proof, actions, binding_sig)| Self {
flags,
value_balance,
shared_anchor,
proof,
actions: actions
.try_into()
.expect("arbitrary vector size range produces at least one action"),
binding_sig,
},
)
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for Signature<Binding> {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
(vec(any::<u8>(), 64))
.prop_filter_map(
"zero Signature::<Binding> values are invalid",
|sig_bytes| {
let mut b = [0u8; 64];
b.copy_from_slice(sig_bytes.as_slice());
if b == [0u8; 64] {
return None;
}
Some(Signature::<Binding>::from(b))
},
)
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for Transaction {
type Parameters = LedgerState;
@ -372,6 +430,7 @@ pub fn transaction_to_fake_v5(
lock_time: *lock_time,
expiry_height: block::Height(0),
sapling_shielded_data: None,
orchard_shielded_data: None,
},
V2 {
inputs,
@ -385,6 +444,7 @@ pub fn transaction_to_fake_v5(
lock_time: *lock_time,
expiry_height: block::Height(0),
sapling_shielded_data: None,
orchard_shielded_data: None,
},
V3 {
inputs,
@ -399,6 +459,7 @@ pub fn transaction_to_fake_v5(
lock_time: *lock_time,
expiry_height: *expiry_height,
sapling_shielded_data: None,
orchard_shielded_data: None,
},
V4 {
inputs,
@ -417,6 +478,7 @@ pub fn transaction_to_fake_v5(
.clone()
.map(sapling_shielded_v4_to_fake_v5)
.flatten(),
orchard_shielded_data: None,
},
v5 @ V5 { .. } => v5.clone(),
}

View File

@ -7,11 +7,15 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use halo2::{arithmetic::FieldExt, pasta::pallas};
use crate::{
amount,
block::MAX_BLOCK_BYTES,
parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID},
primitives::{Groth16Proof, ZkSnarkProof},
primitives::{
redpallas::{Binding, Signature, SpendAuth},
Groth16Proof, Halo2Proof, ZkSnarkProof,
},
serialization::{
zcash_deserialize_external_count, zcash_serialize_external_count, ReadZcashExt,
zcash_deserialize_external_count, zcash_serialize_external_count, AtLeastOne, ReadZcashExt,
SerializationError, TrustedPreallocate, WriteZcashExt, ZcashDeserialize,
ZcashDeserializeInto, ZcashSerialize,
},
@ -245,6 +249,115 @@ impl ZcashDeserialize for Option<sapling::ShieldedData<SharedAnchor>> {
}
}
impl ZcashSerialize for Option<orchard::ShieldedData> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
match self {
None => {
// nActionsOrchard
writer.write_compactsize(0)?;
// We don't need to write anything else here.
// "The fields flagsOrchard, valueBalanceOrchard, anchorOrchard, sizeProofsOrchard,
// proofsOrchard , and bindingSigOrchard are present if and only if nActionsOrchard > 0."
// https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus notes of the second
// table, section sign.
}
Some(orchard_shielded_data) => {
orchard_shielded_data.zcash_serialize(&mut writer)?;
}
}
Ok(())
}
}
impl ZcashSerialize for orchard::ShieldedData {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
// Split the AuthorizedAction
let (actions, sigs): (Vec<orchard::Action>, Vec<Signature<SpendAuth>>) = self
.actions
.iter()
.cloned()
.map(orchard::AuthorizedAction::into_parts)
.unzip();
// nActionsOrchard and vActionsOrchard
actions.zcash_serialize(&mut writer)?;
// flagsOrchard
self.flags.zcash_serialize(&mut writer)?;
// valueBalanceOrchard
self.value_balance.zcash_serialize(&mut writer)?;
// anchorOrchard
self.shared_anchor.zcash_serialize(&mut writer)?;
// sizeProofsOrchard and proofsOrchard
self.proof.zcash_serialize(&mut writer)?;
// vSpendAuthSigsOrchard
zcash_serialize_external_count(&sigs, &mut writer)?;
// bindingSigOrchard
self.binding_sig.zcash_serialize(&mut writer)?;
Ok(())
}
}
// we can't split ShieldedData out of Option<ShieldedData> deserialization,
// because the counts are read along with the arrays.
impl ZcashDeserialize for Option<orchard::ShieldedData> {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
// nActionsOrchard and vActionsOrchard
let actions: Vec<orchard::Action> = (&mut reader).zcash_deserialize_into()?;
// "sizeProofsOrchard ... [is] present if and only if nActionsOrchard > 0"
// https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus
if actions.is_empty() {
return Ok(None);
}
// flagsOrchard
let flags: orchard::Flags = (&mut reader).zcash_deserialize_into()?;
// valueBalanceOrchard
let value_balance: amount::Amount = (&mut reader).zcash_deserialize_into()?;
// anchorOrchard
let shared_anchor: orchard::tree::Root = (&mut reader).zcash_deserialize_into()?;
// sizeProofsOrchard and proofsOrchard
let proof: Halo2Proof = (&mut reader).zcash_deserialize_into()?;
// vSpendAuthSigsOrchard
let sigs: Vec<Signature<SpendAuth>> =
zcash_deserialize_external_count(actions.len(), &mut reader)?;
// bindingSigOrchard
let binding_sig: Signature<Binding> = (&mut reader).zcash_deserialize_into()?;
// Create the AuthorizedAction from deserialized parts
let authorized_actions: Vec<orchard::AuthorizedAction> = actions
.into_iter()
.zip(sigs.into_iter())
.map(|(action, spend_auth_sig)| {
orchard::AuthorizedAction::from_parts(action, spend_auth_sig)
})
.collect();
let actions: AtLeastOne<orchard::AuthorizedAction> = authorized_actions.try_into()?;
Ok(Some(orchard::ShieldedData {
flags,
value_balance,
shared_anchor,
proof,
actions,
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.
@ -374,6 +487,7 @@ impl ZcashSerialize for Transaction {
inputs,
outputs,
sapling_shielded_data,
orchard_shielded_data,
} => {
// Transaction V5 spec:
// https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus
@ -401,10 +515,7 @@ impl ZcashSerialize for Transaction {
sapling_shielded_data.zcash_serialize(&mut writer)?;
// orchard
// TODO: Parse orchard into structs
// In the meantime, to keep the transaction valid, we add `0`
// as the nActionsOrchard field
writer.write_compactsize(0)?;
orchard_shielded_data.zcash_serialize(&mut writer)?;
}
}
Ok(())
@ -544,14 +655,7 @@ impl ZcashDeserialize for Transaction {
let sapling_shielded_data = (&mut reader).zcash_deserialize_into()?;
// orchard
// TODO: Parse orchard into structs
// In the meantime, check the orchard section is just `0`
let empty_orchard_section = reader.read_compactsize()?;
if empty_orchard_section != 0 {
return Err(SerializationError::Parse(
"expected orchard section to be just a zero",
));
}
let orchard_shielded_data = (&mut reader).zcash_deserialize_into()?;
Ok(Transaction::V5 {
network_upgrade,
@ -560,6 +664,7 @@ impl ZcashDeserialize for Transaction {
inputs,
outputs,
sapling_shielded_data,
orchard_shielded_data,
})
}
(_, _) => Err(SerializationError::Parse("bad tx header")),

View File

@ -109,6 +109,7 @@ fn empty_v5_round_trip() {
inputs: Vec::new(),
outputs: Vec::new(),
sapling_shielded_data: None,
orchard_shielded_data: None,
};
let data = tx.zcash_serialize_to_vec().expect("tx should serialize");