From ff78e0f556a6ebf08a3d481425f38112259f8a53 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Thu, 3 Apr 2025 10:58:07 +0200 Subject: [PATCH 1/7] Modifiy zebra-chain structures to support multiple action groups --- zebra-chain/src/orchard.rs | 2 +- zebra-chain/src/orchard/shielded_data.rs | 98 ++++++++++++++++--- .../src/orchard/shielded_data_flavor.rs | 2 + zebra-chain/src/transaction.rs | 28 +++--- zebra-chain/src/transaction/arbitrary.rs | 35 ++++--- zebra-chain/src/transaction/serialize.rs | 70 ++++++++----- zebra-consensus/src/primitives/halo2.rs | 14 +-- zebra-consensus/src/transaction.rs | 15 +-- zebra-consensus/src/transaction/check.rs | 3 +- zebra-state/src/service/check/anchors.rs | 6 +- .../src/service/check/tests/nullifier.rs | 8 +- .../finalized_state/zebra_db/arbitrary.rs | 2 +- .../components/mempool/storage/tests/prop.rs | 13 ++- 13 files changed, 206 insertions(+), 90 deletions(-) diff --git a/zebra-chain/src/orchard.rs b/zebra-chain/src/orchard.rs index 290ddb4931a..2a2e5ac7c37 100644 --- a/zebra-chain/src/orchard.rs +++ b/zebra-chain/src/orchard.rs @@ -23,7 +23,7 @@ pub use address::Address; 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 use shielded_data::{ActionGroup, AuthorizedAction, Flags, ShieldedData}; pub use shielded_data_flavor::{OrchardVanilla, ShieldedDataFlavor}; pub(crate) use shielded_data::ActionCommon; diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index 292ebe34266..68c29fc97d0 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -22,19 +22,18 @@ use crate::{ use super::{OrchardVanilla, ShieldedDataFlavor}; -/// A bundle of [`Action`] descriptions and signature data. +// FIXME: wrap all ActionGroup usages withj tx-v6 feature flag? +/// FIXME: add doc +#[cfg(feature = "tx-v6")] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(bound( serialize = "FL::EncryptedNote: serde::Serialize, FL::BurnType: serde::Serialize", deserialize = "FL::BurnType: serde::Deserialize<'de>" ))] -pub struct ShieldedData { +pub struct ActionGroup { /// The orchard flags for this transaction. /// Denoted as `flagsOrchard` in the spec. pub flags: Flags, - /// The net value of Orchard spends minus outputs. - /// Denoted as `valueBalanceOrchard` in the spec. - pub value_balance: Amount, /// The shared anchor for all `Spend`s in this transaction. /// Denoted as `anchorOrchard` in the spec. pub shared_anchor: tree::Root, @@ -44,28 +43,66 @@ pub struct ShieldedData { /// The Orchard Actions, in the order they appear in the transaction. /// Denoted as `vActionsOrchard` and `vSpendAuthSigsOrchard` in the spec. pub actions: AtLeastOne>, - /// A signature on the transaction `sighash`. - /// Denoted as `bindingSigOrchard` in the spec. - pub binding_sig: Signature, #[cfg(feature = "tx-v6")] /// Assets intended for burning /// Denoted as `vAssetBurn` in the spec (ZIP 230). - pub burn: FL::BurnType, + pub(crate) burn: FL::BurnType, } -impl fmt::Display for ShieldedData { +impl ActionGroup { + /// Iterate over the [`Action`]s for the [`AuthorizedAction`]s in this + /// action group, in the order they appear in it. + pub fn actions(&self) -> impl Iterator> { + self.actions.actions() + } +} + +/// A bundle of [`Action`] descriptions and signature data. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(bound( + serialize = "FL::EncryptedNote: serde::Serialize, FL::BurnType: serde::Serialize", + deserialize = "FL::BurnType: serde::Deserialize<'de>" +))] +pub struct ShieldedData { + /// FIXME: add doc + pub action_groups: AtLeastOne>, + /// Denoted as `valueBalanceOrchard` in the spec. + pub value_balance: Amount, + /// A signature on the transaction `sighash`. + /// Denoted as `bindingSigOrchard` in the spec. + pub binding_sig: Signature, +} + +impl fmt::Display for ActionGroup { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut fmter = f.debug_struct("orchard::ShieldedData"); + let mut fmter = f.debug_struct("orchard::ActionGroup"); + + // FIXME: reorder fields here according the struct/spec? fmter.field("actions", &self.actions.len()); - fmter.field("value_balance", &self.value_balance); fmter.field("flags", &self.flags); fmter.field("proof_len", &self.proof.zcash_serialized_size()); fmter.field("shared_anchor", &self.shared_anchor); + #[cfg(feature = "tx-v6")] + fmter.field("burn", &self.burn.as_ref().len()); + + fmter.finish() + } +} + +impl fmt::Display for ShieldedData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut fmter = f.debug_struct("orchard::ShieldedData"); + + fmter.field("action_groups", &self.action_groups); + fmter.field("value_balance", &self.value_balance); + + // FIXME: format binding_sig as well? + fmter.finish() } } @@ -74,13 +111,14 @@ impl ShieldedData { /// 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> { - self.actions.actions() + self.authorized_actions() + .map(|authorized_action| &authorized_action.action) } /// Return an iterator for the [`ActionCommon`] copy of the Actions in this /// transaction, in the order they appear in it. pub fn action_commons(&self) -> impl Iterator + '_ { - self.actions.actions().map(|action| action.into()) + self.actions().map(|action| action.into()) } /// Collect the [`Nullifier`]s for this transaction. @@ -123,7 +161,14 @@ impl ShieldedData { // FIXME: use asset to create ValueCommitment here for burns and above for value_balance? #[cfg(feature = "tx-v6")] - let key_bytes: [u8; 32] = (cv - cv_balance - self.burn.clone().into()).into(); + let key_bytes: [u8; 32] = (cv + - cv_balance + - self + .action_groups + .iter() + .map(|action_group| action_group.burn.clone().into()) + .sum::()) + .into(); key_bytes.into() } @@ -140,6 +185,29 @@ impl ShieldedData { pub fn note_commitments(&self) -> impl Iterator { self.actions().map(|action| &action.cm_x) } + + /// Makes a union of the flags for this transaction. + pub fn flags_union(&self) -> Flags { + self.action_groups + .iter() + .map(|action_group| &action_group.flags) + .fold(Flags::empty(), |result, flags| result.union(*flags)) + } + + /// Collect the shared anchors for this transaction. + pub fn shared_anchors(&self) -> impl Iterator + '_ { + self.action_groups + .iter() + .map(|action_group| action_group.shared_anchor.clone()) + } + + /// Iterate over the [`AuthorizedAction`]s in this + /// transaction, in the order they appear in it. + pub fn authorized_actions(&self) -> impl Iterator> { + self.action_groups + .iter() + .flat_map(|action_group| action_group.actions.iter()) + } } impl AtLeastOne> { diff --git a/zebra-chain/src/orchard/shielded_data_flavor.rs b/zebra-chain/src/orchard/shielded_data_flavor.rs index ec3bc24cb58..02bb45fadfc 100644 --- a/zebra-chain/src/orchard/shielded_data_flavor.rs +++ b/zebra-chain/src/orchard/shielded_data_flavor.rs @@ -57,6 +57,8 @@ pub trait ShieldedDataFlavor: OrchardFlavor { type BurnType: Clone + Debug + Default + + PartialEq + + Eq + ZcashDeserialize + ZcashSerialize + Into diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 5a96cc530af..0c59a4b0c8c 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -80,8 +80,8 @@ macro_rules! orchard_shielded_data_iter { // FIXME: doc this // Move down -macro_rules! orchard_shielded_data_field { - ($self:expr, $field:ident) => { +macro_rules! orchard_shielded_data_method { + ($self:expr, $method:ident) => { match $self { // No Orchard shielded data Transaction::V1 { .. } @@ -92,13 +92,13 @@ macro_rules! orchard_shielded_data_field { Transaction::V5 { orchard_shielded_data, .. - } => orchard_shielded_data.as_ref().map(|data| data.$field), + } => orchard_shielded_data.as_ref().map(|data| data.$method()), #[cfg(feature = "tx-v6")] Transaction::V6 { orchard_shielded_data, .. - } => orchard_shielded_data.as_ref().map(|data| data.$field), + } => orchard_shielded_data.as_ref().map(|data| data.$method()), } }; } @@ -354,11 +354,12 @@ impl Transaction { /// /// See [`Self::has_transparent_or_shielded_inputs`] for details. pub fn has_shielded_inputs(&self) -> bool { + // FIXME: is it correct to use orchard_flags_union here? self.joinsplit_count() > 0 || self.sapling_spends_per_anchor().count() > 0 || (self.orchard_actions().count() > 0 && self - .orchard_flags() + .orchard_flags_union() .unwrap_or_else(orchard::Flags::empty) .contains(orchard::Flags::ENABLE_SPENDS)) } @@ -372,11 +373,12 @@ impl Transaction { /// /// See [`Self::has_transparent_or_shielded_outputs`] for details. pub fn has_shielded_outputs(&self) -> bool { + // FIXME: is it correct to use orchard_flags_union here? self.joinsplit_count() > 0 || self.sapling_outputs().count() > 0 || (self.orchard_actions().count() > 0 && self - .orchard_flags() + .orchard_flags_union() .unwrap_or_else(orchard::Flags::empty) .contains(orchard::Flags::ENABLE_OUTPUTS)) } @@ -386,7 +388,7 @@ impl Transaction { if self.version() < 5 || self.orchard_actions().count() == 0 { return true; } - self.orchard_flags() + self.orchard_flags_union() .unwrap_or_else(orchard::Flags::empty) .intersects(orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS) } @@ -1070,20 +1072,20 @@ impl Transaction { /// Access the [`orchard::Flags`] in this transaction, if there is any, /// regardless of version. - pub fn orchard_flags(&self) -> Option { - orchard_shielded_data_field!(self, flags) + pub fn orchard_flags_union(&self) -> Option { + orchard_shielded_data_method!(self, flags_union) } /// Access the [`orchard::tree::Root`] in this transaction, if there is any, /// regardless of version. - pub fn orchard_shared_anchor(&self) -> Option { - orchard_shielded_data_field!(self, shared_anchor) + pub fn orchard_shared_anchors(&self) -> Box + '_> { + orchard_shielded_data_iter!(self, orchard::ShieldedData::shared_anchors) } /// Return if the transaction has any Orchard shielded data, /// regardless of version. pub fn has_orchard_shielded_data(&self) -> bool { - self.orchard_flags().is_some() + orchard_shielded_data_method!(self, value_balance).is_some() } // value balances @@ -1458,7 +1460,7 @@ impl Transaction { /// pub fn orchard_value_balance(&self) -> ValueBalance { let orchard_value_balance = - orchard_shielded_data_field!(self, value_balance).unwrap_or_else(Amount::zero); + orchard_shielded_data_method!(self, value_balance).unwrap_or_else(Amount::zero); ValueBalance::from_orchard_amount(orchard_value_balance) } diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index 4394e588c91..1c16ee44d32 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -14,7 +14,7 @@ use crate::{ parameters::{Network, NetworkUpgrade}, primitives::{Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof}, sapling::{self, AnchorVariant, PerSpendAnchor, SharedAnchor}, - serialization::ZcashDeserializeInto, + serialization::{AtLeastOne, ZcashDeserializeInto}, sprout, transparent, value_balance::{ValueBalance, ValueBalanceError}, LedgerState, @@ -811,17 +811,20 @@ impl Arbitrary for orchard::ShieldedD let (flags, value_balance, shared_anchor, proof, actions, binding_sig, burn) = props; + // FIXME: support multiple action groups Self { - flags, + action_groups: AtLeastOne::from_one(orchard::ActionGroup { + flags, + shared_anchor, + proof, + actions: actions + .try_into() + .expect("arbitrary vector size range produces at least one action"), + #[cfg(feature = "tx-v6")] + burn, + }), value_balance, - shared_anchor, - proof, - actions: actions - .try_into() - .expect("arbitrary vector size range produces at least one action"), binding_sig: binding_sig.0, - #[cfg(feature = "tx-v6")] - burn, } }) .boxed() @@ -1157,14 +1160,16 @@ pub fn insert_fake_orchard_shielded_data( // Place the dummy action inside the Orchard shielded data let dummy_shielded_data = orchard::ShieldedData:: { - flags: orchard::Flags::empty(), + action_groups: AtLeastOne::from_one(orchard::ActionGroup { + flags: orchard::Flags::empty(), + shared_anchor: orchard::tree::Root::default(), + proof: Halo2Proof(vec![]), + actions: at_least_one![dummy_authorized_action], + #[cfg(feature = "tx-v6")] + burn: Default::default(), + }), value_balance: Amount::try_from(0).expect("invalid transaction amount"), - shared_anchor: orchard::tree::Root::default(), - proof: Halo2Proof(vec![]), - actions: at_least_one![dummy_authorized_action], binding_sig: Signature::from([0u8; 64]), - #[cfg(feature = "tx-v6")] - burn: Default::default(), }; // Replace the shielded data in the transaction diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index fdecf4e3fef..4034f4eb960 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -11,7 +11,7 @@ use reddsa::{orchard::Binding, orchard::SpendAuth, Signature}; use crate::{ amount, block::MAX_BLOCK_BYTES, - orchard::{OrchardVanilla, OrchardZSA}, + orchard::{ActionGroup, OrchardVanilla, OrchardZSA}, parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID}, primitives::{Halo2Proof, ZkSnarkProof}, serialization::{ @@ -351,11 +351,18 @@ impl ZcashSerialize for Option> { impl ZcashSerialize for orchard::ShieldedData { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + assert!( + self.action_groups.len() == 1, + "only one action group is supported for transaction V5" + ); + + let action_group = self.action_groups.first(); + // Split the AuthorizedAction let (actions, sigs): ( Vec>, Vec>, - ) = self + ) = action_group .actions .iter() .cloned() @@ -366,16 +373,16 @@ impl ZcashSerialize for orchard::ShieldedData { actions.zcash_serialize(&mut writer)?; // Denoted as `flagsOrchard` in the spec. - self.flags.zcash_serialize(&mut writer)?; + action_group.flags.zcash_serialize(&mut writer)?; // Denoted as `valueBalanceOrchard` in the spec. self.value_balance.zcash_serialize(&mut writer)?; // Denoted as `anchorOrchard` in the spec. - self.shared_anchor.zcash_serialize(&mut writer)?; + action_group.shared_anchor.zcash_serialize(&mut writer)?; // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. - self.proof.zcash_serialize(&mut writer)?; + action_group.proof.zcash_serialize(&mut writer)?; // Denoted as `vSpendAuthSigsOrchard` in the spec. zcash_serialize_external_count(&sigs, &mut writer)?; @@ -415,30 +422,39 @@ impl ZcashSerialize for Option> { #[allow(clippy::unwrap_in_result)] impl ZcashSerialize for orchard::ShieldedData { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + // FIXME: support multiple action groups + assert!( + self.action_groups.len() == 1, + "only one action group is supported for transaction V6 for now" + ); + + let action_group = self.action_groups.first(); + // Exactly one action group for NU7 CompactSizeMessage::try_from(1) .expect("1 should convert to CompactSizeMessage") .zcash_serialize(&mut writer)?; // Split the AuthorizedAction - let (actions, sigs): (Vec>, Vec>) = self - .actions - .iter() - .cloned() - .map(orchard::AuthorizedAction::into_parts) - .unzip(); + let (actions, sigs): (Vec>, Vec>) = + action_group + .actions + .iter() + .cloned() + .map(orchard::AuthorizedAction::into_parts) + .unzip(); // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. actions.zcash_serialize(&mut writer)?; // Denoted as `flagsOrchard` in the spec. - self.flags.zcash_serialize(&mut writer)?; + action_group.flags.zcash_serialize(&mut writer)?; // Denoted as `anchorOrchard` in the spec. - self.shared_anchor.zcash_serialize(&mut writer)?; + action_group.shared_anchor.zcash_serialize(&mut writer)?; // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. - self.proof.zcash_serialize(&mut writer)?; + action_group.proof.zcash_serialize(&mut writer)?; // Timelimit must be zero for NU7 writer.write_u32::(0)?; @@ -450,7 +466,7 @@ impl ZcashSerialize for orchard::ShieldedData { self.value_balance.zcash_serialize(&mut writer)?; // Denoted as `vAssetBurn` in the spec (ZIP 230). - self.burn.zcash_serialize(&mut writer)?; + action_group.burn.zcash_serialize(&mut writer)?; // Denoted as `bindingSigOrchard` in the spec. self.binding_sig.zcash_serialize(&mut writer)?; @@ -523,12 +539,14 @@ impl ZcashDeserialize for Option> { authorized_actions.try_into()?; Ok(Some(orchard::ShieldedData:: { - flags, + action_groups: AtLeastOne::from_one(ActionGroup { + flags, + shared_anchor, + proof, + actions, + burn: Default::default(), + }), value_balance, - burn: Default::default(), - shared_anchor, - proof, - actions, binding_sig, })) } @@ -615,12 +633,14 @@ impl ZcashDeserialize for Option> { authorized_actions.try_into()?; Ok(Some(orchard::ShieldedData:: { - flags, + action_groups: AtLeastOne::from_one(ActionGroup { + flags, + shared_anchor, + proof, + actions, + burn, + }), value_balance, - burn, - shared_anchor, - proof, - actions, binding_sig, })) } diff --git a/zebra-consensus/src/primitives/halo2.rs b/zebra-consensus/src/primitives/halo2.rs index ea07b79a253..560a5f28a8b 100644 --- a/zebra-consensus/src/primitives/halo2.rs +++ b/zebra-consensus/src/primitives/halo2.rs @@ -19,7 +19,7 @@ use tower::{util::ServiceFn, Service}; use tower_batch_control::{Batch, BatchControl}; use tower_fallback::Fallback; -use zebra_chain::orchard::{OrchardVanilla, OrchardZSA, ShieldedData, ShieldedDataFlavor}; +use zebra_chain::orchard::{ActionGroup, OrchardVanilla, OrchardZSA, ShieldedDataFlavor}; use crate::BoxError; @@ -136,16 +136,16 @@ impl BatchVerifier { // === END TEMPORARY BATCH HALO2 SUBSTITUTE === -impl From<&ShieldedData> for Item { - fn from(shielded_data: &ShieldedData) -> Item { +impl From<&ActionGroup> for Item { + fn from(action_group: &ActionGroup) -> Item { use orchard::{circuit, note, primitives::redpallas, tree, value}; - let anchor = tree::Anchor::from_bytes(shielded_data.shared_anchor.into()).unwrap(); + let anchor = tree::Anchor::from_bytes(action_group.shared_anchor.into()).unwrap(); - let flags = orchard::bundle::Flags::from_byte(shielded_data.flags.bits()) + let flags = orchard::bundle::Flags::from_byte(action_group.flags.bits()) .expect("type should not have unexpected bits"); - let instances = shielded_data + let instances = action_group .actions() .map(|action| { circuit::Instance::from_parts( @@ -164,7 +164,7 @@ impl From<&ShieldedData> for Item { Item { instances, - proof: orchard::circuit::Proof::new(shielded_data.proof.0.clone()), + proof: orchard::circuit::Proof::new(action_group.proof.0.clone()), } } } diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 8c5a6d69c92..223315e7dcd 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -1136,6 +1136,7 @@ where let mut async_checks = AsyncChecks::new(); if let Some(orchard_shielded_data) = orchard_shielded_data { + // FIXME: update the comment to describe action groups // # Consensus // // > The proof 𝜋 MUST be valid given a primary input (cv, rt^{Orchard}, @@ -1147,13 +1148,15 @@ where // aggregated Halo2 proof per transaction, even with multiple // Actions in one transaction. So we queue it for verification // only once instead of queuing it up for every Action description. - async_checks.push( - V::get_verifier() - .clone() - .oneshot(primitives::halo2::Item::from(orchard_shielded_data)), - ); + for action_group in orchard_shielded_data.action_groups.iter() { + async_checks.push( + V::get_verifier() + .clone() + .oneshot(primitives::halo2::Item::from(action_group)), + ) + } - for authorized_action in orchard_shielded_data.actions.iter().cloned() { + for authorized_action in orchard_shielded_data.authorized_actions().cloned() { let (action, spend_auth_sig) = authorized_action.into_parts(); // # Consensus diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index 133c7e80470..1906c634ec1 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -172,7 +172,8 @@ pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), Tr return Err(TransactionError::CoinbaseHasSpend); } - if let Some(orchard_flags) = tx.orchard_flags() { + // FIXME: is it correct to use orchard_flags_union here? + if let Some(orchard_flags) = tx.orchard_flags_union() { if orchard_flags.contains(Flags::ENABLE_SPENDS) { return Err(TransactionError::CoinbaseHasEnableSpendsOrchard); } diff --git a/zebra-state/src/service/check/anchors.rs b/zebra-state/src/service/check/anchors.rs index b4a53c8f176..488d9361a76 100644 --- a/zebra-state/src/service/check/anchors.rs +++ b/zebra-state/src/service/check/anchors.rs @@ -88,9 +88,12 @@ fn sapling_orchard_anchors_refer_to_final_treestates( // > earlier block’s final Orchard treestate. // // - if let Some(shared_anchor) = transaction.orchard_shared_anchor() { + for (shared_anchor_index_in_tx, shared_anchor) in + transaction.orchard_shared_anchors().enumerate() + { tracing::debug!( ?shared_anchor, + ?shared_anchor_index_in_tx, ?tx_index_in_block, ?height, "observed orchard anchor", @@ -111,6 +114,7 @@ fn sapling_orchard_anchors_refer_to_final_treestates( tracing::debug!( ?shared_anchor, + ?shared_anchor_index_in_tx, ?tx_index_in_block, ?height, "validated orchard anchor", diff --git a/zebra-state/src/service/check/tests/nullifier.rs b/zebra-state/src/service/check/tests/nullifier.rs index 8a8b17e4fd0..a6c15832d9b 100644 --- a/zebra-state/src/service/check/tests/nullifier.rs +++ b/zebra-state/src/service/check/tests/nullifier.rs @@ -1134,9 +1134,11 @@ fn transaction_v5_with_orchard_shielded_data( if let Some(ref mut orchard_shielded_data) = orchard_shielded_data { // make sure there are no other nullifiers, by replacing all the authorized_actions - orchard_shielded_data.actions = authorized_actions.try_into().expect( - "unexpected invalid orchard::ShieldedData: must have at least one AuthorizedAction", - ); + // FIXME: works for V5 or V6 with a single action group only + orchard_shielded_data.action_groups.first_mut().actions = + authorized_actions.try_into().expect( + "unexpected invalid orchard::ShieldedData: must have at least one AuthorizedAction", + ); // set value balance to 0 to pass the chain value pool checks let zero_amount = 0.try_into().expect("unexpected invalid zero amount"); diff --git a/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs b/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs index 39bf082d43d..25b55e8e57f 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs @@ -66,7 +66,7 @@ impl ZebraDb { } // Orchard - if let Some(shared_anchor) = transaction.orchard_shared_anchor() { + for shared_anchor in transaction.orchard_shared_anchors() { batch.zs_insert(&orchard_anchors, shared_anchor, ()); } } diff --git a/zebrad/src/components/mempool/storage/tests/prop.rs b/zebrad/src/components/mempool/storage/tests/prop.rs index 3b7fd40467c..b4ee6029c3f 100644 --- a/zebrad/src/components/mempool/storage/tests/prop.rs +++ b/zebrad/src/components/mempool/storage/tests/prop.rs @@ -747,7 +747,10 @@ impl SpendConflictTestInput { conflicts: &HashSet, ) { if let Some(shielded_data) = maybe_shielded_data.take() { + // FIXME: works for V5 or V6 with a single action group only let updated_actions: Vec<_> = shielded_data + .action_groups + .first() .actions .into_vec() .into_iter() @@ -955,8 +958,14 @@ impl OrchardSpendConflict { orchard_shielded_data: &mut Option>, ) { if let Some(shielded_data) = orchard_shielded_data.as_mut() { - shielded_data.actions.first_mut().action.nullifier = - self.new_shielded_data.actions.first().action.nullifier; + // FIXME: works for V5 or V6 with a single action group only + shielded_data + .action_groups + .first_mut() + .actions + .first_mut() + .action + .nullifier = self.new_shielded_data.actions.first().action.nullifier; } else { *orchard_shielded_data = Some(self.new_shielded_data.0); } From 38f50f644cae9a59d9f2a78181b292579850f8b5 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Thu, 3 Apr 2025 12:15:41 +0200 Subject: [PATCH 2/7] Fix zebrad test compilation errors --- .../components/mempool/storage/tests/prop.rs | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/zebrad/src/components/mempool/storage/tests/prop.rs b/zebrad/src/components/mempool/storage/tests/prop.rs index b4ee6029c3f..a1932cd534d 100644 --- a/zebrad/src/components/mempool/storage/tests/prop.rs +++ b/zebrad/src/components/mempool/storage/tests/prop.rs @@ -746,22 +746,18 @@ impl SpendConflictTestInput { maybe_shielded_data: &mut Option>, conflicts: &HashSet, ) { - if let Some(shielded_data) = maybe_shielded_data.take() { - // FIXME: works for V5 or V6 with a single action group only - let updated_actions: Vec<_> = shielded_data - .action_groups - .first() - .actions - .into_vec() - .into_iter() - .filter(|action| !conflicts.contains(&action.action.nullifier)) - .collect(); - - if let Ok(actions) = AtLeastOne::try_from(updated_actions) { - *maybe_shielded_data = Some(orchard::ShieldedData { - actions, - ..shielded_data - }); + if let Some(shielded_data) = maybe_shielded_data { + for action_group in shielded_data.action_groups.iter_mut() { + let updated_actions: Vec<_> = action_group + .actions + .clone() + .into_iter() + .filter(|action| !conflicts.contains(&action.action.nullifier)) + .collect(); + + if let Ok(actions) = AtLeastOne::try_from(updated_actions) { + action_group.actions = actions; + } } } } @@ -965,7 +961,7 @@ impl OrchardSpendConflict { .actions .first_mut() .action - .nullifier = self.new_shielded_data.actions.first().action.nullifier; + .nullifier = self.new_shielded_data.actions().next().action.nullifier; } else { *orchard_shielded_data = Some(self.new_shielded_data.0); } From 221d8bcaeb0ae07b8cd4b6e9b55297241fc1477e Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Thu, 3 Apr 2025 16:30:19 +0200 Subject: [PATCH 3/7] Fix zebra-consensus tests compilation errors --- zebra-chain/src/orchard/shielded_data.rs | 2 +- zebra-consensus/src/primitives/halo2/tests.rs | 84 ++++++++++--------- zebra-consensus/src/transaction/tests.rs | 58 +++++++------ 3 files changed, 80 insertions(+), 64 deletions(-) diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index 68c29fc97d0..462d52fe1a4 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -47,7 +47,7 @@ pub struct ActionGroup { #[cfg(feature = "tx-v6")] /// Assets intended for burning /// Denoted as `vAssetBurn` in the spec (ZIP 230). - pub(crate) burn: FL::BurnType, + pub burn: FL::BurnType, } impl ActionGroup { diff --git a/zebra-consensus/src/primitives/halo2/tests.rs b/zebra-consensus/src/primitives/halo2/tests.rs index 2a379dc96f3..c3e8e5a9202 100644 --- a/zebra-consensus/src/primitives/halo2/tests.rs +++ b/zebra-consensus/src/primitives/halo2/tests.rs @@ -19,7 +19,7 @@ use rand::rngs::OsRng; use zebra_chain::{ orchard::{OrchardVanilla, ShieldedData}, - serialization::{ZcashDeserializeInto, ZcashSerialize}, + serialization::{AtLeastOne, ZcashDeserializeInto, ZcashSerialize}, }; use crate::primitives::halo2::*; @@ -71,37 +71,39 @@ fn generate_test_vectors() { .unwrap(); ShieldedData:: { - flags, + action_groups: AtLeastOne::from_one(ActionGroup { + flags, + shared_anchor: anchor_bytes.try_into().unwrap(), + proof: zebra_chain::primitives::Halo2Proof( + bundle.authorization().proof().as_ref().into(), + ), + actions: bundle + .actions() + .iter() + .map(|a| { + let action = zebra_chain::orchard::Action:: { + cv: a.cv_net().to_bytes().try_into().unwrap(), + nullifier: a.nullifier().to_bytes().try_into().unwrap(), + rk: <[u8; 32]>::from(a.rk()).into(), + cm_x: pallas::Base::from_repr(a.cmx().into()).unwrap(), + ephemeral_key: a.encrypted_note().epk_bytes.try_into().unwrap(), + enc_ciphertext: a.encrypted_note().enc_ciphertext.0.into(), + out_ciphertext: a.encrypted_note().out_ciphertext.into(), + }; + zebra_chain::orchard::shielded_data::AuthorizedAction { + action, + spend_auth_sig: <[u8; 64]>::from(a.authorization()).into(), + } + }) + .collect::>() + .try_into() + .unwrap(), + // FIXME: use a proper value when implementing V6 + #[cfg(feature = "tx-v6")] + burn: Default::default(), + }), value_balance: note_value.try_into().unwrap(), - shared_anchor: anchor_bytes.try_into().unwrap(), - proof: zebra_chain::primitives::Halo2Proof( - bundle.authorization().proof().as_ref().into(), - ), - actions: bundle - .actions() - .iter() - .map(|a| { - let action = zebra_chain::orchard::Action:: { - cv: a.cv_net().to_bytes().try_into().unwrap(), - nullifier: a.nullifier().to_bytes().try_into().unwrap(), - rk: <[u8; 32]>::from(a.rk()).into(), - cm_x: pallas::Base::from_repr(a.cmx().into()).unwrap(), - ephemeral_key: a.encrypted_note().epk_bytes.try_into().unwrap(), - enc_ciphertext: a.encrypted_note().enc_ciphertext.0.into(), - out_ciphertext: a.encrypted_note().out_ciphertext.into(), - }; - zebra_chain::orchard::shielded_data::AuthorizedAction { - action, - spend_auth_sig: <[u8; 64]>::from(a.authorization()).into(), - } - }) - .collect::>() - .try_into() - .unwrap(), binding_sig: <[u8; 64]>::from(bundle.authorization().binding_signature()).into(), - // FIXME: use a proper value when implementing V6 - #[cfg(feature = "tx-v6")] - burn: Default::default(), } }) .collect(); @@ -125,11 +127,13 @@ where let mut async_checks = FuturesUnordered::new(); for sd in shielded_data { - tracing::trace!(?sd); + for ag in sd.action_groups { + tracing::trace!(?ag); - let rsp = verifier.ready().await?.call(Item::from(&sd)); + let rsp = verifier.ready().await?.call(Item::from(&ag)); - async_checks.push(rsp); + async_checks.push(rsp); + } } while let Some(result) = async_checks.next().await { @@ -192,16 +196,18 @@ where let mut async_checks = FuturesUnordered::new(); for sd in shielded_data { - let mut sd = sd.clone(); + for ag in sd.action_groups { + let mut ag = ag.clone(); - sd.flags.remove(zebra_chain::orchard::Flags::ENABLE_SPENDS); - sd.flags.remove(zebra_chain::orchard::Flags::ENABLE_OUTPUTS); + ag.flags.remove(zebra_chain::orchard::Flags::ENABLE_SPENDS); + ag.flags.remove(zebra_chain::orchard::Flags::ENABLE_OUTPUTS); - tracing::trace!(?sd); + tracing::trace!(?ag); - let rsp = verifier.ready().await?.call(Item::from(&sd)); + let rsp = verifier.ready().await?.call(Item::from(&ag)); - async_checks.push(rsp); + async_checks.push(rsp); + } } while let Some(result) = async_checks.next().await { diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 5494537edb8..f3b50ee1428 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -88,7 +88,7 @@ fn fake_v5_transaction_with_orchard_actions_has_inputs_and_outputs() { // If we add ENABLE_SPENDS flag it will pass the inputs check but fails with the outputs // TODO: Avoid new calls to `insert_fake_orchard_shielded_data` for each check #2409. let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = zebra_chain::orchard::Flags::ENABLE_SPENDS; + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_SPENDS; assert_eq!( check::has_inputs_and_outputs(&transaction), @@ -97,7 +97,7 @@ fn fake_v5_transaction_with_orchard_actions_has_inputs_and_outputs() { // If we add ENABLE_OUTPUTS flag it will pass the outputs check but fails with the inputs let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = zebra_chain::orchard::Flags::ENABLE_OUTPUTS; + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_OUTPUTS; assert_eq!( check::has_inputs_and_outputs(&transaction), @@ -106,7 +106,7 @@ fn fake_v5_transaction_with_orchard_actions_has_inputs_and_outputs() { // Finally make it valid by adding both required flags let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_SPENDS | zebra_chain::orchard::Flags::ENABLE_OUTPUTS; assert!(check::has_inputs_and_outputs(&transaction).is_ok()); @@ -141,17 +141,17 @@ fn fake_v5_transaction_with_orchard_actions_has_flags() { // If we add ENABLE_SPENDS flag it will pass. let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = zebra_chain::orchard::Flags::ENABLE_SPENDS; + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_SPENDS; assert!(check::has_enough_orchard_flags(&transaction).is_ok()); // If we add ENABLE_OUTPUTS flag instead, it will pass. let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = zebra_chain::orchard::Flags::ENABLE_OUTPUTS; + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_OUTPUTS; assert!(check::has_enough_orchard_flags(&transaction).is_ok()); // If we add BOTH ENABLE_SPENDS and ENABLE_OUTPUTS flags it will pass. let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_SPENDS | zebra_chain::orchard::Flags::ENABLE_OUTPUTS; assert!(check::has_enough_orchard_flags(&transaction).is_ok()); } @@ -840,7 +840,7 @@ fn v5_coinbase_transaction_with_enable_spends_flag_fails_validation() { let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = zebra_chain::orchard::Flags::ENABLE_SPENDS; + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_SPENDS; assert_eq!( check::coinbase_tx_no_prevout_joinsplit_spend(&transaction), @@ -2418,14 +2418,18 @@ fn v5_with_duplicate_orchard_action() { let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); // Enable spends - shielded_data.flags = zebra_chain::orchard::Flags::ENABLE_SPENDS + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_SPENDS | zebra_chain::orchard::Flags::ENABLE_OUTPUTS; // Duplicate the first action - let duplicate_action = shielded_data.actions.first().clone(); + let duplicate_action = shielded_data.action_groups.first().actions.first().clone(); let duplicate_nullifier = duplicate_action.action.nullifier; - shielded_data.actions.push(duplicate_action); + shielded_data + .action_groups + .first_mut() + .actions + .push(duplicate_action); // Initialize the verifier let state_service = @@ -2867,15 +2871,18 @@ fn coinbase_outputs_are_decryptable_for_fake_v5_blocks() { .expect("At least one fake V5 transaction with no inputs and no outputs"); let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = zebra_chain::orchard::Flags::ENABLE_SPENDS + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_SPENDS | zebra_chain::orchard::Flags::ENABLE_OUTPUTS; - let action = - fill_action_with_note_encryption_test_vector(&shielded_data.actions[0].action, v); - let sig = shielded_data.actions[0].spend_auth_sig; - shielded_data.actions = vec![AuthorizedAction::from_parts(action, sig)] - .try_into() - .unwrap(); + let action = fill_action_with_note_encryption_test_vector( + &shielded_data.action_groups.first().actions[0].action, + v, + ); + let sig = shielded_data.action_groups.first().actions[0].spend_auth_sig; + shielded_data.action_groups.first_mut().actions = + vec![AuthorizedAction::from_parts(action, sig)] + .try_into() + .unwrap(); assert_eq!( check::coinbase_outputs_are_decryptable( @@ -2909,15 +2916,18 @@ fn shielded_outputs_are_not_decryptable_for_fake_v5_blocks() { .expect("At least one fake V5 transaction with no inputs and no outputs"); let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); - shielded_data.flags = zebra_chain::orchard::Flags::ENABLE_SPENDS + shielded_data.action_groups.first_mut().flags = zebra_chain::orchard::Flags::ENABLE_SPENDS | zebra_chain::orchard::Flags::ENABLE_OUTPUTS; - let action = - fill_action_with_note_encryption_test_vector(&shielded_data.actions[0].action, v); - let sig = shielded_data.actions[0].spend_auth_sig; - shielded_data.actions = vec![AuthorizedAction::from_parts(action, sig)] - .try_into() - .unwrap(); + let action = fill_action_with_note_encryption_test_vector( + &shielded_data.action_groups.first().actions[0].action, + v, + ); + let sig = shielded_data.action_groups.first().actions[0].spend_auth_sig; + shielded_data.action_groups.first_mut().actions = + vec![AuthorizedAction::from_parts(action, sig)] + .try_into() + .unwrap(); assert_eq!( check::coinbase_outputs_are_decryptable( From d8cd8199ab7f9f386c5ad21069f47856ae6be776 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Thu, 3 Apr 2025 16:52:17 +0200 Subject: [PATCH 4/7] Fix zebrad tests compilation errors --- zebrad/src/components/mempool/storage/tests/prop.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/zebrad/src/components/mempool/storage/tests/prop.rs b/zebrad/src/components/mempool/storage/tests/prop.rs index a1932cd534d..3914d74001c 100644 --- a/zebrad/src/components/mempool/storage/tests/prop.rs +++ b/zebrad/src/components/mempool/storage/tests/prop.rs @@ -961,7 +961,12 @@ impl OrchardSpendConflict { .actions .first_mut() .action - .nullifier = self.new_shielded_data.actions().next().action.nullifier; + .nullifier = self + .new_shielded_data + .actions() + .next() + .expect("at least one action") + .nullifier; } else { *orchard_shielded_data = Some(self.new_shielded_data.0); } From 518fa234e47353455d41461569f7e594f1a33199 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Thu, 3 Apr 2025 17:33:54 +0200 Subject: [PATCH 5/7] Fix cargo clippy error --- zebra-chain/src/orchard/shielded_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index 462d52fe1a4..b969f33b137 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -198,7 +198,7 @@ impl ShieldedData { pub fn shared_anchors(&self) -> impl Iterator + '_ { self.action_groups .iter() - .map(|action_group| action_group.shared_anchor.clone()) + .map(|action_group| action_group.shared_anchor) } /// Iterate over the [`AuthorizedAction`]s in this From d9305da70591fc029880e9da855c1cb1e2b8d557 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Thu, 3 Apr 2025 18:36:49 +0200 Subject: [PATCH 6/7] Minor fixes in comments --- zebra-chain/src/orchard/shielded_data.rs | 7 ++++--- zebra-chain/src/transaction/serialize.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index b969f33b137..31613763ac2 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -22,8 +22,8 @@ use crate::{ use super::{OrchardVanilla, ShieldedDataFlavor}; -// FIXME: wrap all ActionGroup usages withj tx-v6 feature flag? -/// FIXME: add doc +// FIXME: wrap all ActionGroup usages with tx-v6 feature flag? +/// Action Group description. #[cfg(feature = "tx-v6")] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(bound( @@ -65,7 +65,8 @@ impl ActionGroup { deserialize = "FL::BurnType: serde::Deserialize<'de>" ))] pub struct ShieldedData { - /// FIXME: add doc + /// Action Group descriptions. + /// Denoted as `vActionGroupsOrchard` in the spec (ZIP 230). pub action_groups: AtLeastOne>, /// Denoted as `valueBalanceOrchard` in the spec. pub value_balance: Amount, diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index 8a60d105807..ca958fcb493 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -541,7 +541,7 @@ impl ZcashDeserialize for Option> { fn zcash_deserialize(mut reader: R) -> Result { // FIXME: Implement deserialization of multiple action groups (under a feature flag) - // Denoted as `nActionGroupsOrchard` in the spec (ZIP 230) (must be one for V6/NU7). + // Denoted as `nActionGroupsOrchard` in the spec (ZIP 230) (must be one for V6/NU7). let n_action_groups: usize = (&mut reader) .zcash_deserialize_into::()? .into(); From 5612c75ade0516f8a9c81b46cbf710d2cf6dd372 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Fri, 4 Apr 2025 12:38:40 +0200 Subject: [PATCH 7/7] Add support of serialization of multiple action groups (use new zsa-swap feature flag to enable) --- zebra-chain/Cargo.toml | 4 + zebra-chain/src/transaction/serialize.rs | 197 +++++++++++++---------- zebra-consensus/Cargo.toml | 7 + zebra-state/Cargo.toml | 6 + zebrad/Cargo.toml | 8 + 5 files changed, 138 insertions(+), 84 deletions(-) diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 4d0ff8e272f..fbc5f0e059e 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -17,6 +17,7 @@ categories = ["asynchronous", "cryptography::cryptocurrencies", "encoding"] [features] #default = [] default = ["tx-v6"] +#default = ["tx-v6", "zsa-swap"] # Production features that activate extra functionality @@ -67,6 +68,9 @@ tx-v6 = [ "nonempty" ] +# Support for ZSA asset swaps +zsa-swap = [] + [dependencies] # Cryptography diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index ca958fcb493..96cbbf4d50d 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -356,7 +356,7 @@ impl ZcashSerialize for orchard::ShieldedData { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { assert!( self.action_groups.len() == 1, - "only one action group is supported for transaction V5" + "V6 transaction must contain exactly one action group" ); let action_group = self.action_groups.first(); @@ -402,52 +402,60 @@ impl ZcashSerialize for orchard::ShieldedData { #[allow(clippy::unwrap_in_result)] impl ZcashSerialize for orchard::ShieldedData { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { - // FIXME: Implement serialization of multiple action groups (under a feature flag) - + #[cfg(not(feature = "zsa-swap"))] assert!( self.action_groups.len() == 1, "V6 transaction must contain exactly one action group" ); - let action_group = self.action_groups.first(); + // FIXME: consider using use zcash_serialize_external_count for or Vec::zcash_serialize + for action_group in self.action_groups.iter() { + // Denoted as `nActionGroupsOrchard` in the spec (ZIP 230) (must be one for V6/NU7). + CompactSizeMessage::try_from(self.action_groups.len()) + .expect("nActionGroupsOrchard should convert to CompactSizeMessage") + .zcash_serialize(&mut writer)?; - // Denoted as `nActionGroupsOrchard` in the spec (ZIP 230) (must be one for V6/NU7). - CompactSizeMessage::try_from(self.action_groups.len()) - .expect("nActionGroupsOrchard should convert to CompactSizeMessage") - .zcash_serialize(&mut writer)?; + // Split the AuthorizedAction + let (actions, sigs): (Vec>, Vec>) = + action_group + .actions + .iter() + .cloned() + .map(orchard::AuthorizedAction::into_parts) + .unzip(); - // Split the AuthorizedAction - let (actions, sigs): (Vec>, Vec>) = - action_group - .actions - .iter() - .cloned() - .map(orchard::AuthorizedAction::into_parts) - .unzip(); + // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. + actions.zcash_serialize(&mut writer)?; - // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. - actions.zcash_serialize(&mut writer)?; + // Denoted as `flagsOrchard` in the spec. + action_group.flags.zcash_serialize(&mut writer)?; - // Denoted as `flagsOrchard` in the spec. - action_group.flags.zcash_serialize(&mut writer)?; + // Denoted as `anchorOrchard` in the spec. + action_group.shared_anchor.zcash_serialize(&mut writer)?; - // Denoted as `anchorOrchard` in the spec. - action_group.shared_anchor.zcash_serialize(&mut writer)?; + // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. + action_group.proof.zcash_serialize(&mut writer)?; - // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. - action_group.proof.zcash_serialize(&mut writer)?; + // Denoted as `nAGExpiryHeight` in the spec (ZIP 230) (must be zero for V6/NU7). + writer.write_u32::(0)?; - // Denoted as `nAGExpiryHeight` in the spec (ZIP 230) (must be zero for V6/NU7). - writer.write_u32::(0)?; + // Denoted as `vAssetBurn` in the spec (ZIP 230). + #[cfg(feature = "zsa-swap")] + action_group.burn.zcash_serialize(&mut writer)?; - // Denoted as `vSpendAuthSigsOrchard` in the spec. - zcash_serialize_external_count(&sigs, &mut writer)?; + // Denoted as `vSpendAuthSigsOrchard` in the spec. + zcash_serialize_external_count(&sigs, &mut writer)?; + } // Denoted as `valueBalanceOrchard` in the spec. self.value_balance.zcash_serialize(&mut writer)?; // Denoted as `vAssetBurn` in the spec (ZIP 230). - action_group.burn.zcash_serialize(&mut writer)?; + #[cfg(not(feature = "zsa-swap"))] + self.action_groups + .first() + .burn + .zcash_serialize(&mut writer)?; // Denoted as `bindingSigOrchard` in the spec. self.binding_sig.zcash_serialize(&mut writer)?; @@ -545,82 +553,103 @@ impl ZcashDeserialize for Option> { let n_action_groups: usize = (&mut reader) .zcash_deserialize_into::()? .into(); + if n_action_groups == 0 { return Ok(None); - } else if n_action_groups != 1 { + } + + #[cfg(not(feature = "zsa-swap"))] + if n_action_groups != 1 { return Err(SerializationError::Parse( "V6 transaction must contain exactly one action group", )); } - // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. - let actions: Vec> = (&mut reader).zcash_deserialize_into()?; - - // # Consensus - // - // > Elements of an Action description MUST be canonical encodings of the types given above. - // - // https://zips.z.cash/protocol/protocol.pdf#actiondesc - // - // Some Action elements are validated in this function; they are described below. - - // Denoted as `flagsOrchard` in the spec. - // Consensus: type of each flag is 𝔹, i.e. a bit. This is enforced implicitly - // in [`Flags::zcash_deserialized`]. - let flags: orchard::Flags = (&mut reader).zcash_deserialize_into()?; + let mut action_groups = Vec::with_capacity(n_action_groups); + + // FIXME: use zcash_deserialize_external_count for or Vec::zcash_deserialize for allocation safety + for _ in 0..n_action_groups { + // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. + let actions: Vec> = + (&mut reader).zcash_deserialize_into()?; + + // # Consensus + // + // > Elements of an Action description MUST be canonical encodings of the types given above. + // + // https://zips.z.cash/protocol/protocol.pdf#actiondesc + // + // Some Action elements are validated in this function; they are described below. + + // Denoted as `flagsOrchard` in the spec. + // Consensus: type of each flag is 𝔹, i.e. a bit. This is enforced implicitly + // in [`Flags::zcash_deserialized`]. + let flags: orchard::Flags = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `anchorOrchard` in the spec. + // Consensus: type is `{0 .. 𝑞_ℙ − 1}`. See [`orchard::tree::Root::zcash_deserialize`]. + let shared_anchor: orchard::tree::Root = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. + // Consensus: type is `ZKAction.Proof`, i.e. a byte sequence. + // https://zips.z.cash/protocol/protocol.pdf#halo2encoding + let proof: Halo2Proof = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `nAGExpiryHeight` in the spec (ZIP 230) (must be zero for V6/NU7). + let n_ag_expiry_height = reader.read_u32::()?; + if n_ag_expiry_height != 0 { + return Err(SerializationError::Parse("nAGExpiryHeight for V6/NU7")); + } - // Denoted as `anchorOrchard` in the spec. - // Consensus: type is `{0 .. 𝑞_ℙ − 1}`. See [`orchard::tree::Root::zcash_deserialize`]. - let shared_anchor: orchard::tree::Root = (&mut reader).zcash_deserialize_into()?; + // Denoted as `vAssetBurn` in the spec (ZIP 230). + #[cfg(feature = "zsa-swap")] + let burn = (&mut reader).zcash_deserialize_into()?; + #[cfg(not(feature = "zsa-swap"))] + let burn = Default::default(); + + // Denoted as `vSpendAuthSigsOrchard` in the spec. + // Consensus: this validates the `spendAuthSig` elements, whose type is + // SpendAuthSig^{Orchard}.Signature, i.e. + // B^Y^{[ceiling(ℓ_G/8) + ceiling(bitlength(𝑟_G)/8)]} i.e. 64 bytes + // See [`Signature::zcash_deserialize`]. + let sigs: Vec> = + zcash_deserialize_external_count(actions.len(), &mut reader)?; + + // Create the AuthorizedAction from deserialized parts + let authorized_actions: Vec> = actions + .into_iter() + .zip(sigs) + .map(|(action, spend_auth_sig)| { + orchard::AuthorizedAction::from_parts(action, spend_auth_sig) + }) + .collect(); - // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. - // Consensus: type is `ZKAction.Proof`, i.e. a byte sequence. - // https://zips.z.cash/protocol/protocol.pdf#halo2encoding - let proof: Halo2Proof = (&mut reader).zcash_deserialize_into()?; + let actions: AtLeastOne> = + authorized_actions.try_into()?; - // Denoted as `nAGExpiryHeight` in the spec (ZIP 230) (must be zero for V6/NU7). - let n_ag_expiry_height = reader.read_u32::()?; - if n_ag_expiry_height != 0 { - return Err(SerializationError::Parse("nAGExpiryHeight for V6/NU7")); + action_groups.push(ActionGroup { + flags, + shared_anchor, + proof, + actions, + burn, + }) } - // Denoted as `vSpendAuthSigsOrchard` in the spec. - // Consensus: this validates the `spendAuthSig` elements, whose type is - // SpendAuthSig^{Orchard}.Signature, i.e. - // B^Y^{[ceiling(ℓ_G/8) + ceiling(bitlength(𝑟_G)/8)]} i.e. 64 bytes - // See [`Signature::zcash_deserialize`]. - let sigs: Vec> = - zcash_deserialize_external_count(actions.len(), &mut reader)?; - // Denoted as `valueBalanceOrchard` in the spec. let value_balance: amount::Amount = (&mut reader).zcash_deserialize_into()?; // Denoted as `vAssetBurn` in the spec (ZIP 230). - let burn = (&mut reader).zcash_deserialize_into()?; + #[cfg(not(feature = "zsa-swap"))] + { + action_groups[0].burn = (&mut reader).zcash_deserialize_into()?; + } // Denoted as `bindingSigOrchard` in the spec. let binding_sig: Signature = (&mut reader).zcash_deserialize_into()?; - // Create the AuthorizedAction from deserialized parts - let authorized_actions: Vec> = actions - .into_iter() - .zip(sigs) - .map(|(action, spend_auth_sig)| { - orchard::AuthorizedAction::from_parts(action, spend_auth_sig) - }) - .collect(); - - let actions: AtLeastOne> = - authorized_actions.try_into()?; - Ok(Some(orchard::ShieldedData:: { - action_groups: AtLeastOne::from_one(ActionGroup { - flags, - shared_anchor, - proof, - actions, - burn, - }), + action_groups: action_groups.try_into()?, value_balance, binding_sig, })) diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index 05211f296ac..5268b5657fb 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -17,6 +17,7 @@ categories = ["asynchronous", "cryptography::cryptocurrencies"] [features] #default = [] default = ["tx-v6"] +#default = ["tx-v6", "zsa-swap"] # Production features that activate extra dependencies, or extra features in dependencies @@ -41,6 +42,12 @@ tx-v6 = [ "zebra-chain/tx-v6" ] +# Support for ZSA asset swaps +zsa-swap = [ + "zebra-state/zsa-swap", + "zebra-chain/zsa-swap" +] + [dependencies] blake2b_simd = "1.0.2" bellman = "0.14.0" diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index 45dcf9e96c6..1dff5cd3eb4 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -17,6 +17,7 @@ categories = ["asynchronous", "caching", "cryptography::cryptocurrencies"] [features] #default = [] default = ["tx-v6"] +#default = ["tx-v6", "zsa-swap"] # Production features that activate extra dependencies, or extra features in dependencies @@ -52,6 +53,11 @@ tx-v6 = [ "zebra-chain/tx-v6" ] +# Support for ZSA asset swaps +zsa-swap = [ + "zebra-chain/zsa-swap" +] + [dependencies] bincode = "1.3.3" chrono = { version = "0.4.38", default-features = false, features = ["clock", "std"] } diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index fc1c8d4a889..e260658c87a 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -54,6 +54,7 @@ features = [ # In release builds, don't compile debug logging code, to improve performance. #default = ["release_max_level_info", "progress-bar", "getblocktemplate-rpcs"] default = ["release_max_level_info", "progress-bar", "getblocktemplate-rpcs", "tx-v6"] +#default = ["release_max_level_info", "progress-bar", "getblocktemplate-rpcs", "tx-v6", "zsa-swap"] # Default features for official ZF binary release builds default-release-binaries = ["default", "sentry"] @@ -164,6 +165,13 @@ tx-v6 = [ "zebra-chain/tx-v6" ] +# Support for ZSA asset swaps +zsa-swap = [ + "zebra-consensus/zsa-swap", + "zebra-state/zsa-swap", + "zebra-chain/zsa-swap" +] + [dependencies] zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.41" } zebra-consensus = { path = "../zebra-consensus", version = "1.0.0-beta.41" }