Add support for Orchard ZSA and updated generic structures for Tx V6 (without unit tests fixing for now).

- Refactored `ShieldedData` and `Action` structures to be generics parameterized by Orchard flavor
  (`OrchardVanilla` or `OrchardZSA`), enabling support for both Orchard protocols in Tx V6.
- Introduced a `burn` field in `ShieldedData` to support ZSA, with unit type for Tx V5 and a vector of burn items for Tx V6.
- Modified `Transaction` enum methods (orchard_...) to handle generics properly, ensuring compatibility with both Orchard flavors.
- Implemented serialization and deserialization for Tx V6 while avoiding code redundancy with Tx V5 wherever possible.
This commit is contained in:
Dmitry Demin 2024-09-16 10:00:59 +02:00
parent 8e53d246f9
commit 23e318c0b0
9 changed files with 303 additions and 150 deletions

View File

@ -6000,6 +6000,7 @@ dependencies = [
"itertools 0.13.0",
"jubjub",
"lazy_static",
"nonempty",
"num-integer",
"orchard",
"primitive-types",

View File

@ -15,7 +15,8 @@ keywords = ["zebra", "zcash"]
categories = ["asynchronous", "cryptography::cryptocurrencies", "encoding"]
[features]
default = []
#default = []
default = ["tx-v6"]
# Production features that activate extra functionality
@ -60,6 +61,11 @@ proptest-impl = [
bench = ["zebra-test"]
# Support for transaction version 6
tx-v6 = [
"nonempty"
]
[dependencies]
# Cryptography
@ -102,6 +108,9 @@ sapling-crypto.workspace = true
zcash_protocol.workspace = true
zcash_address.workspace = true
# Used for orchard serialization
nonempty = { version = "0.7", optional = true }
# Time
chrono = { version = "0.4.38", default-features = false, features = ["clock", "std", "serde"] }
humantime = "2.1.0"

View File

@ -40,6 +40,9 @@ pub mod transparent;
pub mod value_balance;
pub mod work;
#[cfg(feature = "tx-v6")]
pub mod orchard_zsa;
#[cfg(any(test, feature = "proptest-impl"))]
pub use block::LedgerState;

View File

@ -6,6 +6,7 @@ mod action;
mod address;
mod commitment;
mod note;
mod orchard_flavor_ext;
mod sinsemilla;
#[cfg(any(test, feature = "proptest-impl"))]
@ -23,3 +24,7 @@ pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment};
pub use keys::Diversifier;
pub use note::{EncryptedNote, Note, Nullifier, WrappedNoteKey};
pub use shielded_data::{AuthorizedAction, Flags, ShieldedData};
pub(crate) use crate::orchard_zsa::issuance::IssueData;
pub(crate) use orchard_flavor_ext::{OrchardFlavorExt, OrchardVanilla, OrchardZSA};
pub(crate) use shielded_data::ActionCommon;

View File

@ -11,6 +11,7 @@ use super::{
commitment::{self, ValueCommitment},
keys,
note::{self, Nullifier},
OrchardFlavorExt,
};
/// An Action description, as described in the [Zcash specification §7.3][actiondesc].
@ -21,7 +22,7 @@ use super::{
///
/// [actiondesc]: https://zips.z.cash/protocol/nu5.pdf#actiondesc
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Action {
pub struct Action<V: OrchardFlavorExt> {
/// A value commitment to net value of the input note minus the output note
pub cv: commitment::ValueCommitment,
/// The nullifier of the input note being spent.
@ -35,14 +36,14 @@ pub struct Action {
/// encrypted private key in `out_ciphertext`.
pub ephemeral_key: keys::EphemeralPublicKey,
/// A ciphertext component for the encrypted output note.
pub enc_ciphertext: note::EncryptedNote,
pub enc_ciphertext: V::EncryptedNote,
/// A ciphertext component that allows the holder of a full viewing key to
/// recover the recipient diversified transmission key and the ephemeral
/// private key (and therefore the entire note plaintext).
pub out_ciphertext: note::WrappedNoteKey,
}
impl ZcashSerialize for Action {
impl<V: OrchardFlavorExt> ZcashSerialize for Action<V> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
self.cv.zcash_serialize(&mut writer)?;
writer.write_all(&<[u8; 32]>::from(self.nullifier)[..])?;
@ -55,7 +56,7 @@ impl ZcashSerialize for Action {
}
}
impl ZcashDeserialize for Action {
impl<V: OrchardFlavorExt> ZcashDeserialize for Action<V> {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
// # Consensus
//
@ -93,7 +94,7 @@ impl ZcashDeserialize for Action {
// https://zips.z.cash/protocol/protocol.pdf#concretesym but fixed to
// 580 bytes in https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus
// See [`note::EncryptedNote::zcash_deserialize`].
enc_ciphertext: note::EncryptedNote::zcash_deserialize(&mut reader)?,
enc_ciphertext: V::EncryptedNote::zcash_deserialize(&mut reader)?,
// Type is `Sym.C`, i.e. `𝔹^Y^{\[N\]}`, i.e. arbitrary-sized byte arrays
// https://zips.z.cash/protocol/protocol.pdf#concretesym but fixed to
// 80 bytes in https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus

View File

@ -1,7 +1,5 @@
//! Encrypted parts of Orchard notes.
// FIXME: make it a generic and add support for OrchardZSA (encrypted tote size ofr it is not 580!)
use std::{fmt, io};
use serde_big_array::BigArray;
@ -12,20 +10,20 @@ use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}
///
/// Corresponds to the Orchard 'encCiphertext's
#[derive(Deserialize, Serialize)]
pub struct EncryptedNote(#[serde(with = "BigArray")] pub(crate) [u8; 580]);
pub struct EncryptedNote<const N: usize>(#[serde(with = "BigArray")] pub(crate) [u8; N]);
// These impls all only exist because of array length restrictions.
// TODO: use const generics https://github.com/ZcashFoundation/zebra/issues/2042
impl Copy for EncryptedNote {}
impl<const N: usize> Copy for EncryptedNote<N> {}
impl Clone for EncryptedNote {
impl<const N: usize> Clone for EncryptedNote<N> {
fn clone(&self) -> Self {
*self
}
}
impl fmt::Debug for EncryptedNote {
impl<const N: usize> fmt::Debug for EncryptedNote<N> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("EncryptedNote")
.field(&hex::encode(&self.0[..]))
@ -33,36 +31,36 @@ impl fmt::Debug for EncryptedNote {
}
}
impl Eq for EncryptedNote {}
impl<const N: usize> Eq for EncryptedNote<N> {}
impl From<[u8; 580]> for EncryptedNote {
fn from(bytes: [u8; 580]) -> Self {
impl<const N: usize> From<[u8; N]> for EncryptedNote<N> {
fn from(bytes: [u8; N]) -> Self {
EncryptedNote(bytes)
}
}
impl From<EncryptedNote> for [u8; 580] {
fn from(enc_ciphertext: EncryptedNote) -> Self {
impl<const N: usize> From<EncryptedNote<N>> for [u8; N] {
fn from(enc_ciphertext: EncryptedNote<N>) -> Self {
enc_ciphertext.0
}
}
impl PartialEq for EncryptedNote {
impl<const N: usize> PartialEq for EncryptedNote<N> {
fn eq(&self, other: &Self) -> bool {
self.0[..] == other.0[..]
}
}
impl ZcashSerialize for EncryptedNote {
impl<const N: usize> ZcashSerialize for EncryptedNote<N> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&self.0[..])?;
Ok(())
}
}
impl ZcashDeserialize for EncryptedNote {
impl<const N: usize> ZcashDeserialize for EncryptedNote<N> {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
let mut bytes = [0; 580];
let mut bytes = [0; N];
reader.read_exact(&mut bytes[..])?;
Ok(Self(bytes))
}
@ -133,7 +131,7 @@ use proptest::prelude::*;
proptest! {
#[test]
fn encrypted_ciphertext_roundtrip(ec in any::<EncryptedNote>()) {
fn encrypted_ciphertext_roundtrip(ec in any::<EncryptedNote::<{ crate::orchard::ENCRYPTED_NOTE_SIZE_V5 }>>()) {
let _init_guard = zebra_test::init();
let mut data = Vec::new();

View File

@ -20,9 +20,17 @@ use crate::{
},
};
use super::OrchardFlavorExt;
#[cfg(not(feature = "tx-v6"))]
use super::OrchardVanilla;
#[cfg(feature = "tx-v6")]
use super::OrchardZSA;
/// A bundle of [`Action`] descriptions and signature data.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct ShieldedData {
pub struct ShieldedData<V: OrchardFlavorExt> {
/// The orchard flags for this transaction.
/// Denoted as `flagsOrchard` in the spec.
pub flags: Flags,
@ -37,13 +45,18 @@ pub struct ShieldedData {
pub proof: Halo2Proof,
/// The Orchard Actions, in the order they appear in the transaction.
/// Denoted as `vActionsOrchard` and `vSpendAuthSigsOrchard` in the spec.
pub actions: AtLeastOne<AuthorizedAction>,
pub actions: AtLeastOne<AuthorizedAction<V>>,
/// A signature on the transaction `sighash`.
/// Denoted as `bindingSigOrchard` in the spec.
pub binding_sig: Signature<Binding>,
#[cfg(feature = "tx-v6")]
/// Assets intended for burning
/// Denoted as `vAssetBurn` in the spec (ZIP 230).
pub burn: V::BurnType,
}
impl fmt::Display for ShieldedData {
impl<V: OrchardFlavorExt> fmt::Display for ShieldedData<V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut fmter = f.debug_struct("orchard::ShieldedData");
@ -59,13 +72,18 @@ impl fmt::Display for ShieldedData {
}
}
impl ShieldedData {
impl<V: OrchardFlavorExt> ShieldedData<V> {
/// Iterate over the [`Action`]s for the [`AuthorizedAction`]s in this
/// transaction, in the order they appear in it.
pub fn actions(&self) -> impl Iterator<Item = &Action> {
pub fn actions(&self) -> impl Iterator<Item = &Action<V>> {
self.actions.actions()
}
// FIXME: add a doc comment
pub fn action_commons(&self) -> impl Iterator<Item = ActionCommon> + '_ {
self.actions.actions().map(|action| action.into())
}
/// Collect the [`Nullifier`]s for this transaction.
pub fn nullifiers(&self) -> impl Iterator<Item = &Nullifier> {
self.actions().map(|action| &action.nullifier)
@ -119,9 +137,9 @@ impl ShieldedData {
}
}
impl AtLeastOne<AuthorizedAction> {
impl<V: OrchardFlavorExt> AtLeastOne<AuthorizedAction<V>> {
/// Iterate over the [`Action`]s of each [`AuthorizedAction`].
pub fn actions(&self) -> impl Iterator<Item = &Action> {
pub fn actions(&self) -> impl Iterator<Item = &Action<V>> {
self.iter()
.map(|authorized_action| &authorized_action.action)
}
@ -131,23 +149,64 @@ impl AtLeastOne<AuthorizedAction> {
///
/// Every authorized Orchard `Action` must have a corresponding `SpendAuth` signature.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct AuthorizedAction {
pub struct AuthorizedAction<V: OrchardFlavorExt> {
/// The action description of this Action.
pub action: Action,
pub action: Action<V>,
/// The spend signature.
pub spend_auth_sig: Signature<SpendAuth>,
}
impl AuthorizedAction {
impl<V: OrchardFlavorExt> AuthorizedAction<V> {
/// The size of a single Action
///
/// Actions are 5 * 32 + ENCRYPTED_NOTE_SIZE + 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 + (V::ENCRYPTED_NOTE_SIZE as u64) + 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 = Self::ACTION_SIZE + Self::SPEND_AUTH_SIG_SIZE;
/// The maximum number of actions in the transaction.
// 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
pub const ACTION_MAX_ALLOCATION: u64 = (MAX_BLOCK_BYTES - 1) / Self::AUTHORIZED_ACTION_SIZE;
// To be but we ensure ACTION_MAX_ALLOCATION is less than 2^16 on compile time
// (this is a workaround, as static_assertions::const_assert! doesn't work for generics,
// see TrustedPreallocate for Action<V>)
const _ACTION_MAX_ALLOCATION_OK: u64 = (1 << 16) - Self::ACTION_MAX_ALLOCATION;
/* FIXME: remove this
const ACTION_MAX_ALLOCATION_OK: () = assert!(
Self::ACTION_MAX_ALLOCATION < 1, //(1 << 16),
"must be less than 2^16"
);
*/
/// Split out the action and the signature for V5 transaction
/// serialization.
pub fn into_parts(self) -> (Action, Signature<SpendAuth>) {
pub fn into_parts(self) -> (Action<V>, 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 {
pub fn from_parts(
action: Action<V>,
spend_auth_sig: Signature<SpendAuth>,
) -> AuthorizedAction<V> {
AuthorizedAction {
action,
spend_auth_sig,
@ -155,38 +214,48 @@ impl AuthorizedAction {
}
}
/// 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;
// TODO: FIXME: Consider moving it to transaction.rs as it's not used here. Or move its usage here from transaction.rs.
/// A struct that contains values of several fields of an `Action` struct.
/// Those fields are used in other parts of the code that call the `orchard_actions()` method of the `Transaction`.
/// The goal of using `ActionCommon` is that it's not a generic, unlike `Action`, so it can be returned from Transaction methods
/// (the fields of `ActionCommon` do not depend on the generic parameter `Version` of `Action`).
pub struct ActionCommon {
/// A reference to the value commitment to the net value of the input note minus the output note.
pub cv: super::commitment::ValueCommitment,
/// A reference to the nullifier of the input note being spent.
pub nullifier: super::note::Nullifier,
/// A reference to the randomized validating key for `spendAuthSig`.
pub rk: reddsa::VerificationKeyBytes<SpendAuth>,
/// A reference to the x-coordinate of the note commitment for the output note.
pub cm_x: pallas::Base,
}
/// 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;
impl<V: OrchardFlavorExt> From<&Action<V>> for ActionCommon {
fn from(action: &Action<V>) -> Self {
Self {
cv: action.cv,
nullifier: action.nullifier,
rk: action.rk,
cm_x: action.cm_x,
}
}
}
/// 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;
/*
struct AssertBlockSizeLimit<const N: u64>;
impl<const N: u64> AssertBlockSizeLimit<N> {
const OK: () = assert!(N < (1 << 16), "must be less than 2^16");
}
*/
/// 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 {
impl<V: OrchardFlavorExt> TrustedPreallocate for Action<V> {
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
const MAX: u64 = (MAX_BLOCK_BYTES - 1) / AUTHORIZED_ACTION_SIZE;
// # Consensus
//
// > [NU5 onward] nSpendsSapling, nOutputsSapling, and nActionsOrchard MUST all be less than 2^16.
@ -196,15 +265,28 @@ impl TrustedPreallocate for Action {
// This acts as nActionsOrchard and is therefore subject to the rule.
// The maximum value is actually smaller due to the block size limit,
// but we ensure the 2^16 limit with a static assertion.
static_assertions::const_assert!(MAX < (1 << 16));
MAX
//
// TODO: FIXME: find a better way to use static check (see https://github.com/nvzqz/static-assertions/issues/40,
// https://users.rust-lang.org/t/how-do-i-static-assert-a-property-of-a-generic-u32-parameter/76307)?
// The following expression doesn't work for generics, so a workaround with _ACTION_MAX_ALLOCATION_OK in
// AuthorizedAction impl is used instead:
// static_assertions::const_assert!(AuthorizedAction::<V>::ACTION_MAX_ALLOCATION < (1 << 16));
AuthorizedAction::<V>::ACTION_MAX_ALLOCATION
}
}
impl TrustedPreallocate for Signature<SpendAuth> {
fn max_allocation() -> u64 {
// Each signature must have a corresponding action.
Action::max_allocation()
#[cfg(not(feature = "tx-v6"))]
let result = Action::<OrchardVanilla>::max_allocation();
// TODO: FIXME: Check this: V6 is used as it provides the max size of the action.
// So it's used even for V5 - is this correct?
#[cfg(feature = "tx-v6")]
let result = Action::<OrchardZSA>::max_allocation();
result
}
}

View File

@ -53,6 +53,56 @@ use crate::{
value_balance::{ValueBalance, ValueBalanceError},
};
// FIXME: doc this
macro_rules! shielded_data_iter {
($self:expr, $mapper:expr) => {
match $self {
// Maybe Orchard shielded data
Transaction::V5 {
orchard_shielded_data,
..
} => Box::new(orchard_shielded_data.into_iter().flat_map($mapper)),
// FIXME: process V6 properly?
Transaction::V6 {
orchard_shielded_data,
..
} => Box::new(orchard_shielded_data.into_iter().flat_map($mapper)),
// No Orchard shielded data
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. } => Box::new(std::iter::empty()),
}
};
}
// FIXME: doc this
macro_rules! shielded_data_field {
($self:expr, $field:ident) => {
match $self {
// Maybe Orchard shielded data
Transaction::V5 {
orchard_shielded_data,
..
} => orchard_shielded_data.as_ref().map(|data| data.$field),
// FIXME: process V6 properly?
Transaction::V6 {
orchard_shielded_data,
..
} => orchard_shielded_data.as_ref().map(|data| data.$field),
// No Orchard shielded data
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. } => None,
}
};
}
/// A Zcash transaction.
///
/// A transaction is an encoded data structure that facilitates the transfer of
@ -140,7 +190,7 @@ pub enum Transaction {
/// The sapling shielded data for this transaction, if any.
sapling_shielded_data: Option<sapling::ShieldedData<sapling::SharedAnchor>>,
/// The orchard data for this transaction, if any.
orchard_shielded_data: Option<orchard::ShieldedData>,
orchard_shielded_data: Option<orchard::ShieldedData<orchard::OrchardVanilla>>,
},
// FIXME: implement V6 properly (now it's just a coipy of V5)
/// A `version = 6` transaction , which supports Orchard ZSA, Orchard Vanille, Sapling and
@ -161,8 +211,10 @@ pub enum Transaction {
outputs: Vec<transparent::Output>,
/// The sapling shielded data for this transaction, if any.
sapling_shielded_data: Option<sapling::ShieldedData<sapling::SharedAnchor>>,
/// The orchard data for this transaction, if any.
orchard_shielded_data: Option<orchard::ShieldedData>,
/// The ZSA orchard shielded data for this transaction, if any.
orchard_shielded_data: Option<orchard::ShieldedData<orchard::OrchardZSA>>,
/// The ZSA issuance data for this transaction, if any.
orchard_zsa_issue_data: Option<orchard::IssueData>,
},
}
@ -1017,96 +1069,52 @@ impl Transaction {
// orchard
/// Access the [`orchard::ShieldedData`] in this transaction,
/// regardless of version.
pub fn orchard_shielded_data(&self) -> Option<&orchard::ShieldedData> {
match self {
// Maybe Orchard shielded data
Transaction::V5 {
orchard_shielded_data,
..
} => orchard_shielded_data.as_ref(),
// FIXME: Support V6/OrchardZSA propetly.
Transaction::V6 {
orchard_shielded_data,
..
} => orchard_shielded_data.as_ref(),
// No Orchard shielded data
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. } => None,
}
}
/// Modify the [`orchard::ShieldedData`] in this transaction,
/// regardless of version.
#[cfg(any(test, feature = "proptest-impl"))]
pub fn orchard_shielded_data_mut(&mut self) -> Option<&mut orchard::ShieldedData> {
match self {
Transaction::V5 {
orchard_shielded_data: Some(orchard_shielded_data),
..
} => Some(orchard_shielded_data),
// FIXME: Support V6/OrchardZSA propetly.
Transaction::V6 {
orchard_shielded_data: Some(orchard_shielded_data),
..
} => Some(orchard_shielded_data),
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. }
| Transaction::V5 {
orchard_shielded_data: None,
..
}
| Transaction::V6 {
orchard_shielded_data: None,
..
} => None,
}
}
/// Iterate over the [`orchard::Action`]s in this transaction, if there are any,
/// regardless of version.
pub fn orchard_actions(&self) -> impl Iterator<Item = &orchard::Action> {
self.orchard_shielded_data()
.into_iter()
.flat_map(orchard::ShieldedData::actions)
pub fn orchard_actions(&self) -> Box<dyn Iterator<Item = orchard::ActionCommon> + '_> {
shielded_data_iter!(self, orchard::ShieldedData::action_commons)
}
/// Access the [`orchard::Nullifier`]s in this transaction, if there are any,
/// regardless of version.
pub fn orchard_nullifiers(&self) -> impl Iterator<Item = &orchard::Nullifier> {
self.orchard_shielded_data()
.into_iter()
.flat_map(orchard::ShieldedData::nullifiers)
pub fn orchard_nullifiers(&self) -> Box<dyn Iterator<Item = &orchard::Nullifier> + '_> {
shielded_data_iter!(self, orchard::ShieldedData::nullifiers)
}
/// Access the note commitments in this transaction, if there are any,
/// regardless of version.
pub fn orchard_note_commitments(&self) -> impl Iterator<Item = &pallas::Base> {
self.orchard_shielded_data()
.into_iter()
.flat_map(orchard::ShieldedData::note_commitments)
pub fn orchard_note_commitments(&self) -> Box<dyn Iterator<Item = &pallas::Base> + '_> {
shielded_data_iter!(self, orchard::ShieldedData::note_commitments)
}
/// Access the [`orchard::Flags`] in this transaction, if there is any,
/// regardless of version.
pub fn orchard_flags(&self) -> Option<orchard::shielded_data::Flags> {
self.orchard_shielded_data()
.map(|orchard_shielded_data| orchard_shielded_data.flags)
shielded_data_field!(self, flags)
}
/// Return if the transaction has any Orchard shielded data,
/// regardless of version.
pub fn has_orchard_shielded_data(&self) -> bool {
self.orchard_shielded_data().is_some()
match self {
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. } => false,
Transaction::V5 {
orchard_shielded_data,
..
} => orchard_shielded_data.is_some(),
Transaction::V6 {
orchard_shielded_data,
..
} => orchard_shielded_data.is_some(),
}
}
// FIXME: add doc
pub fn orchard_shared_anchor(&self) -> Option<orchard::tree::Root> {
shielded_data_field!(self, shared_anchor)
}
// value balances
@ -1501,10 +1509,8 @@ impl Transaction {
///
/// <https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions>
pub fn orchard_value_balance(&self) -> ValueBalance<NegativeAllowed> {
let orchard_value_balance = self
.orchard_shielded_data()
.map(|shielded_data| shielded_data.value_balance)
.unwrap_or_else(Amount::zero);
let orchard_value_balance =
shielded_data_field!(self, value_balance).unwrap_or_else(Amount::zero);
ValueBalance::from_orchard_amount(orchard_value_balance)
}
@ -1515,8 +1521,30 @@ impl Transaction {
/// See `orchard_value_balance` for details.
#[cfg(any(test, feature = "proptest-impl"))]
pub fn orchard_value_balance_mut(&mut self) -> Option<&mut Amount<NegativeAllowed>> {
self.orchard_shielded_data_mut()
.map(|shielded_data| &mut shielded_data.value_balance)
match self {
Transaction::V5 {
orchard_shielded_data: Some(orchard_shielded_data),
..
} => Some(&mut orchard_shielded_data.value_balance),
Transaction::V6 {
orchard_shielded_data: Some(orchard_shielded_data),
..
} => Some(&mut orchard_shielded_data.value_balance),
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. }
| Transaction::V5 {
orchard_shielded_data: None,
..
}
| Transaction::V6 {
orchard_shielded_data: None,
..
} => None,
}
}
/// Returns the value balances for this transaction using the provided transparent outputs.

View File

@ -11,6 +11,7 @@ use reddsa::{orchard::Binding, orchard::SpendAuth, Signature};
use crate::{
amount,
block::MAX_BLOCK_BYTES,
orchard::OrchardFlavorExt,
parameters::{
OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID,
TX_V6_VERSION_GROUP_ID,
@ -326,7 +327,7 @@ impl ZcashDeserialize for Option<sapling::ShieldedData<sapling::SharedAnchor>> {
}
}
impl ZcashSerialize for Option<orchard::ShieldedData> {
impl<V: OrchardFlavorExt> ZcashSerialize for Option<orchard::ShieldedData<V>> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
match self {
None => {
@ -342,14 +343,15 @@ impl ZcashSerialize for Option<orchard::ShieldedData> {
orchard_shielded_data.zcash_serialize(&mut writer)?;
}
}
Ok(())
}
}
impl ZcashSerialize for orchard::ShieldedData {
impl<V: OrchardFlavorExt> ZcashSerialize for orchard::ShieldedData<V> {
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
let (actions, sigs): (Vec<orchard::Action<V>>, Vec<Signature<SpendAuth>>) = self
.actions
.iter()
.cloned()
@ -377,16 +379,20 @@ impl ZcashSerialize for orchard::ShieldedData {
// Denoted as `bindingSigOrchard` in the spec.
self.binding_sig.zcash_serialize(&mut writer)?;
#[cfg(feature = "tx-v6")]
// Denoted as `vAssetBurn` in the spec (ZIP 230).
self.burn.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> {
impl<V: OrchardFlavorExt> ZcashDeserialize for Option<orchard::ShieldedData<V>> {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
// Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec.
let actions: Vec<orchard::Action> = (&mut reader).zcash_deserialize_into()?;
let actions: Vec<orchard::Action<V>> = (&mut reader).zcash_deserialize_into()?;
// "The fields flagsOrchard, valueBalanceOrchard, anchorOrchard, sizeProofsOrchard,
// proofsOrchard , and bindingSigOrchard are present if and only if nActionsOrchard > 0."
@ -432,7 +438,7 @@ impl ZcashDeserialize for Option<orchard::ShieldedData> {
let binding_sig: Signature<Binding> = (&mut reader).zcash_deserialize_into()?;
// Create the AuthorizedAction from deserialized parts
let authorized_actions: Vec<orchard::AuthorizedAction> = actions
let authorized_actions: Vec<orchard::AuthorizedAction<V>> = actions
.into_iter()
.zip(sigs)
.map(|(action, spend_auth_sig)| {
@ -440,11 +446,17 @@ impl ZcashDeserialize for Option<orchard::ShieldedData> {
})
.collect();
let actions: AtLeastOne<orchard::AuthorizedAction> = authorized_actions.try_into()?;
let actions: AtLeastOne<orchard::AuthorizedAction<V>> = authorized_actions.try_into()?;
Ok(Some(orchard::ShieldedData {
// TODO: FIXME: add a proper comment
#[cfg(feature = "tx-v6")]
let burn = (&mut reader).zcash_deserialize_into()?;
Ok(Some(orchard::ShieldedData::<V> {
flags,
value_balance,
#[cfg(feature = "tx-v6")]
burn,
shared_anchor,
proof,
actions,
@ -677,7 +689,6 @@ impl ZcashSerialize for Transaction {
orchard_shielded_data.zcash_serialize(&mut writer)?;
}
// FIXME: implement a proper serialization for V6
Transaction::V6 {
network_upgrade,
lock_time,
@ -686,9 +697,11 @@ impl ZcashSerialize for Transaction {
outputs,
sapling_shielded_data,
orchard_shielded_data,
orchard_zsa_issue_data,
} => {
// FIXME: fix spec or use another link as the current version of the PDF
// doesn't contain V6 description.
// Transaction V6 spec:
// FIXME: specify a proper ref
// https://zips.z.cash/protocol/protocol.pdf#txnencoding
// Denoted as `nVersionGroupId` in the spec.
@ -723,6 +736,9 @@ impl ZcashSerialize for Transaction {
// `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`,
// `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`.
orchard_shielded_data.zcash_serialize(&mut writer)?;
// TODO: FIXME: add ref to spec
orchard_zsa_issue_data.zcash_serialize(&mut writer)?;
}
}
Ok(())
@ -982,8 +998,9 @@ impl ZcashDeserialize for Transaction {
}
// FIXME: implement a proper deserialization for V6
(6, true) => {
// FIXME: fix spec or use another link as the current version of the PDF
// doesn't contain V6 description.
// Transaction V6 spec:
// FIXME: specify a proper ref
// https://zips.z.cash/protocol/protocol.pdf#txnencoding
// Denoted as `nVersionGroupId` in the spec.
@ -1024,6 +1041,9 @@ impl ZcashDeserialize for Transaction {
// `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`.
let orchard_shielded_data = (&mut limited_reader).zcash_deserialize_into()?;
// TODO: FIXME: add ref to spec
let orchard_zsa_issue_data = (&mut limited_reader).zcash_deserialize_into()?;
Ok(Transaction::V6 {
network_upgrade,
lock_time,
@ -1032,6 +1052,7 @@ impl ZcashDeserialize for Transaction {
outputs,
sapling_shielded_data,
orchard_shielded_data,
orchard_zsa_issue_data,
})
}
(_, _) => Err(SerializationError::Parse("bad tx header")),
@ -1081,6 +1102,11 @@ pub const MIN_TRANSPARENT_TX_V4_SIZE: u64 = MIN_TRANSPARENT_TX_SIZE + 4;
/// v5 transactions also have an expiry height and a consensus branch ID.
pub const MIN_TRANSPARENT_TX_V5_SIZE: u64 = MIN_TRANSPARENT_TX_SIZE + 4 + 4;
/// The minimum transaction size for v6 transactions.
///
/// FIXME: specify a proper value and description.
pub const MIN_TRANSPARENT_TX_V6_SIZE: u64 = MIN_TRANSPARENT_TX_V5_SIZE;
/// No valid Zcash message contains more transactions than can fit in a single block
///
/// `tx` messages contain a single transaction, and `block` messages are limited to the maximum