diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index f202955e0b4..b7c6954ab47 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -66,6 +66,9 @@ tx_v6 = [ "nonempty" ] +# Support for ZSA asset swaps +zsa-swap = [] + [dependencies] # Cryptography diff --git a/zebra-chain/src/orchard.rs b/zebra-chain/src/orchard.rs index 4d7bbe0df5a..2d6696cd94f 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}; #[cfg(feature = "tx_v6")] diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index 2acf2c20519..458094cdad1 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -28,7 +28,8 @@ use orchard::{note::AssetBase, value::ValueSum}; use super::{OrchardVanilla, ShieldedDataFlavor}; -/// A bundle of [`Action`] descriptions and signature data. +// FIXME: wrap all ActionGroup usages with tx_v6 feature flag? +/// Action Group description. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[cfg_attr( not(feature = "tx_v6"), @@ -41,40 +42,77 @@ use super::{OrchardVanilla, ShieldedDataFlavor}; deserialize = "Flavor::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, + + /// Block height after which this Bundle's Actions are invalid by consensus. + /// Denoted as `nAGExpiryHeight` in the spec. + #[cfg(feature = "tx_v6")] + pub expiry_height: u32, + + /// Assets intended for burning + /// Denoted as `vAssetBurn` in the spec (ZIP 230). + #[cfg(feature = "tx_v6")] + pub burn: Flavor::BurnType, + /// The aggregated zk-SNARK proof for all the actions in this transaction. /// Denoted as `proofsOrchard` in the spec. 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>, +} + +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)] +#[cfg_attr( + not(feature = "tx_v6"), + serde(bound(serialize = "Flavor::EncryptedNote: serde::Serialize")) +)] +#[cfg_attr( + feature = "tx_v6", + serde(bound( + serialize = "Flavor::EncryptedNote: serde::Serialize, Flavor::BurnType: serde::Serialize", + deserialize = "Flavor::BurnType: serde::Deserialize<'de>" + )) +)] +pub struct ShieldedData { + /// 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, /// 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: Flavor::BurnType, } -impl fmt::Display for ShieldedData { +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"); fmter.field("actions", &self.actions.len()); - fmter.field("value_balance", &self.value_balance); fmter.field("flags", &self.flags); + #[cfg(feature = "tx_v6")] + fmter.field("expiry_height", &self.expiry_height); + + #[cfg(feature = "tx_v6")] + fmter.field("burn", &self.burn.as_ref().len()); + fmter.field("proof_len", &self.proof.zcash_serialized_size()); fmter.field("shared_anchor", &self.shared_anchor); @@ -83,11 +121,25 @@ impl fmt::Display for ShieldedData { } } +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() + } +} + 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) } /// Collect the [`Nullifier`]s for this transaction. @@ -139,7 +191,11 @@ impl ShieldedData { (ValueSum::default() + i64::from(self.value_balance)).unwrap(), AssetBase::native(), ); - let burn_value_commitment = compute_burn_value_commitment(self.burn.as_ref()); + let burn_value_commitment = self + .action_groups + .iter() + .map(|action_group| compute_burn_value_commitment(action_group.burn.as_ref())) + .sum::(); cv - cv_balance - burn_value_commitment }; @@ -160,6 +216,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) + } + + /// 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 11ef89df399..eec474574d0 100644 --- a/zebra-chain/src/orchard/shielded_data_flavor.rs +++ b/zebra-chain/src/orchard/shielded_data_flavor.rs @@ -53,6 +53,8 @@ pub trait ShieldedDataFlavor: OrchardFlavor { #[cfg(feature = "tx_v6")] type BurnType: Clone + Debug + + PartialEq + + Eq + ZcashDeserialize + ZcashSerialize + AsRef<[BurnItem]> diff --git a/zebra-chain/src/primitives/zcash_note_encryption.rs b/zebra-chain/src/primitives/zcash_note_encryption.rs index c030a13aa26..a43e2f411ab 100644 --- a/zebra-chain/src/primitives/zcash_note_encryption.rs +++ b/zebra-chain/src/primitives/zcash_note_encryption.rs @@ -70,8 +70,8 @@ pub fn decrypts_successfully(transaction: &Transaction, network: &Network, heigh } /// Checks if all actions in an Orchard bundle decrypt successfully. -fn orchard_bundle_decrypts_successfully( - bundle: &Bundle, +fn orchard_bundle_decrypts_successfully( + bundle: &Bundle, ) -> bool { bundle.actions().iter().all(|act| { zcash_note_encryption::try_output_recovery_with_ovk( diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 4f8762baeb3..ef814b85714 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -204,32 +204,6 @@ impl fmt::Display for Transaction { } } -// Macro to get a specific field from an Orchard shielded data struct. -// Returns `None` for transaction versions that don't support Orchard (V1-V4). -// This avoids repeating the same match block pattern across multiple accessor methods. -macro_rules! orchard_shielded_data_field { - ($self:expr, $field:ident) => { - match $self { - // No Orchard shielded data - Transaction::V1 { .. } - | Transaction::V2 { .. } - | Transaction::V3 { .. } - | Transaction::V4 { .. } => None, - - Transaction::V5 { - orchard_shielded_data, - .. - } => orchard_shielded_data.as_ref().map(|data| data.$field), - - #[cfg(feature = "tx_v6")] - Transaction::V6 { - orchard_shielded_data, - .. - } => orchard_shielded_data.as_ref().map(|data| data.$field), - } - }; -} - impl Transaction { // identifiers and hashes @@ -319,11 +293,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_action_count() > 0 && self - .orchard_flags() + .orchard_flags_union() .unwrap_or_else(orchard::Flags::empty) .contains(orchard::Flags::ENABLE_SPENDS)) } @@ -332,11 +307,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_action_count() > 0 && self - .orchard_flags() + .orchard_flags_union() .unwrap_or_else(orchard::Flags::empty) .contains(orchard::Flags::ENABLE_OUTPUTS)) } @@ -351,7 +327,7 @@ impl Transaction { if self.version() < 5 || self.orchard_action_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) } @@ -1109,20 +1085,88 @@ 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 { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.flags_union()), + + #[cfg(feature = "tx_v6")] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.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 + '_> { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => Box::new(std::iter::empty()), + + Transaction::V5 { + orchard_shielded_data, + .. + } => Box::new( + orchard_shielded_data + .iter() + .flat_map(orchard::ShieldedData::shared_anchors), + ), + + #[cfg(feature = "tx_v6")] + Transaction::V6 { + orchard_shielded_data, + .. + } => Box::new( + orchard_shielded_data + .iter() + .flat_map(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() + // FIXME: avoid code duplication with orchard_value_balance + let value_balance = match self { + // No Orchard shielded data + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.value_balance()), + + #[cfg(feature = "tx_v6")] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.value_balance()), + }; + + value_balance.is_some() } // value balances @@ -1377,10 +1421,29 @@ 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); + let value_balance = match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.value_balance()), + + #[cfg(feature = "tx_v6")] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.value_balance()), + }; - ValueBalance::from_orchard_amount(orchard_value_balance) + ValueBalance::from_orchard_amount(value_balance.unwrap_or_else(Amount::zero)) } /// Returns the value balances for this transaction using the provided transparent outputs. diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index 96166e4c957..afd4f4ebcca 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, @@ -807,16 +807,21 @@ impl Arbitrary for orchard::Shiel props; Self { - flags, + action_groups: AtLeastOne::from_one(orchard::ActionGroup { + flags, + shared_anchor, + // FIXME: use a proper arbitrary value here? + #[cfg(feature = "tx_v6")] + expiry_height: 0, + #[cfg(feature = "tx_v6")] + burn, + proof, + actions: actions + .try_into() + .expect("arbitrary vector size range produces at least one action"), + }), 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() @@ -1169,14 +1174,19 @@ 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(), + // FIXME: use a proper arbitrary value here? + #[cfg(feature = "tx_v6")] + expiry_height: 0, + #[cfg(feature = "tx_v6")] + burn: Default::default(), + proof: Halo2Proof(vec![]), + actions: at_least_one![dummy_authorized_action], + }), 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 2804b099b32..ac3e91e5743 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -10,7 +10,7 @@ use reddsa::{orchard::Binding, orchard::SpendAuth, Signature}; use crate::{ block::MAX_BLOCK_BYTES, - orchard::{OrchardVanilla, ShieldedDataFlavor}, + orchard::{ActionGroup, OrchardVanilla, ShieldedDataFlavor}, parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID}, primitives::{Halo2Proof, ZkSnarkProof}, serialization::{ @@ -358,11 +358,18 @@ where impl ZcashSerialize for orchard::ShieldedData { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + assert!( + self.action_groups.len() == 1, + "V6 transaction must contain exactly one action group" + ); + + let action_group = self.action_groups.first(); + // Split the AuthorizedAction let (actions, sigs): ( Vec>, Vec>, - ) = self + ) = action_group .actions .iter() .cloned() @@ -373,16 +380,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)?; @@ -398,44 +405,58 @@ 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> { + #[cfg(not(feature = "zsa-swap"))] + assert!( + self.action_groups.len() == 1, + "TxV6 currently supports only one action group" + ); + // Denoted as `nActionGroupsOrchard` in the spec (ZIP 230). // TxV6 currently supports only one action group. - CompactSizeMessage::try_from(1) - .unwrap_or_else(|_| unreachable!()) + 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>>, - ) = self - .actions - .iter() - .cloned() - .map(orchard::AuthorizedAction::into_parts) - .map(|(action, sig)| (action, VersionedSigV0::new(sig))) - .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)?; - - // Denoted as `anchorOrchard` in the spec. - self.shared_anchor.zcash_serialize(&mut writer)?; - - // 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). - self.burn.zcash_serialize(&mut writer)?; - - // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. - self.proof.zcash_serialize(&mut writer)?; - - // Denoted as `vSpendAuthSigsOrchard` in the spec. - zcash_serialize_external_count(&sigs, &mut writer)?; + // FIXME: consider using use zcash_serialize_external_count for or Vec::zcash_serialize + for action_group in self.action_groups.iter() { + // Split the AuthorizedAction + let (actions, sigs): ( + Vec>, + Vec>>, + ) = action_group + .actions + .iter() + .cloned() + .map(orchard::AuthorizedAction::into_parts) + .map(|(action, sig)| (action, VersionedSigV0::new(sig))) + .unzip(); + + // 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 `anchorOrchard` in the spec. + action_group.shared_anchor.zcash_serialize(&mut writer)?; + + // Denoted as `nAGExpiryHeight` in the spec (ZIP 230). + #[cfg(not(feature = "zsa-swap"))] + assert!( + action_group.expiry_height == 0, + "nAGExpiryHeight must be zero for TxV6" + ); + writer.write_u32::(action_group.expiry_height)?; + + // Denoted as `vAssetBurn` in the spec (ZIP 230). + action_group.burn.zcash_serialize(&mut writer)?; + + // Denoted as `sizeProofsOrchard` and `proofsOrchard` in the spec. + action_group.proof.zcash_serialize(&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)?; @@ -511,13 +532,17 @@ impl ZcashDeserialize for Option> { authorized_actions.try_into()?; Ok(Some(orchard::ShieldedData:: { - flags, + action_groups: AtLeastOne::from_one(ActionGroup { + flags, + shared_anchor, + #[cfg(feature = "tx_v6")] + expiry_height: 0, + #[cfg(feature = "tx_v6")] + burn: NoBurn, + proof, + actions, + }), value_balance, - #[cfg(feature = "tx_v6")] - burn: NoBurn, - shared_anchor, - proof, - actions, binding_sig, })) } @@ -530,49 +555,80 @@ impl ZcashDeserialize for Option> { impl ZcashDeserialize for Option> { fn zcash_deserialize(mut reader: R) -> Result { // Denoted as `nActionGroupsOrchard` in the spec (ZIP 230). - // TxV6 currently supports only one action group. 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", + "TxV6 currently supports only one action group.", )); } - // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. - let actions: Vec> = (&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()?; + + // 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 `nAGExpiryHeight` in the spec (ZIP 230). + let expiry_height = reader.read_u32::()?; + #[cfg(not(feature = "zsa-swap"))] + if expiry_height != 0 { + return Err(SerializationError::Parse( + "nAGExpiryHeight must be zero for TxV6", + )); + } - // 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 `vAssetBurn` in the spec (ZIP 230). + let burn = (&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 must be zero for NU7", - )); - } + // Denoted as `vSpendAuthSigsOrchard` in the spec. + let spend_sigs: Vec>> = + zcash_deserialize_external_count(actions.len(), &mut reader)?; - // Denoted as `vAssetBurn` in the spec (ZIP 230). - let burn = (&mut reader).zcash_deserialize_into()?; + // Create the AuthorizedAction from deserialized parts + let authorized_actions: Vec> = actions + .into_iter() + .zip(spend_sigs) + .map(|(action, spend_sig)| { + orchard::AuthorizedAction::from_parts(action, spend_sig.into_signature()) + }) + .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 `vSpendAuthSigsOrchard` in the spec. - let spend_sigs: Vec>> = - zcash_deserialize_external_count(actions.len(), &mut reader)?; + action_groups.push(ActionGroup { + flags, + shared_anchor, + expiry_height, + burn, + proof, + actions, + }) + } // Denoted as `valueBalanceOrchard` in the spec. let value_balance: Amount = (&mut reader).zcash_deserialize_into()?; @@ -581,25 +637,9 @@ impl ZcashDeserialize for Option> { let binding_sig: Signature = VersionedSigV0::zcash_deserialize(&mut reader)?.into_signature(); - // Create the AuthorizedAction from deserialized parts - let authorized_actions: Vec> = actions - .into_iter() - .zip(spend_sigs) - .map(|(action, spend_sig)| { - orchard::AuthorizedAction::from_parts(action, spend_sig.into_signature()) - }) - .collect(); - - let actions: AtLeastOne> = - authorized_actions.try_into()?; - Ok(Some(orchard::ShieldedData:: { - flags, + action_groups: action_groups.try_into()?, value_balance, - burn, - shared_anchor, - proof, - actions, binding_sig, })) } diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index 56495ba0526..13e027732db 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -40,6 +40,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-consensus/src/primitives/halo2.rs b/zebra-consensus/src/primitives/halo2.rs index 691bfaede62..8a0e71be596 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, ShieldedData, ShieldedDataFlavor}; +use zebra_chain::orchard::{ActionGroup, OrchardVanilla, ShieldedDataFlavor}; #[cfg(feature = "tx_v6")] use zebra_chain::orchard::OrchardZSA; @@ -141,16 +141,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()) - .expect("failed to convert flags: shielded_data.flags contains unexpected bits that are not valid in orchard::bundle::Flags"); + let flags = orchard::bundle::Flags::from_byte(action_group.flags.bits()) + .expect("failed to convert flags: action_group.flags contains unexpected bits that are not valid in orchard::bundle::Flags"); - let instances = shielded_data + let instances = action_group .actions() .map(|action| { circuit::Instance::from_parts( @@ -169,7 +169,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/primitives/halo2/tests.rs b/zebra-consensus/src/primitives/halo2/tests.rs index 1b2da85af00..12915449aa7 100644 --- a/zebra-consensus/src/primitives/halo2/tests.rs +++ b/zebra-consensus/src/primitives/halo2/tests.rs @@ -18,10 +18,13 @@ use orchard::{ use rand::rngs::OsRng; use zebra_chain::{ - orchard::{ShieldedData, ShieldedDataFlavor}, - serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}, + orchard::{OrchardVanilla, ShieldedData, ShieldedDataFlavor}, + serialization::{AtLeastOne, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}, }; +#[cfg(feature = "tx_v6")] +use zebra_chain::orchard::OrchardZSA; + use crate::primitives::halo2::*; #[allow(dead_code, clippy::print_stdout)] @@ -77,42 +80,47 @@ where .unwrap(); ShieldedData:: { - flags, + action_groups: AtLeastOne::from_one(ActionGroup { + flags, + shared_anchor: anchor_bytes.try_into().unwrap(), + // FIXME: use a proper value below if needed + #[cfg(feature = "tx_v6")] + expiry_height: 0, + #[cfg(feature = "tx_v6")] + burn: bundle.burn().as_slice().into(), + 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 + .as_ref() + .try_into() + .unwrap(), + out_ciphertext: a.encrypted_note().out_ciphertext.into(), + }; + zebra_chain::orchard::shielded_data::AuthorizedAction { + action, + spend_auth_sig: <[u8; 64]>::from(a.authorization().sig()).into(), + } + }) + .collect::>() + .try_into() + .unwrap(), + }), 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 - .as_ref() - .try_into() - .unwrap(), - out_ciphertext: a.encrypted_note().out_ciphertext.into(), - }; - zebra_chain::orchard::shielded_data::AuthorizedAction { - action, - spend_auth_sig: <[u8; 64]>::from(a.authorization().sig()).into(), - } - }) - .collect::>() - .try_into() - .unwrap(), binding_sig: <[u8; 64]>::from(bundle.authorization().binding_signature().sig()) .into(), - #[cfg(feature = "tx_v6")] - burn: bundle.burn().as_slice().into(), } }) .collect(); @@ -136,11 +144,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 { @@ -216,17 +226,19 @@ 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); - // FIXME: What about zebra_chain::orchard::Flags::ENABLE_ZSA? + ag.flags.remove(zebra_chain::orchard::Flags::ENABLE_SPENDS); + ag.flags.remove(zebra_chain::orchard::Flags::ENABLE_OUTPUTS); + // FIXME: What about zebra_chain::orchard::Flags::ENABLE_ZSA? - 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.rs b/zebra-consensus/src/transaction.rs index 4e134f7f59e..9af5cb77aca 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -1056,6 +1056,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}, @@ -1067,13 +1068,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-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 06aa7040b1c..06bc30015a5 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 = @@ -2866,15 +2870,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( @@ -2908,15 +2915,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( diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index f341e26cde3..aba8657d1cd 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -51,6 +51,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/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/Cargo.toml b/zebrad/Cargo.toml index 0691a2367be..0137892e205 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -163,6 +163,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" } diff --git a/zebrad/src/components/mempool/storage/tests/prop.rs b/zebrad/src/components/mempool/storage/tests/prop.rs index ef0da69d2d1..31cbf786178 100644 --- a/zebrad/src/components/mempool/storage/tests/prop.rs +++ b/zebrad/src/components/mempool/storage/tests/prop.rs @@ -773,19 +773,18 @@ impl SpendConflictTestInput { maybe_shielded_data: &mut Option>, conflicts: &HashSet, ) { - if let Some(shielded_data) = maybe_shielded_data.take() { - let updated_actions: Vec<_> = shielded_data - .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; + } } } } @@ -1013,8 +1012,19 @@ impl OrchardSpendConflict { /// The transaction will then conflict with any other transaction with the same new nullifier. pub fn apply_to(self, 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() + .next() + .expect("at least one action") + .nullifier; } else { *orchard_shielded_data = Some(self.new_shielded_data.0); }