diff --git a/Cargo.lock b/Cargo.lock index 08054b362ec..cad04052478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2896,6 +2896,7 @@ dependencies = [ "memuse", "nonempty", "pasta_curves", + "proptest", "rand 0.8.5", "reddsa", "serde", diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 37e013c0924..82cc28f3045 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -15,8 +15,8 @@ keywords = ["zebra", "zcash"] categories = ["asynchronous", "cryptography::cryptocurrencies", "encoding"] [features] -default = [] -#default = ["tx-v6"] +#default = [] +default = ["tx-v6"] # Production features that activate extra functionality @@ -57,6 +57,7 @@ proptest-impl = [ "rand_chacha", "tokio/tracing", "zebra-test", + "orchard/test-dependencies" ] bench = ["zebra-test"] @@ -179,6 +180,8 @@ tokio = { version = "1.41.0", features = ["full", "tracing", "test-util"] } zebra-test = { path = "../zebra-test/", version = "1.0.0-beta.41" } +orchard = { workspace = true, features = ["test-dependencies"] } + [[bench]] name = "block" harness = false diff --git a/zebra-chain/src/orchard/arbitrary.rs b/zebra-chain/src/orchard/arbitrary.rs index 54572085f11..89618e451ad 100644 --- a/zebra-chain/src/orchard/arbitrary.rs +++ b/zebra-chain/src/orchard/arbitrary.rs @@ -11,17 +11,21 @@ use proptest::{array, collection::vec, prelude::*}; use super::{ keys::*, note, tree, Action, AuthorizedAction, Flags, NoteCommitment, OrchardFlavorExt, - OrchardVanilla, ValueCommitment, + ValueCommitment, }; -impl Arbitrary for Action { +impl Arbitrary for Action +// FIXME: define the constraint in OrchardFlavorExt? +where + ::Strategy: 'static, +{ type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( any::(), any::(), - any::>(), + any::(), any::(), ) .prop_map(|(nullifier, rk, enc_ciphertext, out_ciphertext)| Self { @@ -55,11 +59,15 @@ impl Arbitrary for note::Nullifier { type Strategy = BoxedStrategy; } -impl Arbitrary for AuthorizedAction { +impl Arbitrary for AuthorizedAction +// FIXME: define the constraint in OrchardFlavorExt? +where + ::Strategy: 'static, +{ type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::>(), any::()) + (any::>(), any::()) .prop_map(|(action, spend_auth_sig)| Self { action, spend_auth_sig: spend_auth_sig.0, diff --git a/zebra-chain/src/orchard/orchard_flavor_ext.rs b/zebra-chain/src/orchard/orchard_flavor_ext.rs index f8ce25d0ed5..119646ae7b9 100644 --- a/zebra-chain/src/orchard/orchard_flavor_ext.rs +++ b/zebra-chain/src/orchard/orchard_flavor_ext.rs @@ -1,6 +1,6 @@ //! This module defines traits and structures for supporting the Orchard Shielded Protocol //! for `V5` and `V6` versions of the transaction. -use std::{fmt::Debug, io}; +use std::fmt::Debug; use serde::{de::DeserializeOwned, Serialize}; @@ -9,17 +9,28 @@ use proptest_derive::Arbitrary; use orchard::{note_encryption::OrchardDomainCommon, orchard_flavor}; -use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}; +use crate::serialization::{ZcashDeserialize, ZcashSerialize}; + +#[cfg(feature = "tx-v6")] +use crate::orchard_zsa::burn::{Burn, NoBurn}; use super::note; -#[cfg(feature = "tx-v6")] -use crate::orchard_zsa::burn::BurnItem; +#[cfg(not(any(test, feature = "proptest-impl")))] +pub trait TestArbitrary {} + +#[cfg(not(any(test, feature = "proptest-impl")))] +impl TestArbitrary for T {} + +#[cfg(any(test, feature = "proptest-impl"))] +pub trait TestArbitrary: proptest::prelude::Arbitrary {} + +#[cfg(any(test, feature = "proptest-impl"))] +impl TestArbitrary for T {} /// A trait representing compile-time settings of Orchard Shielded Protocol used in /// the transactions `V5` and `V6`. pub trait OrchardFlavorExt: Clone + Debug { - /// A type representing an encrypted note for this protocol version. /// A type representing an encrypted note for this protocol version. type EncryptedNote: Clone + Debug @@ -28,16 +39,18 @@ pub trait OrchardFlavorExt: Clone + Debug { + DeserializeOwned + Serialize + ZcashDeserialize - + ZcashSerialize; + + ZcashSerialize + + TestArbitrary; - /// FIXME: add doc + /// Specifies the Orchard protocol flavor from `orchard` crate used by this implementation. type Flavor: orchard_flavor::OrchardFlavor; /// The size of the encrypted note for this protocol version. const ENCRYPTED_NOTE_SIZE: usize = Self::Flavor::ENC_CIPHERTEXT_SIZE; /// A type representing a burn field for this protocol version. - type BurnType: Clone + Debug + Default + ZcashDeserialize + ZcashSerialize; + #[cfg(feature = "tx-v6")] + type BurnType: Clone + Debug + Default + ZcashDeserialize + ZcashSerialize + TestArbitrary; } /// A structure representing a tag for Orchard protocol variant used for the transaction version `V5`. @@ -52,27 +65,11 @@ pub struct OrchardVanilla; #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] pub struct OrchardZSA; -/// A special marker type indicating the absence of a burn field in Orchard ShieldedData for `V5` transactions. -/// Useful for unifying ShieldedData serialization and deserialization implementations across various -/// Orchard protocol variants (i.e. various transaction versions). -#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] -pub struct NoBurn; - -impl ZcashSerialize for NoBurn { - fn zcash_serialize(&self, mut _writer: W) -> Result<(), io::Error> { - Ok(()) - } -} - -impl ZcashDeserialize for NoBurn { - fn zcash_deserialize(mut _reader: R) -> Result { - Ok(Self) - } -} - impl OrchardFlavorExt for OrchardVanilla { type Flavor = orchard_flavor::OrchardVanilla; type EncryptedNote = note::EncryptedNote<{ Self::ENCRYPTED_NOTE_SIZE }>; + + #[cfg(feature = "tx-v6")] type BurnType = NoBurn; } @@ -80,5 +77,7 @@ impl OrchardFlavorExt for OrchardVanilla { impl OrchardFlavorExt for OrchardZSA { type Flavor = orchard_flavor::OrchardZSA; type EncryptedNote = note::EncryptedNote<{ Self::ENCRYPTED_NOTE_SIZE }>; - type BurnType = Vec; + + #[cfg(feature = "tx-v6")] + type BurnType = Burn; } diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs index 9806850daf9..d1dae03d792 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -1,7 +1,12 @@ //! Orchard ZSA related functionality. +// FIXME: remove pub(crate) later if possible +#[cfg(any(test, feature = "proptest-impl"))] +pub(crate) mod arbitrary; + +mod common; + pub mod burn; pub mod issuance; -pub mod serialize; pub use burn::BurnItem; diff --git a/zebra-chain/src/orchard_zsa/arbitrary.rs b/zebra-chain/src/orchard_zsa/arbitrary.rs new file mode 100644 index 00000000000..0dc89ce7080 --- /dev/null +++ b/zebra-chain/src/orchard_zsa/arbitrary.rs @@ -0,0 +1,68 @@ +//! Randomised data generation for Orchard ZSA types. + +use proptest::prelude::*; + +use orchard::{bundle::testing::BundleArb, issuance::testing::arb_signed_issue_bundle}; + +// FIXME: consider using another value, i.e. define MAX_BURN_ITEMS constant for that +use crate::transaction::arbitrary::MAX_ARBITRARY_ITEMS; + +use super::{ + burn::{Burn, BurnItem, NoBurn}, + issuance::IssueData, +}; + +impl Arbitrary for BurnItem { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // FIXME: move arb_asset_to_burn out of BundleArb in orchard + // as it does not depend on flavor (we pinned it here OrchardVanilla + // just for certainty, as there's no difference, which flavor to use) + // FIXME: consider to use BurnItem(asset_base, value.try_into().expect("Invalid value for Amount")) + // instead of filtering non-convertable values + // FIXME: should we filter/protect from including native assets into burn here? + BundleArb::::arb_asset_to_burn() + .prop_filter_map("Conversion to Amount failed", |(asset_base, value)| { + BurnItem::try_from((asset_base, value)).ok() + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for NoBurn { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // FIXME: consider using this instead, for clarity: any::<()>().prop_map(|_| NoBurn).boxed() + Just(Self).boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for Burn { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop::collection::vec(any::(), 0..MAX_ARBITRARY_ITEMS) + .prop_map(|inner| inner.into()) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for IssueData { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_signed_issue_bundle(MAX_ARBITRARY_ITEMS) + .prop_map(|bundle| bundle.into()) + .boxed() + } + + type Strategy = BoxedStrategy; +} diff --git a/zebra-chain/src/orchard_zsa/burn.rs b/zebra-chain/src/orchard_zsa/burn.rs index 04c878feb49..812728b9380 100644 --- a/zebra-chain/src/orchard_zsa/burn.rs +++ b/zebra-chain/src/orchard_zsa/burn.rs @@ -8,19 +8,29 @@ use crate::{ serialization::{SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashSerialize}, }; -use orchard::note::AssetBase; +use orchard::{note::AssetBase, value::NoteValue}; -use super::serialize::ASSET_BASE_SIZE; +use super::common::ASSET_BASE_SIZE; // Sizes of the serialized values for types in bytes (used for TrustedPreallocate impls) const AMOUNT_SIZE: u64 = 8; + // FIXME: is this a correct way to calculate (simple sum of sizes of components)? const BURN_ITEM_SIZE: u64 = ASSET_BASE_SIZE + AMOUNT_SIZE; -/// Represents an Orchard ZSA burn item. +/// Orchard ZSA burn item. #[derive(Clone, Debug, PartialEq, Eq)] pub struct BurnItem(AssetBase, Amount); +// Convert from burn item type used in `orchard` crate +impl TryFrom<(AssetBase, NoteValue)> for BurnItem { + type Error = crate::amount::Error; + + fn try_from(item: (AssetBase, NoteValue)) -> Result { + Ok(Self(item.0, item.1.inner().try_into()?)) + } +} + impl ZcashSerialize for BurnItem { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { let BurnItem(asset_base, amount) = self; @@ -50,18 +60,16 @@ impl TrustedPreallocate for BurnItem { } } -#[cfg(any(test, feature = "proptest-impl"))] impl serde::Serialize for BurnItem { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { - // FIXME: return custom error with a meaningful description? + // FIXME: return a custom error with a meaningful description? (self.0.to_bytes(), &self.1).serialize(serializer) } } -#[cfg(any(test, feature = "proptest-impl"))] impl<'de> serde::Deserialize<'de> for BurnItem { fn deserialize(deserializer: D) -> Result where @@ -78,3 +86,43 @@ impl<'de> serde::Deserialize<'de> for BurnItem { )) } } + +/// A special marker type indicating the absence of a burn field in Orchard ShieldedData for `V5` transactions. +/// Useful for unifying ShieldedData serialization and deserialization implementations across various +/// Orchard protocol variants (i.e. various transaction versions). +#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct NoBurn; + +impl ZcashSerialize for NoBurn { + fn zcash_serialize(&self, mut _writer: W) -> Result<(), io::Error> { + Ok(()) + } +} + +impl ZcashDeserialize for NoBurn { + fn zcash_deserialize(mut _reader: R) -> Result { + Ok(Self) + } +} + +/// Orchard ZSA burn items (assets intended for burning) +#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Burn(Vec); + +impl From> for Burn { + fn from(inner: Vec) -> Self { + Self(inner) + } +} + +impl ZcashSerialize for Burn { + fn zcash_serialize(&self, writer: W) -> Result<(), io::Error> { + self.0.zcash_serialize(writer) + } +} + +impl ZcashDeserialize for Burn { + fn zcash_deserialize(reader: R) -> Result { + Ok(Burn(Vec::::zcash_deserialize(reader)?)) + } +} diff --git a/zebra-chain/src/orchard_zsa/serialize.rs b/zebra-chain/src/orchard_zsa/common.rs similarity index 94% rename from zebra-chain/src/orchard_zsa/serialize.rs rename to zebra-chain/src/orchard_zsa/common.rs index 6afc51e1887..deb3969ced7 100644 --- a/zebra-chain/src/orchard_zsa/serialize.rs +++ b/zebra-chain/src/orchard_zsa/common.rs @@ -7,7 +7,7 @@ use crate::serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, Z use orchard::note::AssetBase; // The size of the serialized AssetBase in bytes (used for TrustedPreallocate impls) -pub(crate) const ASSET_BASE_SIZE: u64 = 32; +pub(super) const ASSET_BASE_SIZE: u64 = 32; impl ZcashSerialize for AssetBase { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { diff --git a/zebra-chain/src/orchard_zsa/issuance.rs b/zebra-chain/src/orchard_zsa/issuance.rs index 9d31276072c..8a1dde30ca5 100644 --- a/zebra-chain/src/orchard_zsa/issuance.rs +++ b/zebra-chain/src/orchard_zsa/issuance.rs @@ -26,13 +26,19 @@ use orchard::{ Address, Note, }; -use super::serialize::ASSET_BASE_SIZE; +use super::common::ASSET_BASE_SIZE; /// Wrapper for `IssueBundle` used in the context of Transaction V6. This allows the implementation of /// a Serde serializer for unit tests within this crate. #[derive(Clone, Debug, PartialEq, Eq)] pub struct IssueData(IssueBundle); +impl From> for IssueData { + fn from(inner: IssueBundle) -> Self { + Self(inner) + } +} + // Sizes of the serialized values for types in bytes (used for TrustedPreallocate impls) // FIXME: are those values correct (43, 32 etc.)? //const ISSUANCE_VALIDATING_KEY_SIZE: u64 = 32; diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index ffd169f651f..eb8c7c56ed6 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -20,6 +20,9 @@ use crate::{ LedgerState, }; +#[cfg(feature = "tx-v6")] +use crate::orchard_zsa::issuance::IssueData; + use itertools::Itertools; use super::{ @@ -132,8 +135,21 @@ impl Transaction { .boxed() } - /// Generate a proptest strategy for V5 Transactions - pub fn v5_strategy(ledger_state: LedgerState) -> BoxedStrategy { + /// Helper function to generate the common transaction fields. + /// This function is generic over the Orchard shielded data type. + fn v5_v6_strategy_common( + ledger_state: LedgerState, + ) -> impl Strategy< + Value = ( + NetworkUpgrade, + LockTime, + block::Height, + Vec, + Vec, + Option>, + Option>, + ), + > + 'static { ( NetworkUpgrade::branch_id_strategy(), any::(), @@ -141,7 +157,7 @@ impl Transaction { transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS), vec(any::(), 0..MAX_ARBITRARY_ITEMS), option::of(any::>()), - option::of(any::>()), + option::of(any::>()), ) .prop_map( move |( @@ -153,29 +169,97 @@ impl Transaction { sapling_shielded_data, orchard_shielded_data, )| { - Transaction::V5 { - network_upgrade: if ledger_state.transaction_has_valid_network_upgrade() { - ledger_state.network_upgrade() - } else { - network_upgrade - }, + // Apply conditional logic based on ledger_state + let network_upgrade = if ledger_state.transaction_has_valid_network_upgrade() { + ledger_state.network_upgrade() + } else { + network_upgrade + }; + + let sapling_shielded_data = if ledger_state.height.is_min() { + // The genesis block should not contain any shielded data. + None + } else { + sapling_shielded_data + }; + + let orchard_shielded_data = if ledger_state.height.is_min() { + // The genesis block should not contain any shielded data. + None + } else { + orchard_shielded_data + }; + + ( + network_upgrade, lock_time, expiry_height, inputs, outputs, - sapling_shielded_data: if ledger_state.height.is_min() { - // The genesis block should not contain any shielded data. - None - } else { - sapling_shielded_data - }, - orchard_shielded_data: if ledger_state.height.is_min() { - // The genesis block should not contain any shielded data. - None - } else { - orchard_shielded_data - }, - } + sapling_shielded_data, + orchard_shielded_data, + ) + }, + ) + } + + /// Generate a proptest strategy for V5 Transactions + pub fn v5_strategy(ledger_state: LedgerState) -> BoxedStrategy { + Self::v5_v6_strategy_common::(ledger_state) + .prop_map( + move |( + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + )| Transaction::V5 { + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + }, + ) + .boxed() + } + + /// Generate a proptest strategy for V6 Transactions + #[cfg(feature = "tx-v6")] + pub fn v6_strategy(ledger_state: LedgerState) -> BoxedStrategy { + Self::v5_v6_strategy_common::(ledger_state) + .prop_flat_map(|common_fields| { + // FIXME: Can IssueData present in V6 transaction without orchard::ShieldedData? + // If no, we possibly need to use something like prop_filter_map to filter wrong + // combnations (orchard_shielded_data: None, orchard_zsa_issue_data: Some) + option::of(any::()) + .prop_map(move |issue_data| (common_fields.clone(), issue_data)) + }) + .prop_map( + |( + ( + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + ), + orchard_zsa_issue_data, + )| Transaction::V6 { + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + orchard_zsa_issue_data, }, ) .boxed() @@ -697,7 +781,12 @@ impl Arbitrary for sapling::TransferData { type Strategy = BoxedStrategy; } -impl Arbitrary for orchard::ShieldedData { +impl Arbitrary for orchard::ShieldedData +// FIXME: remove the following lines +// FIXME: define the constraint in OrchardFlavorExt? +//where +// ::Strategy: 'static, +{ type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { @@ -707,13 +796,22 @@ impl Arbitrary for orchard::ShieldedData { any::(), any::(), vec( - any::>(), + any::>(), 1..MAX_ARBITRARY_ITEMS, ), any::(), + #[cfg(feature = "tx-v6")] + any::(), ) - .prop_map( - |(flags, value_balance, shared_anchor, proof, actions, binding_sig)| Self { + .prop_map(|props| { + #[cfg(not(feature = "tx-v6"))] + let (flags, value_balance, shared_anchor, proof, actions, binding_sig) = props; + + #[cfg(feature = "tx-v6")] + let (flags, value_balance, shared_anchor, proof, actions, binding_sig, burn) = + props; + + Self { flags, value_balance, shared_anchor, @@ -723,9 +821,9 @@ impl Arbitrary for orchard::ShieldedData { .expect("arbitrary vector size range produces at least one action"), binding_sig: binding_sig.0, #[cfg(feature = "tx-v6")] - burn: Default::default(), - }, - ) + burn, + } + }) .boxed() } @@ -767,6 +865,8 @@ impl Arbitrary for Transaction { Some(3) => return Self::v3_strategy(ledger_state), Some(4) => return Self::v4_strategy(ledger_state), Some(5) => return Self::v5_strategy(ledger_state), + #[cfg(feature = "tx-v6")] + Some(6) => return Self::v6_strategy(ledger_state), Some(_) => unreachable!("invalid transaction version in override"), None => {} } @@ -780,11 +880,13 @@ impl Arbitrary for Transaction { NetworkUpgrade::Blossom | NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => { Self::v4_strategy(ledger_state) } + // FIXME: should v6_strategy be included here? NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 => prop_oneof![ Self::v4_strategy(ledger_state.clone()), Self::v5_strategy(ledger_state) ] .boxed(), + // FIXME: process NetworkUpgrade::Nu7 properly, with v6 strategy } } diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index f85b10d9370..05211f296ac 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -15,8 +15,8 @@ keywords = ["zebra", "zcash"] categories = ["asynchronous", "cryptography::cryptocurrencies"] [features] -default = [] -#default = ["tx-v6"] +#default = [] +default = ["tx-v6"] # Production features that activate extra dependencies, or extra features in dependencies