Redesign Sapling data model for V5 shared anchor and spends (#2021)
* Redesign Sapling data model for V5 shared anchor and spends The shared anchor is only present if there are any spends. As part of this change, delete the manual PartialEq impl and its tests, because we can derive PartialEq now. * Stop creating a temporary Vec for the spend and output iterators * Rename TransferData variants Interactive rename using the following commands: ```sh fastmod Spends SpendsAndMaybeOutputs fastmod NoSpends JustOutputs ``` * Refactor out common sprout nullifier code * Implement the AtLeastOne constrained vector type This vector wrapper ensures that it always contains at least one element. * Simplify Sapling TransferData using AtLeastOne Also update the RFC to use AtLeastOne for Orchard.
This commit is contained in:
parent
ad272f2bee
commit
53779d2a3c
|
@ -38,9 +38,9 @@ To highlight changes most of the document comments from the code snippets in the
|
|||
|
||||
V4 and V5 transactions both support sapling, but the underlying data structures are different. So we need to make the sapling data types generic over the V4 and V5 structures.
|
||||
|
||||
In V4, anchors are per-spend, but in V5, they are per-transaction.
|
||||
In V4, anchors are per-spend, but in V5, they are per-transaction. In V5, the shared anchor is only present if there is at least one spend.
|
||||
|
||||
For consistency, also we move some fields into the `ShieldedData` type, and rename some fields and types.
|
||||
For consistency, we also move some fields into the `ShieldedData` type, and rename some fields and types.
|
||||
|
||||
## Orchard Additions Overview
|
||||
[orchard-additions-overview]: #orchard-additions-overview
|
||||
|
@ -128,11 +128,11 @@ struct FieldNotPresent;
|
|||
|
||||
impl AnchorVariant for PerSpendAnchor {
|
||||
type Shared = FieldNotPresent;
|
||||
type PerSpend = tree::Root;
|
||||
type PerSpend = sapling::tree::Root;
|
||||
}
|
||||
|
||||
impl AnchorVariant for SharedAnchor {
|
||||
type Shared = tree::Root;
|
||||
type Shared = sapling::tree::Root;
|
||||
type PerSpend = FieldNotPresent;
|
||||
}
|
||||
|
||||
|
@ -146,20 +146,51 @@ trait AnchorVariant {
|
|||
[changes-to-sapling-shieldeddata]: #changes-to-sapling-shieldeddata
|
||||
|
||||
We use `AnchorVariant` in `ShieldedData` to model the anchor differences between V4 and V5:
|
||||
* in V4, there is a per-spend anchor
|
||||
* in V5, there is a shared anchor, which is only present when there are spends
|
||||
|
||||
If there are no spends and no outputs:
|
||||
* in v4, the value_balance is fixed to zero
|
||||
* in v5, the value balance field is not present
|
||||
* in both versions, the binding_sig field is not present
|
||||
|
||||
```rust
|
||||
/// ShieldedData ensures that value_balance and binding_sig are only present when
|
||||
/// there is at least one spend or output.
|
||||
struct sapling::ShieldedData<AnchorV: AnchorVariant> {
|
||||
value_balance: Amount,
|
||||
shared_anchor: AnchorV::Shared,
|
||||
// The following fields are in a different order to the serialized data, see:
|
||||
// https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus
|
||||
first: Either<Spend<AnchorV>, Output>,
|
||||
rest_spends: Vec<Spend<AnchorV>>,
|
||||
rest_outputs: Vec<Output>,
|
||||
transfers: sapling::TransferData<AnchorV>,
|
||||
binding_sig: redjubjub::Signature<Binding>,
|
||||
}
|
||||
|
||||
/// TransferData ensures that:
|
||||
/// * there is at least one spend or output, and
|
||||
/// * the shared anchor is only present when there are spends
|
||||
enum sapling::TransferData<AnchorV: AnchorVariant> {
|
||||
/// In Transaction::V5, if there are any spends,
|
||||
/// there must also be a shared spend anchor.
|
||||
SpendsAndMaybeOutputs {
|
||||
shared_anchor: AnchorV::Shared,
|
||||
spends: AtLeastOne<Spend<AnchorV>>,
|
||||
maybe_outputs: Vec<Output>,
|
||||
}
|
||||
|
||||
/// If there are no spends, there must not be a shared
|
||||
/// anchor.
|
||||
JustOutputs {
|
||||
outputs: AtLeastOne<Output>,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `AtLeastOne` type is a vector wrapper which always contains at least one
|
||||
element. For more details, see [its documentation](https://github.com/ZcashFoundation/zebra/blob/673b95dea5f0b057c11f2f450943b012fec75c00/zebra-chain/src/serialization/constraint.rs).
|
||||
<!-- TODO: update link to main branch when PR #2021 merges -->
|
||||
|
||||
Some of these fields are in a different order to the serialized data, see
|
||||
[the V4 and V5 transaction specs](https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus)
|
||||
for details.
|
||||
|
||||
The following types have `ZcashSerialize` and `ZcashDeserialize` implementations,
|
||||
because they can be serialized into a single byte vector:
|
||||
* `Amount`
|
||||
|
@ -314,12 +345,7 @@ struct orchard::ShieldedData {
|
|||
value_balance: Amount,
|
||||
shared_anchor: tree::Root,
|
||||
proof: Halo2Proof,
|
||||
/// An authorized action description.
|
||||
///
|
||||
/// Storing this separately ensures that it is impossible to construct
|
||||
/// an invalid `ShieldedData` with no actions.
|
||||
first: AuthorizedAction,
|
||||
rest: Vec<AuthorizedAction>,
|
||||
actions: AtLeastOne<AuthorizedAction>,
|
||||
binding_sig: redpallas::Signature<Binding>,
|
||||
}
|
||||
```
|
||||
|
|
|
@ -22,6 +22,6 @@ pub use keys::Diversifier;
|
|||
pub use note::{EncryptedNote, Note, Nullifier, WrappedNoteKey};
|
||||
pub use output::{Output, OutputInTransactionV4, OutputPrefixInTransactionV5};
|
||||
pub use shielded_data::{
|
||||
AnchorVariant, FieldNotPresent, PerSpendAnchor, SharedAnchor, ShieldedData,
|
||||
AnchorVariant, FieldNotPresent, PerSpendAnchor, SharedAnchor, ShieldedData, TransferData,
|
||||
};
|
||||
pub use spend::{Spend, SpendPrefixInTransactionV5};
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
//! The `value_balance` change is handled using the default zero value.
|
||||
//! The anchor change is handled using the `AnchorVariant` type trait.
|
||||
|
||||
use futures::future::Either;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
use crate::{
|
||||
|
@ -17,7 +16,7 @@ use crate::{
|
|||
output::OutputPrefixInTransactionV5, spend::SpendPrefixInTransactionV5, tree, Nullifier,
|
||||
Output, Spend, ValueCommitment,
|
||||
},
|
||||
serialization::{serde_helpers, TrustedPreallocate},
|
||||
serialization::{AtLeastOne, TrustedPreallocate},
|
||||
};
|
||||
|
||||
use std::{
|
||||
|
@ -74,54 +73,93 @@ pub trait AnchorVariant {
|
|||
///
|
||||
/// The Sapling `value_balance` field is optional in `Transaction::V5`, but
|
||||
/// required in `Transaction::V4`. In both cases, if there is no `ShieldedData`,
|
||||
/// then the field value must be zero. Therefore, only need to store
|
||||
/// then the field value must be zero. Therefore, we only need to store
|
||||
/// `value_balance` when there is some Sapling `ShieldedData`.
|
||||
///
|
||||
/// In `Transaction::V4`, each `Spend` has its own anchor. In `Transaction::V5`,
|
||||
/// there is a single `shared_anchor` for the entire transaction. This
|
||||
/// structural difference is modeled using the `AnchorVariant` type trait.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
/// there is a single `shared_anchor` for the entire transaction, which is only
|
||||
/// present when there is at least one spend. These structural differences are
|
||||
/// modeled using the `AnchorVariant` type trait and `TransferData` enum.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ShieldedData<AnchorV>
|
||||
where
|
||||
AnchorV: AnchorVariant + Clone,
|
||||
{
|
||||
/// The net value of Sapling spend transfers minus output transfers.
|
||||
pub value_balance: Amount,
|
||||
/// The shared anchor for all `Spend`s in this transaction.
|
||||
|
||||
/// A bundle of spends and outputs, containing at least one spend or
|
||||
/// output.
|
||||
///
|
||||
/// The anchor is the root of the Sapling note commitment tree in a previous
|
||||
/// block. This root should be in the best chain for a transaction to be
|
||||
/// mined, and it must be in the relevant chain for a transaction to be
|
||||
/// valid.
|
||||
///
|
||||
/// Some transaction versions have a per-spend anchor, rather than a shared
|
||||
/// anchor.
|
||||
pub shared_anchor: AnchorV::Shared,
|
||||
/// Either a spend or output description.
|
||||
///
|
||||
/// Storing this separately ensures that it is impossible to construct
|
||||
/// an invalid `ShieldedData` with no spends or outputs.
|
||||
///
|
||||
/// However, it's not necessary to access or process `first` and `rest`
|
||||
/// separately, as the [`ShieldedData::spends`] and [`ShieldedData::outputs`]
|
||||
/// methods provide iterators over all of the [`Spend`]s and
|
||||
/// [`Output`]s.
|
||||
#[serde(with = "serde_helpers::Either")]
|
||||
pub first: Either<Spend<AnchorV>, Output>,
|
||||
/// The rest of the [`Spend`]s for this transaction.
|
||||
///
|
||||
/// Note that the [`ShieldedData::spends`] method provides an iterator
|
||||
/// over all spend descriptions.
|
||||
pub rest_spends: Vec<Spend<AnchorV>>,
|
||||
/// The rest of the [`Output`]s for this transaction.
|
||||
///
|
||||
/// Note that the [`ShieldedData::outputs`] method provides an iterator
|
||||
/// over all output descriptions.
|
||||
pub rest_outputs: Vec<Output>,
|
||||
/// In V5 transactions, also contains a shared anchor, if there are any
|
||||
/// spends.
|
||||
pub transfers: TransferData<AnchorV>,
|
||||
|
||||
/// A signature on the transaction hash.
|
||||
pub binding_sig: Signature<Binding>,
|
||||
}
|
||||
|
||||
/// A bundle of [`Spend`] and [`Output`] descriptions, and a shared anchor.
|
||||
///
|
||||
/// This wrapper type bundles at least one Spend or Output description with
|
||||
/// the required anchor data, so that an `Option<ShieldedData>` (which contains
|
||||
/// this type) correctly models the presence or absence of any spends and
|
||||
/// shielded data, across both V4 and V5 transactions.
|
||||
///
|
||||
/// Specifically, TransferData ensures that:
|
||||
/// * there is at least one spend or output, and
|
||||
/// * the shared anchor is only present when there are spends.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum TransferData<AnchorV>
|
||||
where
|
||||
AnchorV: AnchorVariant + Clone,
|
||||
{
|
||||
/// A bundle containing at least one spend, and the shared spend anchor.
|
||||
/// There can also be zero or more outputs.
|
||||
///
|
||||
/// In Transaction::V5, if there are any spends, there must also be a shared
|
||||
/// spend anchor.
|
||||
SpendsAndMaybeOutputs {
|
||||
/// The shared anchor for all `Spend`s in this transaction.
|
||||
///
|
||||
/// The anchor is the root of the Sapling note commitment tree in a previous
|
||||
/// block. This root should be in the best chain for a transaction to be
|
||||
/// mined, and it must be in the relevant chain for a transaction to be
|
||||
/// valid.
|
||||
///
|
||||
/// Some transaction versions have a per-spend anchor, rather than a shared
|
||||
/// anchor.
|
||||
///
|
||||
/// Use the `shared_anchor` method to access this field.
|
||||
shared_anchor: AnchorV::Shared,
|
||||
|
||||
/// At least one spend.
|
||||
///
|
||||
/// Use the [`ShieldedData::spends`] method to get an iterator over the
|
||||
/// [`Spend`]s in this `TransferData`.
|
||||
spends: AtLeastOne<Spend<AnchorV>>,
|
||||
|
||||
/// Maybe some outputs (can be empty).
|
||||
///
|
||||
/// Use the [`ShieldedData::outputs`] method to get an iterator over the
|
||||
/// [`Outputs`]s in this `TransferData`.
|
||||
maybe_outputs: Vec<Output>,
|
||||
},
|
||||
|
||||
/// A bundle containing at least one output, with no spends and no shared
|
||||
/// spend anchor.
|
||||
///
|
||||
/// In Transaction::V5, if there are no spends, there must not be a shared
|
||||
/// anchor.
|
||||
JustOutputs {
|
||||
/// At least one output.
|
||||
///
|
||||
/// Use the [`ShieldedData::outputs`] method to get an iterator over the
|
||||
/// [`Outputs`]s in this `TransferData`.
|
||||
outputs: AtLeastOne<Output>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<AnchorV> ShieldedData<AnchorV>
|
||||
where
|
||||
AnchorV: AnchorVariant + Clone,
|
||||
|
@ -136,9 +174,13 @@ where
|
|||
///
|
||||
/// Do not use this function for serialization.
|
||||
pub fn spends_per_anchor(&self) -> impl Iterator<Item = Spend<PerSpendAnchor>> + '_ {
|
||||
self.spends()
|
||||
.cloned()
|
||||
.map(move |spend| Spend::<PerSpendAnchor>::from((spend, self.shared_anchor.clone())))
|
||||
self.spends().cloned().map(move |spend| {
|
||||
Spend::<PerSpendAnchor>::from((
|
||||
spend,
|
||||
self.shared_anchor()
|
||||
.expect("shared anchor must be Some if there are any spends"),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,22 +195,21 @@ where
|
|||
///
|
||||
/// Use this function for serialization.
|
||||
pub fn spends(&self) -> impl Iterator<Item = &Spend<AnchorV>> {
|
||||
match self.first {
|
||||
Either::Left(ref spend) => Some(spend),
|
||||
Either::Right(_) => None,
|
||||
}
|
||||
.into_iter()
|
||||
.chain(self.rest_spends.iter())
|
||||
self.transfers.spends()
|
||||
}
|
||||
|
||||
/// Iterate over the [`Output`]s for this transaction.
|
||||
pub fn outputs(&self) -> impl Iterator<Item = &Output> {
|
||||
match self.first {
|
||||
Either::Left(_) => None,
|
||||
Either::Right(ref output) => Some(output),
|
||||
}
|
||||
.into_iter()
|
||||
.chain(self.rest_outputs.iter())
|
||||
self.transfers.outputs()
|
||||
}
|
||||
|
||||
/// Provide the shared anchor for this transaction, if present.
|
||||
///
|
||||
/// The shared anchor is only present if:
|
||||
/// * there is at least one spend, and
|
||||
/// * this is a `V5` transaction.
|
||||
pub fn shared_anchor(&self) -> Option<AnchorV::Shared> {
|
||||
self.transfers.shared_anchor()
|
||||
}
|
||||
|
||||
/// Collect the [`Nullifier`]s for this transaction, if it contains
|
||||
|
@ -216,40 +257,51 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
// Technically, it's possible to construct two equivalent representations
|
||||
// of a ShieldedData with at least one spend and at least one output, depending
|
||||
// on which goes in the `first` slot. This is annoying but a smallish price to
|
||||
// pay for structural validity.
|
||||
//
|
||||
// A `ShieldedData<PerSpendAnchor>` can never be equal to a
|
||||
// `ShieldedData<SharedAnchor>`, even if they have the same effects.
|
||||
|
||||
impl<AnchorV> std::cmp::PartialEq for ShieldedData<AnchorV>
|
||||
impl<AnchorV> TransferData<AnchorV>
|
||||
where
|
||||
AnchorV: AnchorVariant + Clone + PartialEq,
|
||||
AnchorV: AnchorVariant + Clone,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// First check that the lengths match, so we know it is safe to use zip,
|
||||
// which truncates to the shorter of the two iterators.
|
||||
if self.spends().count() != other.spends().count() {
|
||||
return false;
|
||||
}
|
||||
if self.outputs().count() != other.outputs().count() {
|
||||
return false;
|
||||
}
|
||||
/// Iterate over the [`Spend`]s for this transaction, returning them as
|
||||
/// their generic type.
|
||||
pub fn spends(&self) -> impl Iterator<Item = &Spend<AnchorV>> {
|
||||
use TransferData::*;
|
||||
|
||||
// Now check that all the fields match
|
||||
self.value_balance == other.value_balance
|
||||
&& self.shared_anchor == other.shared_anchor
|
||||
&& self.binding_sig == other.binding_sig
|
||||
&& self.spends().zip(other.spends()).all(|(a, b)| a == b)
|
||||
&& self.outputs().zip(other.outputs()).all(|(a, b)| a == b)
|
||||
let spends = match self {
|
||||
SpendsAndMaybeOutputs { spends, .. } => Some(spends.iter()),
|
||||
JustOutputs { .. } => None,
|
||||
};
|
||||
|
||||
// this awkward construction avoids returning a newtype struct or
|
||||
// type-erased boxed iterator
|
||||
spends.into_iter().flatten()
|
||||
}
|
||||
|
||||
/// Iterate over the [`Output`]s for this transaction.
|
||||
pub fn outputs(&self) -> impl Iterator<Item = &Output> {
|
||||
use TransferData::*;
|
||||
|
||||
match self {
|
||||
SpendsAndMaybeOutputs { maybe_outputs, .. } => maybe_outputs,
|
||||
JustOutputs { outputs, .. } => outputs.as_vec(),
|
||||
}
|
||||
.iter()
|
||||
}
|
||||
|
||||
/// Provide the shared anchor for this transaction, if present.
|
||||
///
|
||||
/// The shared anchor is only present if:
|
||||
/// * there is at least one spend, and
|
||||
/// * this is a `V5` transaction.
|
||||
pub fn shared_anchor(&self) -> Option<AnchorV::Shared> {
|
||||
use TransferData::*;
|
||||
|
||||
match self {
|
||||
SpendsAndMaybeOutputs { shared_anchor, .. } => Some(shared_anchor.clone()),
|
||||
JustOutputs { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<AnchorV> std::cmp::Eq for ShieldedData<AnchorV> where AnchorV: AnchorVariant + Clone + PartialEq
|
||||
{}
|
||||
|
||||
impl TrustedPreallocate for Groth16Proof {
|
||||
fn max_allocation() -> u64 {
|
||||
// Each V5 transaction proof array entry must have a corresponding
|
||||
|
|
|
@ -2,13 +2,11 @@ use proptest::prelude::*;
|
|||
|
||||
use crate::{
|
||||
block,
|
||||
sapling::{self, PerSpendAnchor, SharedAnchor},
|
||||
sapling::{self, OutputInTransactionV4, PerSpendAnchor, SharedAnchor},
|
||||
serialization::{ZcashDeserializeInto, ZcashSerialize},
|
||||
transaction::{LockTime, Transaction},
|
||||
};
|
||||
|
||||
use futures::future::Either;
|
||||
use sapling::OutputInTransactionV4;
|
||||
use std::convert::TryInto;
|
||||
|
||||
proptest! {
|
||||
/// Serialize and deserialize `Spend<PerSpendAnchor>`
|
||||
|
@ -165,8 +163,6 @@ proptest! {
|
|||
fn shielded_data_v4_outputs_only(
|
||||
shielded_v4 in any::<sapling::ShieldedData<PerSpendAnchor>>(),
|
||||
) {
|
||||
use Either::*;
|
||||
|
||||
zebra_test::init();
|
||||
|
||||
// we need at least one output to delete all the spends
|
||||
|
@ -174,10 +170,10 @@ proptest! {
|
|||
|
||||
// TODO: modify the strategy, rather than the shielded data
|
||||
let mut shielded_v4 = shielded_v4;
|
||||
let mut outputs: Vec<_> = shielded_v4.outputs().cloned().collect();
|
||||
shielded_v4.rest_spends = Vec::new();
|
||||
shielded_v4.first = Right(outputs.remove(0));
|
||||
shielded_v4.rest_outputs = outputs;
|
||||
let outputs: Vec<_> = shielded_v4.outputs().cloned().collect();
|
||||
shielded_v4.transfers = sapling::TransferData::JustOutputs {
|
||||
outputs: outputs.try_into().unwrap(),
|
||||
};
|
||||
|
||||
// shielded data doesn't serialize by itself, so we have to stick it in
|
||||
// a transaction
|
||||
|
@ -206,8 +202,6 @@ proptest! {
|
|||
fn shielded_data_v5_outputs_only(
|
||||
shielded_v5 in any::<sapling::ShieldedData<SharedAnchor>>(),
|
||||
) {
|
||||
use Either::*;
|
||||
|
||||
zebra_test::init();
|
||||
|
||||
// we need at least one output to delete all the spends
|
||||
|
@ -215,12 +209,10 @@ proptest! {
|
|||
|
||||
// TODO: modify the strategy, rather than the shielded data
|
||||
let mut shielded_v5 = shielded_v5;
|
||||
let mut outputs: Vec<_> = shielded_v5.outputs().cloned().collect();
|
||||
shielded_v5.rest_spends = Vec::new();
|
||||
shielded_v5.first = Right(outputs.remove(0));
|
||||
shielded_v5.rest_outputs = outputs;
|
||||
// TODO: delete the shared anchor when there are no spends
|
||||
shielded_v5.shared_anchor = Default::default();
|
||||
let outputs: Vec<_> = shielded_v5.outputs().cloned().collect();
|
||||
shielded_v5.transfers = sapling::TransferData::JustOutputs {
|
||||
outputs: outputs.try_into().unwrap(),
|
||||
};
|
||||
|
||||
let data = shielded_v5.zcash_serialize_to_vec().expect("shielded_v5 should serialize");
|
||||
let shielded_v5_parsed = data.zcash_deserialize_into().expect("randomized shielded_v5 should deserialize");
|
||||
|
@ -237,251 +229,4 @@ proptest! {
|
|||
panic!("unexpected parsing error: ShieldedData should be Some(_)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that ShieldedData<PerSpendAnchor> is equal when `first` is swapped
|
||||
/// between a spend and an output
|
||||
#[test]
|
||||
fn shielded_data_per_spend_swap_first_eq(
|
||||
shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>()
|
||||
) {
|
||||
use Either::*;
|
||||
|
||||
zebra_test::init();
|
||||
|
||||
// we need at least one spend and one output to swap them
|
||||
prop_assume!(shielded1.spends().count() > 0 && shielded1.outputs().count() > 0);
|
||||
|
||||
let mut shielded2 = shielded1.clone();
|
||||
let mut spends: Vec<_> = shielded2.spends().cloned().collect();
|
||||
let mut outputs: Vec<_> = shielded2.outputs().cloned().collect();
|
||||
match shielded2.first {
|
||||
Left(_spend) => {
|
||||
shielded2.first = Right(outputs.remove(0));
|
||||
shielded2.rest_outputs = outputs;
|
||||
shielded2.rest_spends = spends;
|
||||
}
|
||||
Right(_output) => {
|
||||
shielded2.first = Left(spends.remove(0));
|
||||
shielded2.rest_spends = spends;
|
||||
shielded2.rest_outputs = outputs;
|
||||
}
|
||||
}
|
||||
|
||||
prop_assert_eq![&shielded1, &shielded2];
|
||||
|
||||
// shielded data doesn't serialize by itself, so we have to stick it in
|
||||
// a transaction
|
||||
let tx1 = Transaction::V4 {
|
||||
inputs: Vec::new(),
|
||||
outputs: Vec::new(),
|
||||
lock_time: LockTime::min_lock_time(),
|
||||
expiry_height: block::Height(0),
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: Some(shielded1),
|
||||
};
|
||||
let tx2 = Transaction::V4 {
|
||||
inputs: Vec::new(),
|
||||
outputs: Vec::new(),
|
||||
lock_time: LockTime::min_lock_time(),
|
||||
expiry_height: block::Height(0),
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: Some(shielded2),
|
||||
};
|
||||
|
||||
prop_assert_eq![&tx1, &tx2];
|
||||
|
||||
let data1 = tx1.zcash_serialize_to_vec().expect("tx1 should serialize");
|
||||
let data2 = tx2.zcash_serialize_to_vec().expect("tx2 should serialize");
|
||||
|
||||
prop_assert_eq![data1, data2];
|
||||
}
|
||||
|
||||
/// Check that ShieldedData<SharedAnchor> is equal when `first` is swapped
|
||||
/// between a spend and an output
|
||||
#[test]
|
||||
fn shielded_data_shared_swap_first_eq(
|
||||
shielded1 in any::<sapling::ShieldedData<SharedAnchor>>()
|
||||
) {
|
||||
use Either::*;
|
||||
|
||||
zebra_test::init();
|
||||
|
||||
// we need at least one spend and one output to swap them
|
||||
prop_assume!(shielded1.spends().count() > 0 && shielded1.outputs().count() > 0);
|
||||
|
||||
let mut shielded2 = shielded1.clone();
|
||||
let mut spends: Vec<_> = shielded2.spends().cloned().collect();
|
||||
let mut outputs: Vec<_> = shielded2.outputs().cloned().collect();
|
||||
match shielded2.first {
|
||||
Left(_spend) => {
|
||||
shielded2.first = Right(outputs.remove(0));
|
||||
shielded2.rest_outputs = outputs;
|
||||
shielded2.rest_spends = spends;
|
||||
}
|
||||
Right(_output) => {
|
||||
shielded2.first = Left(spends.remove(0));
|
||||
shielded2.rest_spends = spends;
|
||||
shielded2.rest_outputs = outputs;
|
||||
}
|
||||
}
|
||||
|
||||
prop_assert_eq![&shielded1, &shielded2];
|
||||
|
||||
let data1 = shielded1.zcash_serialize_to_vec().expect("shielded1 should serialize");
|
||||
let data2 = shielded2.zcash_serialize_to_vec().expect("shielded2 should serialize");
|
||||
|
||||
prop_assert_eq![data1, data2];
|
||||
}
|
||||
|
||||
/// Check that ShieldedData<PerSpendAnchor> serialization is equal if
|
||||
/// `shielded1 == shielded2`
|
||||
#[test]
|
||||
fn shielded_data_per_spend_serialize_eq(
|
||||
shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>(),
|
||||
shielded2 in any::<sapling::ShieldedData<PerSpendAnchor>>()
|
||||
) {
|
||||
zebra_test::init();
|
||||
|
||||
let shielded_eq = shielded1 == shielded2;
|
||||
|
||||
// shielded data doesn't serialize by itself, so we have to stick it in
|
||||
// a transaction
|
||||
let tx1 = Transaction::V4 {
|
||||
inputs: Vec::new(),
|
||||
outputs: Vec::new(),
|
||||
lock_time: LockTime::min_lock_time(),
|
||||
expiry_height: block::Height(0),
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: Some(shielded1),
|
||||
};
|
||||
let tx2 = Transaction::V4 {
|
||||
inputs: Vec::new(),
|
||||
outputs: Vec::new(),
|
||||
lock_time: LockTime::min_lock_time(),
|
||||
expiry_height: block::Height(0),
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: Some(shielded2),
|
||||
};
|
||||
|
||||
if shielded_eq {
|
||||
prop_assert_eq![&tx1, &tx2];
|
||||
} else {
|
||||
prop_assert_ne![&tx1, &tx2];
|
||||
}
|
||||
|
||||
let data1 = tx1.zcash_serialize_to_vec().expect("tx1 should serialize");
|
||||
let data2 = tx2.zcash_serialize_to_vec().expect("tx2 should serialize");
|
||||
|
||||
if shielded_eq {
|
||||
prop_assert_eq![data1, data2];
|
||||
} else {
|
||||
prop_assert_ne![data1, data2];
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that ShieldedData<SharedAnchor> serialization is equal if
|
||||
/// `shielded1 == shielded2`
|
||||
#[test]
|
||||
fn shielded_data_shared_serialize_eq(
|
||||
shielded1 in any::<sapling::ShieldedData<SharedAnchor>>(),
|
||||
shielded2 in any::<sapling::ShieldedData<SharedAnchor>>()
|
||||
) {
|
||||
zebra_test::init();
|
||||
|
||||
let shielded_eq = shielded1 == shielded2;
|
||||
|
||||
let data1 = shielded1.zcash_serialize_to_vec().expect("shielded1 should serialize");
|
||||
let data2 = shielded2.zcash_serialize_to_vec().expect("shielded2 should serialize");
|
||||
|
||||
if shielded_eq {
|
||||
prop_assert_eq![data1, data2];
|
||||
} else {
|
||||
prop_assert_ne![data1, data2];
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that ShieldedData<PerSpendAnchor> serialization is equal when we
|
||||
/// replace all the known fields.
|
||||
///
|
||||
/// This test checks for extra fields that are not in `ShieldedData::eq`.
|
||||
#[test]
|
||||
fn shielded_data_per_spend_field_assign_eq(
|
||||
shielded1 in any::<sapling::ShieldedData<PerSpendAnchor>>(),
|
||||
shielded2 in any::<sapling::ShieldedData<PerSpendAnchor>>()
|
||||
) {
|
||||
zebra_test::init();
|
||||
|
||||
let mut shielded2 = shielded2;
|
||||
|
||||
// these fields must match ShieldedData::eq
|
||||
// the spends() and outputs() checks cover first, rest_spends, and rest_outputs
|
||||
shielded2.first = shielded1.first.clone();
|
||||
shielded2.rest_spends = shielded1.rest_spends.clone();
|
||||
shielded2.rest_outputs = shielded1.rest_outputs.clone();
|
||||
// now for the fields that are checked literally
|
||||
shielded2.value_balance = shielded1.value_balance;
|
||||
shielded2.shared_anchor = shielded1.shared_anchor;
|
||||
shielded2.binding_sig = shielded1.binding_sig;
|
||||
|
||||
prop_assert_eq![&shielded1, &shielded2];
|
||||
|
||||
// shielded data doesn't serialize by itself, so we have to stick it in
|
||||
// a transaction
|
||||
let tx1 = Transaction::V4 {
|
||||
inputs: Vec::new(),
|
||||
outputs: Vec::new(),
|
||||
lock_time: LockTime::min_lock_time(),
|
||||
expiry_height: block::Height(0),
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: Some(shielded1),
|
||||
};
|
||||
let tx2 = Transaction::V4 {
|
||||
inputs: Vec::new(),
|
||||
outputs: Vec::new(),
|
||||
lock_time: LockTime::min_lock_time(),
|
||||
expiry_height: block::Height(0),
|
||||
joinsplit_data: None,
|
||||
sapling_shielded_data: Some(shielded2),
|
||||
};
|
||||
|
||||
prop_assert_eq![&tx1, &tx2];
|
||||
|
||||
let data1 = tx1.zcash_serialize_to_vec().expect("tx1 should serialize");
|
||||
let data2 = tx2.zcash_serialize_to_vec().expect("tx2 should serialize");
|
||||
|
||||
prop_assert_eq![data1, data2];
|
||||
}
|
||||
|
||||
/// Check that ShieldedData<SharedAnchor> serialization is equal when we
|
||||
/// replace all the known fields.
|
||||
///
|
||||
/// This test checks for extra fields that are not in `ShieldedData::eq`.
|
||||
#[test]
|
||||
fn shielded_data_shared_field_assign_eq(
|
||||
shielded1 in any::<sapling::ShieldedData<SharedAnchor>>(),
|
||||
shielded2 in any::<sapling::ShieldedData<SharedAnchor>>()
|
||||
) {
|
||||
zebra_test::init();
|
||||
|
||||
let mut shielded2 = shielded2;
|
||||
|
||||
// TODO: modify the strategy, rather than the shielded data
|
||||
//
|
||||
// these fields must match ShieldedData::eq
|
||||
// the spends() and outputs() checks cover first, rest_spends, and rest_outputs
|
||||
shielded2.first = shielded1.first.clone();
|
||||
shielded2.rest_spends = shielded1.rest_spends.clone();
|
||||
shielded2.rest_outputs = shielded1.rest_outputs.clone();
|
||||
// now for the fields that are checked literally
|
||||
shielded2.value_balance = shielded1.value_balance;
|
||||
shielded2.shared_anchor = shielded1.shared_anchor;
|
||||
shielded2.binding_sig = shielded1.binding_sig;
|
||||
|
||||
prop_assert_eq![&shielded1, &shielded2];
|
||||
|
||||
let data1 = shielded1.zcash_serialize_to_vec().expect("shielded1 should serialize");
|
||||
let data2 = shielded2.zcash_serialize_to_vec().expect("shielded2 should serialize");
|
||||
|
||||
prop_assert_eq![data1, data2];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//! `ReadZcashExt`, extension traits for `io::Read` and `io::Write` with utility functions
|
||||
//! for reading and writing data (e.g., the Bitcoin variable-integer format).
|
||||
|
||||
mod constraint;
|
||||
mod error;
|
||||
mod read_zcash;
|
||||
mod write_zcash;
|
||||
|
@ -16,6 +17,7 @@ pub(crate) mod serde_helpers;
|
|||
|
||||
pub mod sha256d;
|
||||
|
||||
pub use constraint::AtLeastOne;
|
||||
pub use error::SerializationError;
|
||||
pub use read_zcash::ReadZcashExt;
|
||||
pub use write_zcash::WriteZcashExt;
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
//! Serialization constraint helpers.
|
||||
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::serialization::SerializationError;
|
||||
|
||||
/// A `Vec<T>` wrapper that ensures there is at least one `T` in the vector.
|
||||
///
|
||||
/// You can initialize `AtLeastOne` using:
|
||||
/// ```
|
||||
/// # use zebra_chain::serialization::{AtLeastOne, SerializationError};
|
||||
/// # use std::convert::{TryFrom, TryInto};
|
||||
/// #
|
||||
/// let v: AtLeastOne<u32> = vec![42].try_into()?;
|
||||
/// assert_eq!(v.as_slice(), [42]);
|
||||
///
|
||||
/// let v: AtLeastOne<u32> = vec![42].as_slice().try_into()?;
|
||||
/// assert_eq!(v.as_slice(), [42]);
|
||||
///
|
||||
/// let v: AtLeastOne<u32> = [42].try_into()?;
|
||||
/// assert_eq!(v.as_slice(), [42]);
|
||||
///
|
||||
/// let v = AtLeastOne::<u32>::try_from(&[42])?;
|
||||
/// assert_eq!(v.as_slice(), [42]);
|
||||
/// #
|
||||
/// # Ok::<(), SerializationError>(())
|
||||
/// ```
|
||||
///
|
||||
/// And access the inner vector via [deref coercion](https://doc.rust-lang.org/std/ops/trait.Deref.html#more-on-deref-coercion),
|
||||
/// an explicit conversion, or as a slice:
|
||||
/// ```
|
||||
/// # use zebra_chain::serialization::AtLeastOne;
|
||||
/// # use std::convert::TryInto;
|
||||
/// #
|
||||
/// # let v: AtLeastOne<u32> = vec![42].try_into().unwrap();
|
||||
/// #
|
||||
/// let first = v.iter().next().expect("AtLeastOne always has a first element");
|
||||
/// assert_eq!(*first, 42);
|
||||
///
|
||||
/// let s = v.as_slice();
|
||||
/// #
|
||||
/// # assert_eq!(s, [42]);
|
||||
///
|
||||
/// let mut m = v.into_vec();
|
||||
/// #
|
||||
/// # assert_eq!(m.as_slice(), [42]);
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// `AtLeastOne` also re-implements some slice methods with different return
|
||||
/// types, to avoid redundant unwraps:
|
||||
/// ```
|
||||
/// # use zebra_chain::serialization::AtLeastOne;
|
||||
/// # use std::convert::TryInto;
|
||||
/// #
|
||||
/// # let v: AtLeastOne<u32> = vec![42].try_into().unwrap();
|
||||
/// #
|
||||
/// let first = v.first();
|
||||
/// assert_eq!(*first, 42);
|
||||
///
|
||||
/// let (first, rest) = v.split_first();
|
||||
/// assert_eq!(*first, 42);
|
||||
/// assert!(rest.is_empty());
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct AtLeastOne<T> {
|
||||
/// The inner vector, which must have at least one element.
|
||||
///
|
||||
/// `inner` is private, so that it can't be modified in ways that break the
|
||||
/// type constraint.
|
||||
inner: Vec<T>,
|
||||
}
|
||||
|
||||
// CORRECTNESS
|
||||
//
|
||||
// All conversions to `AtLeastOne<T>` must go through `TryFrom<Vec<T>>`,
|
||||
// so that the type constraint is satisfied.
|
||||
|
||||
impl<T> TryFrom<Vec<T>> for AtLeastOne<T> {
|
||||
type Error = SerializationError;
|
||||
|
||||
fn try_from(vec: Vec<T>) -> Result<Self, Self::Error> {
|
||||
if vec.is_empty() {
|
||||
Err(SerializationError::Parse("expected at least one item"))
|
||||
} else {
|
||||
Ok(AtLeastOne { inner: vec })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TryFrom<&[T]> for AtLeastOne<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
type Error = SerializationError;
|
||||
|
||||
fn try_from(slice: &[T]) -> Result<Self, Self::Error> {
|
||||
slice.to_vec().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - reject [T; 0] at compile time and impl From instead?
|
||||
impl<T, const N: usize> TryFrom<[T; N]> for AtLeastOne<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
type Error = SerializationError;
|
||||
|
||||
fn try_from(slice: [T; N]) -> Result<Self, Self::Error> {
|
||||
slice.to_vec().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - reject [T; 0] at compile time and impl From instead?
|
||||
// - remove when std is updated so that `TryFrom<&U>` is always implemented when
|
||||
// `TryFrom<U>`
|
||||
impl<T, const N: usize> TryFrom<&[T; N]> for AtLeastOne<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
type Error = SerializationError;
|
||||
|
||||
fn try_from(slice: &[T; N]) -> Result<Self, Self::Error> {
|
||||
slice.to_vec().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
// Deref (but not DerefMut, because that could break the constraint)
|
||||
|
||||
impl<T> Deref for AtLeastOne<T> {
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
// Extracting one or more items
|
||||
|
||||
impl<T> From<AtLeastOne<T>> for Vec<T> {
|
||||
fn from(vec1: AtLeastOne<T>) -> Self {
|
||||
vec1.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AtLeastOne<T> {
|
||||
/// Returns a reference to the inner vector.
|
||||
pub fn as_vec(&self) -> &Vec<T> {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Converts `self` into a vector without clones or allocation.
|
||||
///
|
||||
/// The resulting vector can be converted back into `AtLeastOne` via `try_into`.
|
||||
pub fn into_vec(self) -> Vec<T> {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// Returns the first element.
|
||||
///
|
||||
/// Unlike `Vec` or slice, `AtLeastOne` always has a first element.
|
||||
pub fn first(&self) -> &T {
|
||||
&self.inner[0]
|
||||
}
|
||||
|
||||
/// Returns the first and all the rest of the elements of the vector.
|
||||
///
|
||||
/// Unlike `Vec` or slice, `AtLeastOne` always has a first element.
|
||||
pub fn split_first(&self) -> (&T, &[T]) {
|
||||
(&self.inner[0], &self.inner[1..])
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: consider implementing `push`, `append`, and `Extend`,
|
||||
// because adding elements can't break the constraint.
|
|
@ -35,10 +35,3 @@ impl From<Fq> for jubjub::Fq {
|
|||
jubjub::Fq::from_bytes(&local.bytes).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(remote = "futures::future::Either")]
|
||||
pub enum Either<A, B> {
|
||||
Left(A),
|
||||
Right(B),
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
|||
io,
|
||||
};
|
||||
|
||||
use super::{ReadZcashExt, SerializationError, MAX_PROTOCOL_MESSAGE_LEN};
|
||||
use super::{AtLeastOne, ReadZcashExt, SerializationError, MAX_PROTOCOL_MESSAGE_LEN};
|
||||
|
||||
/// Consensus-critical serialization for Zcash.
|
||||
///
|
||||
|
@ -32,6 +32,15 @@ impl<T: ZcashDeserialize + TrustedPreallocate> ZcashDeserialize for Vec<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Deserialize an `AtLeastOne` vector, where the number of items is set by a
|
||||
/// compactsize prefix in the data. This is the most common format in Zcash.
|
||||
impl<T: ZcashDeserialize + TrustedPreallocate> ZcashDeserialize for AtLeastOne<T> {
|
||||
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
||||
let v: Vec<T> = (&mut reader).zcash_deserialize_into()?;
|
||||
v.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement ZcashDeserialize for Vec<u8> directly instead of using the blanket Vec implementation
|
||||
///
|
||||
/// This allows us to optimize the inner loop into a single call to `read_exact()`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::io;
|
||||
|
||||
use super::WriteZcashExt;
|
||||
use super::{AtLeastOne, WriteZcashExt};
|
||||
|
||||
/// Consensus-critical serialization for Zcash.
|
||||
///
|
||||
|
@ -40,6 +40,14 @@ impl<T: ZcashSerialize> ZcashSerialize for Vec<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Serialize an `AtLeastOne` vector as a compactsize number of items, then the
|
||||
/// items. This is the most common format in Zcash.
|
||||
impl<T: ZcashSerialize> ZcashSerialize for AtLeastOne<T> {
|
||||
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
||||
self.as_vec().zcash_serialize(&mut writer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize a typed `Vec` **without** writing the number of items as a
|
||||
/// compactsize.
|
||||
///
|
||||
|
|
|
@ -167,6 +167,8 @@ impl Transaction {
|
|||
pub fn sprout_nullifiers(&self) -> Box<dyn Iterator<Item = &sprout::Nullifier> + '_> {
|
||||
// This function returns a boxed iterator because the different
|
||||
// transaction variants end up having different iterator types
|
||||
// (we could extract bctv and groth as separate iterators, then chain
|
||||
// them together, but that would be much harder to read and maintain)
|
||||
match self {
|
||||
// JoinSplits with Bctv14 Proofs
|
||||
Transaction::V2 {
|
||||
|
@ -176,20 +178,12 @@ impl Transaction {
|
|||
| Transaction::V3 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits()
|
||||
.flat_map(|joinsplit| joinsplit.nullifiers.iter()),
|
||||
),
|
||||
} => Box::new(joinsplit_data.nullifiers()),
|
||||
// JoinSplits with Groth Proofs
|
||||
Transaction::V4 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits()
|
||||
.flat_map(|joinsplit| joinsplit.nullifiers.iter()),
|
||||
),
|
||||
} => Box::new(joinsplit_data.nullifiers()),
|
||||
// No JoinSplits
|
||||
Transaction::V1 { .. }
|
||||
| Transaction::V2 {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use futures::future::Either;
|
||||
use proptest::{arbitrary::any, array, collection::vec, option, prelude::*};
|
||||
|
||||
use crate::LedgerState;
|
||||
|
@ -14,6 +13,7 @@ use crate::{
|
|||
};
|
||||
|
||||
use super::{FieldNotPresent, JoinSplitData, LockTime, Memo, Transaction};
|
||||
use sapling::{AnchorVariant, PerSpendAnchor, SharedAnchor};
|
||||
|
||||
impl Transaction {
|
||||
/// Generate a proptest strategy for V1 Transactions
|
||||
|
@ -206,32 +206,59 @@ impl<P: ZkSnarkProof + Arbitrary + 'static> Arbitrary for JoinSplitData<P> {
|
|||
type Strategy = BoxedStrategy<Self>;
|
||||
}
|
||||
|
||||
impl Arbitrary for sapling::ShieldedData<sapling::PerSpendAnchor> {
|
||||
impl<AnchorV> Arbitrary for sapling::ShieldedData<AnchorV>
|
||||
where
|
||||
AnchorV: AnchorVariant + Clone + std::fmt::Debug + 'static,
|
||||
sapling::TransferData<AnchorV>: Arbitrary,
|
||||
{
|
||||
type Parameters = ();
|
||||
|
||||
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||
(
|
||||
any::<Amount>(),
|
||||
prop_oneof![
|
||||
any::<sapling::Spend<sapling::PerSpendAnchor>>().prop_map(Either::Left),
|
||||
any::<sapling::Output>().prop_map(Either::Right)
|
||||
],
|
||||
vec(any::<sapling::Spend<sapling::PerSpendAnchor>>(), 0..10),
|
||||
vec(any::<sapling::Output>(), 0..10),
|
||||
any::<sapling::TransferData<AnchorV>>(),
|
||||
vec(any::<u8>(), 64),
|
||||
)
|
||||
.prop_map(
|
||||
|(value_balance, first, rest_spends, rest_outputs, sig_bytes)| Self {
|
||||
value_balance,
|
||||
shared_anchor: FieldNotPresent,
|
||||
first,
|
||||
rest_spends,
|
||||
rest_outputs,
|
||||
binding_sig: redjubjub::Signature::from({
|
||||
let mut b = [0u8; 64];
|
||||
b.copy_from_slice(sig_bytes.as_slice());
|
||||
b
|
||||
}),
|
||||
.prop_map(|(value_balance, transfers, sig_bytes)| Self {
|
||||
value_balance,
|
||||
transfers,
|
||||
binding_sig: redjubjub::Signature::from({
|
||||
let mut b = [0u8; 64];
|
||||
b.copy_from_slice(sig_bytes.as_slice());
|
||||
b
|
||||
}),
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
}
|
||||
|
||||
impl Arbitrary for sapling::TransferData<PerSpendAnchor> {
|
||||
type Parameters = ();
|
||||
|
||||
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||
// TODO: add an extra spend or output using Either, and stop using filter_map
|
||||
(
|
||||
vec(any::<sapling::Spend<PerSpendAnchor>>(), 0..10),
|
||||
vec(any::<sapling::Output>(), 0..10),
|
||||
)
|
||||
.prop_filter_map(
|
||||
"arbitrary v4 transfers with no spends and no outputs",
|
||||
|(spends, outputs)| {
|
||||
if !spends.is_empty() {
|
||||
Some(sapling::TransferData::SpendsAndMaybeOutputs {
|
||||
shared_anchor: FieldNotPresent,
|
||||
spends: spends.try_into().unwrap(),
|
||||
maybe_outputs: outputs,
|
||||
})
|
||||
} else if !outputs.is_empty() {
|
||||
Some(sapling::TransferData::JustOutputs {
|
||||
outputs: outputs.try_into().unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
|
@ -240,40 +267,32 @@ impl Arbitrary for sapling::ShieldedData<sapling::PerSpendAnchor> {
|
|||
type Strategy = BoxedStrategy<Self>;
|
||||
}
|
||||
|
||||
impl Arbitrary for sapling::ShieldedData<sapling::SharedAnchor> {
|
||||
impl Arbitrary for sapling::TransferData<SharedAnchor> {
|
||||
type Parameters = ();
|
||||
|
||||
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||
// TODO: add an extra spend or output using Either, and stop using filter_map
|
||||
(
|
||||
any::<Amount>(),
|
||||
any::<sapling::tree::Root>(),
|
||||
prop_oneof![
|
||||
any::<sapling::Spend<sapling::SharedAnchor>>().prop_map(Either::Left),
|
||||
any::<sapling::Output>().prop_map(Either::Right)
|
||||
],
|
||||
vec(any::<sapling::Spend<sapling::SharedAnchor>>(), 0..10),
|
||||
vec(any::<sapling::Spend<SharedAnchor>>(), 0..10),
|
||||
vec(any::<sapling::Output>(), 0..10),
|
||||
vec(any::<u8>(), 64),
|
||||
)
|
||||
.prop_map(
|
||||
|(value_balance, shared_anchor, first, rest_spends, rest_outputs, sig_bytes)| {
|
||||
let mut shielded_data = Self {
|
||||
value_balance,
|
||||
shared_anchor,
|
||||
first,
|
||||
rest_spends,
|
||||
rest_outputs,
|
||||
binding_sig: redjubjub::Signature::from({
|
||||
let mut b = [0u8; 64];
|
||||
b.copy_from_slice(sig_bytes.as_slice());
|
||||
b
|
||||
}),
|
||||
};
|
||||
if shielded_data.spends().count() == 0 {
|
||||
// Todo: delete field when there is no spend
|
||||
shielded_data.shared_anchor = Default::default();
|
||||
.prop_filter_map(
|
||||
"arbitrary v5 transfers with no spends and no outputs",
|
||||
|(shared_anchor, spends, outputs)| {
|
||||
if !spends.is_empty() {
|
||||
Some(sapling::TransferData::SpendsAndMaybeOutputs {
|
||||
shared_anchor,
|
||||
spends: spends.try_into().unwrap(),
|
||||
maybe_outputs: outputs,
|
||||
})
|
||||
} else if !outputs.is_empty() {
|
||||
Some(sapling::TransferData::JustOutputs {
|
||||
outputs: outputs.try_into().unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
shielded_data
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
|
|
|
@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::{
|
||||
primitives::{ed25519, ZkSnarkProof},
|
||||
sprout::JoinSplit,
|
||||
sprout::{JoinSplit, Nullifier},
|
||||
};
|
||||
|
||||
/// A bundle of [`JoinSplit`] descriptions and signature data.
|
||||
|
@ -48,4 +48,10 @@ impl<P: ZkSnarkProof> JoinSplitData<P> {
|
|||
pub fn joinsplits(&self) -> impl Iterator<Item = &JoinSplit<P>> {
|
||||
std::iter::once(&self.first).chain(self.rest.iter())
|
||||
}
|
||||
|
||||
/// Iterate over the [`Nullifier`]s in `self`.
|
||||
pub fn nullifiers(&self) -> impl Iterator<Item = &Nullifier> {
|
||||
self.joinsplits()
|
||||
.flat_map(|joinsplit| joinsplit.nullifiers.iter())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Contains impls of `ZcashSerialize`, `ZcashDeserialize` for all of the
|
||||
//! transaction types, so that all of the serialization logic is in one place.
|
||||
|
||||
use std::{io, sync::Arc};
|
||||
use std::{convert::TryInto, io, sync::Arc};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
|
@ -120,8 +120,10 @@ impl ZcashSerialize for sapling::ShieldedData<SharedAnchor> {
|
|||
self.value_balance.zcash_serialize(&mut writer)?;
|
||||
|
||||
// anchorSapling
|
||||
if !spend_prefixes.is_empty() {
|
||||
writer.write_all(&<[u8; 32]>::from(self.shared_anchor)[..])?;
|
||||
// `TransferData` ensures this field is only present when there is at
|
||||
// least one spend.
|
||||
if let Some(shared_anchor) = self.shared_anchor() {
|
||||
writer.write_all(&<[u8; 32]>::from(shared_anchor)[..])?;
|
||||
}
|
||||
|
||||
// vSpendProofsSapling
|
||||
|
@ -162,10 +164,11 @@ impl ZcashDeserialize for Option<sapling::ShieldedData<SharedAnchor>> {
|
|||
let value_balance = (&mut reader).zcash_deserialize_into()?;
|
||||
|
||||
// anchorSapling
|
||||
let mut shared_anchor = None;
|
||||
if spends_count > 0 {
|
||||
shared_anchor = Some(reader.read_32_bytes()?.into());
|
||||
}
|
||||
let shared_anchor = if spends_count > 0 {
|
||||
Some(reader.read_32_bytes()?.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// vSpendProofsSapling
|
||||
let spend_proofs = zcash_deserialize_external_count(spends_count, &mut reader)?;
|
||||
|
@ -179,7 +182,7 @@ impl ZcashDeserialize for Option<sapling::ShieldedData<SharedAnchor>> {
|
|||
let binding_sig = reader.read_64_bytes()?.into();
|
||||
|
||||
// Create shielded spends from deserialized parts
|
||||
let mut spends: Vec<_> = spend_prefixes
|
||||
let spends: Vec<_> = spend_prefixes
|
||||
.into_iter()
|
||||
.zip(spend_proofs.into_iter())
|
||||
.zip(spend_sigs.into_iter())
|
||||
|
@ -187,44 +190,33 @@ impl ZcashDeserialize for Option<sapling::ShieldedData<SharedAnchor>> {
|
|||
.collect();
|
||||
|
||||
// Create shielded outputs from deserialized parts
|
||||
let mut outputs = output_prefixes
|
||||
let outputs = output_prefixes
|
||||
.into_iter()
|
||||
.zip(output_proofs.into_iter())
|
||||
.map(|(prefix, proof)| Output::from_v5_parts(prefix, proof))
|
||||
.collect();
|
||||
|
||||
// Create shielded data
|
||||
use futures::future::Either::*;
|
||||
// TODO: Use a Spend for first if both are present, because the first
|
||||
// spend activates the shared anchor.
|
||||
if spends_count > 0 {
|
||||
Ok(Some(sapling::ShieldedData {
|
||||
value_balance,
|
||||
// TODO: cleanup shared anchor parsing
|
||||
shared_anchor: shared_anchor.expect("present when spends_count > 0"),
|
||||
first: Left(spends.remove(0)),
|
||||
rest_spends: spends,
|
||||
rest_outputs: outputs,
|
||||
binding_sig,
|
||||
}))
|
||||
} else {
|
||||
assert!(
|
||||
outputs_count > 0,
|
||||
"parsing returns early when there are no spends and no outputs"
|
||||
);
|
||||
// Create transfers
|
||||
let transfers = match shared_anchor {
|
||||
Some(shared_anchor) => sapling::TransferData::SpendsAndMaybeOutputs {
|
||||
shared_anchor,
|
||||
spends: spends
|
||||
.try_into()
|
||||
.expect("checked spends when parsing shared anchor"),
|
||||
maybe_outputs: outputs,
|
||||
},
|
||||
None => sapling::TransferData::JustOutputs {
|
||||
outputs: outputs
|
||||
.try_into()
|
||||
.expect("checked spends or outputs and returned early"),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Some(sapling::ShieldedData {
|
||||
value_balance,
|
||||
// TODO: delete shared anchor when there are no spends
|
||||
shared_anchor: shared_anchor.unwrap_or_default(),
|
||||
first: Right(outputs.remove(0)),
|
||||
// the spends are actually empty here, but we use the
|
||||
// vec for consistency and readability
|
||||
rest_spends: spends,
|
||||
rest_outputs: outputs,
|
||||
binding_sig,
|
||||
}))
|
||||
}
|
||||
Ok(Some(sapling::ShieldedData {
|
||||
value_balance,
|
||||
transfers,
|
||||
binding_sig,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -449,8 +441,8 @@ impl ZcashDeserialize for Transaction {
|
|||
let expiry_height = block::Height(reader.read_u32::<LittleEndian>()?);
|
||||
|
||||
let value_balance = (&mut reader).zcash_deserialize_into()?;
|
||||
let mut shielded_spends = Vec::zcash_deserialize(&mut reader)?;
|
||||
let mut shielded_outputs =
|
||||
let shielded_spends = Vec::zcash_deserialize(&mut reader)?;
|
||||
let shielded_outputs =
|
||||
Vec::<sapling::OutputInTransactionV4>::zcash_deserialize(&mut reader)?
|
||||
.into_iter()
|
||||
.map(Output::from_v4)
|
||||
|
@ -458,32 +450,29 @@ impl ZcashDeserialize for Transaction {
|
|||
|
||||
let joinsplit_data = OptV4Jsd::zcash_deserialize(&mut reader)?;
|
||||
|
||||
use futures::future::Either::*;
|
||||
// Arbitraily use a spend for `first`, if both are present
|
||||
let sapling_shielded_data = if !shielded_spends.is_empty() {
|
||||
Some(sapling::ShieldedData {
|
||||
value_balance,
|
||||
let sapling_transfers = if !shielded_spends.is_empty() {
|
||||
Some(sapling::TransferData::SpendsAndMaybeOutputs {
|
||||
shared_anchor: FieldNotPresent,
|
||||
first: Left(shielded_spends.remove(0)),
|
||||
rest_spends: shielded_spends,
|
||||
rest_outputs: shielded_outputs,
|
||||
binding_sig: reader.read_64_bytes()?.into(),
|
||||
spends: shielded_spends.try_into().expect("checked for spends"),
|
||||
maybe_outputs: shielded_outputs,
|
||||
})
|
||||
} else if !shielded_outputs.is_empty() {
|
||||
Some(sapling::ShieldedData {
|
||||
value_balance,
|
||||
shared_anchor: FieldNotPresent,
|
||||
first: Right(shielded_outputs.remove(0)),
|
||||
// the spends are actually empty here, but we use the
|
||||
// vec for consistency and readability
|
||||
rest_spends: shielded_spends,
|
||||
rest_outputs: shielded_outputs,
|
||||
binding_sig: reader.read_64_bytes()?.into(),
|
||||
Some(sapling::TransferData::JustOutputs {
|
||||
outputs: shielded_outputs.try_into().expect("checked for outputs"),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let sapling_shielded_data = match sapling_transfers {
|
||||
Some(transfers) => Some(sapling::ShieldedData {
|
||||
value_balance,
|
||||
transfers,
|
||||
binding_sig: reader.read_64_bytes()?.into(),
|
||||
}),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Transaction::V4 {
|
||||
inputs,
|
||||
outputs,
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use super::super::*;
|
||||
|
||||
use crate::{
|
||||
block::Block,
|
||||
block::{Block, MAX_BLOCK_BYTES},
|
||||
sapling::{PerSpendAnchor, SharedAnchor},
|
||||
serialization::{WriteZcashExt, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
|
||||
};
|
||||
|
||||
use block::MAX_BLOCK_BYTES;
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[test]
|
||||
|
@ -372,8 +372,8 @@ fn transaction_to_fake_v5(trans: &Transaction) -> Transaction {
|
|||
fn sapling_shielded_v4_to_fake_v5(
|
||||
v4_shielded: sapling::ShieldedData<PerSpendAnchor>,
|
||||
) -> Option<sapling::ShieldedData<SharedAnchor>> {
|
||||
use futures::future::Either::*;
|
||||
use sapling::ShieldedData;
|
||||
use sapling::TransferData::*;
|
||||
|
||||
let unique_anchors: Vec<_> = v4_shielded
|
||||
.spends()
|
||||
|
@ -381,30 +381,32 @@ fn sapling_shielded_v4_to_fake_v5(
|
|||
.unique()
|
||||
.collect();
|
||||
|
||||
let shared_anchor = match unique_anchors.as_slice() {
|
||||
[unique_anchor] => *unique_anchor,
|
||||
// TODO: remove shared anchor when there are no spends
|
||||
[] => Default::default(),
|
||||
// Multiple different anchors, can't convert to v5
|
||||
_ => return None,
|
||||
};
|
||||
let fake_spends: Vec<_> = v4_shielded
|
||||
.spends()
|
||||
.cloned()
|
||||
.map(sapling_spend_v4_to_fake_v5)
|
||||
.collect();
|
||||
|
||||
let first = match v4_shielded.first {
|
||||
Left(spend) => Left(sapling_spend_v4_to_fake_v5(spend)),
|
||||
Right(output) => Right(output),
|
||||
let transfers = match v4_shielded.transfers {
|
||||
SpendsAndMaybeOutputs { maybe_outputs, .. } => {
|
||||
let shared_anchor = match unique_anchors.as_slice() {
|
||||
[unique_anchor] => *unique_anchor,
|
||||
// Multiple different anchors, can't convert to v5
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
SpendsAndMaybeOutputs {
|
||||
shared_anchor,
|
||||
spends: fake_spends.try_into().unwrap(),
|
||||
maybe_outputs,
|
||||
}
|
||||
}
|
||||
JustOutputs { outputs } => JustOutputs { outputs },
|
||||
};
|
||||
|
||||
let fake_shielded_v5 = ShieldedData::<SharedAnchor> {
|
||||
value_balance: v4_shielded.value_balance,
|
||||
shared_anchor,
|
||||
first,
|
||||
rest_spends: v4_shielded
|
||||
.rest_spends
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(sapling_spend_v4_to_fake_v5)
|
||||
.collect(),
|
||||
rest_outputs: v4_shielded.rest_outputs,
|
||||
transfers,
|
||||
binding_sig: v4_shielded.binding_sig,
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue