diff --git a/zebra-chain/src/lib.rs b/zebra-chain/src/lib.rs index 1336d488..03c27455 100644 --- a/zebra-chain/src/lib.rs +++ b/zebra-chain/src/lib.rs @@ -18,6 +18,9 @@ #[macro_use] extern crate serde; +#[macro_use] +extern crate bitflags; + pub mod amount; pub mod block; pub mod fmt; diff --git a/zebra-chain/src/orchard.rs b/zebra-chain/src/orchard.rs index 3fbeca40..1b229cd8 100644 --- a/zebra-chain/src/orchard.rs +++ b/zebra-chain/src/orchard.rs @@ -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}; diff --git a/zebra-chain/src/orchard/arbitrary.rs b/zebra-chain/src/orchard/arbitrary.rs index e13f6c26..ded9d3d0 100644 --- a/zebra-chain/src/orchard/arbitrary.rs +++ b/zebra-chain/src/orchard/arbitrary.rs @@ -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::()).prop_map(Self::from).boxed() + use halo2::arithmetic::FieldExt; + + (any::()) + .prop_map(|number| Self::from(pallas::Scalar::from_u64(number).to_bytes())) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for AuthorizedAction { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (any::(), any::>()) + .prop_map(|(action, spend_auth_sig)| Self { + action, + spend_auth_sig, + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for Signature { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (array::uniform32(any::()), array::uniform32(any::())) + .prop_map(|(r_bytes, s_bytes)| Self { + r_bytes, + s_bytes, + _marker: PhantomData, + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for Flags { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (any::()).prop_map(Self::from_bits_truncate).boxed() } type Strategy = BoxedStrategy; diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs new file mode 100644 index 00000000..3adb25f5 --- /dev/null +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -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, + /// A signature on the transaction `sighash`. + pub binding_sig: Signature, +} + +/// 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, +} + +impl AuthorizedAction { + /// Split out the action and the signature for V5 transaction + /// serialization. + pub fn into_parts(self) -> (Action, Signature) { + (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) -> 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 +/// +/// 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`. +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 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 { + 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(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_u8(self.bits())?; + + Ok(()) + } +} + +impl ZcashDeserialize for Flags { + fn zcash_deserialize(mut reader: R) -> Result { + let flags = Flags::from_bits(reader.read_u8()?).unwrap(); + + Ok(flags) + } +} diff --git a/zebra-chain/src/orchard/tests.rs b/zebra-chain/src/orchard/tests.rs new file mode 100644 index 00000000..67af9b07 --- /dev/null +++ b/zebra-chain/src/orchard/tests.rs @@ -0,0 +1 @@ +mod preallocate; diff --git a/zebra-chain/src/orchard/tests/preallocate.rs b/zebra-chain/src/orchard/tests/preallocate.rs new file mode 100644 index 00000000..9fd87de2 --- /dev/null +++ b/zebra-chain/src/orchard/tests/preallocate.rs @@ -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 ::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 ::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::` + prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Signature::::max_allocation()); + prop_assert!((largest_allowed_vec_len as u64) == Signature::::max_allocation()); + prop_assert!((largest_allowed_serialized_len as u64) <= MAX_BLOCK_BYTES); + } +} diff --git a/zebra-chain/src/orchard/tree.rs b/zebra-chain/src/orchard/tree.rs index 1c7578e0..b3679038 100644 --- a/zebra-chain/src/orchard/tree.rs +++ b/zebra-chain/src/orchard/tree.rs @@ -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 for [u8; 32] { + fn from(root: Root) -> Self { + root.0 + } +} + +impl ZcashSerialize for Root { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&<[u8; 32]>::from(*self)[..])?; + + Ok(()) + } +} + +impl ZcashDeserialize for Root { + fn zcash_deserialize(mut reader: R) -> Result { + let anchor = reader.read_32_bytes()?.into(); + + Ok(anchor) + } +} + /// Orchard Note Commitment Tree #[derive(Clone, Debug, Default, Eq, PartialEq)] struct NoteCommitmentTree { diff --git a/zebra-chain/src/primitives/proofs/halo2.rs b/zebra-chain/src/primitives/proofs/halo2.rs index 0b22297e..eb62d1a9 100644 --- a/zebra-chain/src/primitives/proofs/halo2.rs +++ b/zebra-chain/src/primitives/proofs/halo2.rs @@ -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(&self, mut writer: W) -> Result<(), io::Error> { - writer.write_all(&self.0[..])?; - Ok(()) + fn zcash_serialize(&self, writer: W) -> Result<(), io::Error> { + zcash_serialize_bytes(&self.0, writer) } } impl ZcashDeserialize for Halo2Proof { fn zcash_deserialize(mut reader: R) -> Result { - 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"))] diff --git a/zebra-chain/src/primitives/redpallas.rs b/zebra-chain/src/primitives/redpallas.rs index b625b807..9ad37c28 100644 --- a/zebra-chain/src/primitives/redpallas.rs +++ b/zebra-chain/src/primitives/redpallas.rs @@ -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 {} diff --git a/zebra-chain/src/primitives/redpallas/signature.rs b/zebra-chain/src/primitives/redpallas/signature.rs index 303e921d..aae7f30e 100644 --- a/zebra-chain/src/primitives/redpallas/signature.rs +++ b/zebra-chain/src/primitives/redpallas/signature.rs @@ -8,13 +8,14 @@ // - Henry de Valence // - Deirdre Connolly -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 { pub(crate) r_bytes: [u8; 32], pub(crate) s_bytes: [u8; 32], @@ -43,3 +44,16 @@ impl From> for [u8; 64] { bytes } } + +impl ZcashSerialize for Signature { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&<[u8; 64]>::from(*self)[..])?; + Ok(()) + } +} + +impl ZcashDeserialize for Signature { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(reader.read_64_bytes()?.into()) + } +} diff --git a/zebra-chain/src/sapling/tests/preallocate.rs b/zebra-chain/src/sapling/tests/preallocate.rs index 00adbb3a..28934024 100644 --- a/zebra-chain/src/sapling/tests/preallocate.rs +++ b/zebra-chain/src/sapling/tests/preallocate.rs @@ -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` 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(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(), - ) -} diff --git a/zebra-chain/src/serialization.rs b/zebra-chain/src/serialization.rs index 085ca840..693879b2 100644 --- a/zebra-chain/src/serialization.rs +++ b/zebra-chain/src/serialization.rs @@ -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; diff --git a/zebra-chain/src/serialization/tests.rs b/zebra-chain/src/serialization/tests.rs index a7ac2ac3..d0b94745 100644 --- a/zebra-chain/src/serialization/tests.rs +++ b/zebra-chain/src/serialization/tests.rs @@ -1,2 +1,4 @@ mod preallocate; mod prop; + +pub use preallocate::max_allocation_is_big_enough; diff --git a/zebra-chain/src/serialization/tests/preallocate.rs b/zebra-chain/src/serialization/tests/preallocate.rs index 34d635f1..2f7e6b3c 100644 --- a/zebra-chain/src/serialization/tests/preallocate.rs +++ b/zebra-chain/src/serialization/tests/preallocate.rs @@ -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 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(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))] diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 9c3c90a4..21286731 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -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, /// The sapling shielded data for this transaction, if any. sapling_shielded_data: Option>, - // TODO: The orchard data for this transaction, if any. + // The orchard data for this transaction, if any. + orchard_shielded_data: Option, }, } diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index b7bf8216..8dfbb909 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -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::(), 0..10), any::(), any::(), - option::of(any::>()), option::of(any::>()), + option::of(any::>()), ) .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::(), 0..10), option::of(any::>()), + option::of(any::()), ) .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 { type Strategy = BoxedStrategy; } +impl Arbitrary for orchard::ShieldedData { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::(), + any::(), + any::(), + vec(any::(), 1..10), + any::>(), + ) + .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; +} + +impl Arbitrary for Signature { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (vec(any::(), 64)) + .prop_filter_map( + "zero Signature:: 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::::from(b)) + }, + ) + .boxed() + } + + type Strategy = BoxedStrategy; +} + 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(), } diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index 4dad887c..3ebf1c1b 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -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> { } } +impl ZcashSerialize for Option { + fn zcash_serialize(&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(&self, mut writer: W) -> Result<(), io::Error> { + // Split the AuthorizedAction + let (actions, sigs): (Vec, Vec>) = 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 deserialization, +// because the counts are read along with the arrays. +impl ZcashDeserialize for Option { + fn zcash_deserialize(mut reader: R) -> Result { + // nActionsOrchard and vActionsOrchard + let actions: Vec = (&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> = + zcash_deserialize_external_count(actions.len(), &mut reader)?; + + // bindingSigOrchard + let binding_sig: Signature = (&mut reader).zcash_deserialize_into()?; + + // Create the AuthorizedAction from deserialized parts + let authorized_actions: Vec = actions + .into_iter() + .zip(sigs.into_iter()) + .map(|(action, spend_auth_sig)| { + orchard::AuthorizedAction::from_parts(action, spend_auth_sig) + }) + .collect(); + + let actions: AtLeastOne = authorized_actions.try_into()?; + + Ok(Some(orchard::ShieldedData { + flags, + value_balance, + shared_anchor, + proof, + actions, + binding_sig, + })) + } +} + impl ZcashSerialize for Transaction { fn zcash_serialize(&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")), diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs index 1ade0cfc..3bc94e91 100644 --- a/zebra-chain/src/transaction/tests/vectors.rs +++ b/zebra-chain/src/transaction/tests/vectors.rs @@ -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");