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:
parent
40d06657b3
commit
a57c09a3b8
|
@ -18,6 +18,9 @@
|
|||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
|
||||
pub mod amount;
|
||||
pub mod block;
|
||||
pub mod fmt;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
mod preallocate;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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 {}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
mod preallocate;
|
||||
mod prop;
|
||||
|
||||
pub use preallocate::max_allocation_is_big_enough;
|
||||
|
|
|
@ -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))]
|
||||
|
||||
|
|
|
@ -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>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue