diff --git a/src/asset_record.rs b/src/asset_record.rs new file mode 100644 index 000000000..b3af557ca --- /dev/null +++ b/src/asset_record.rs @@ -0,0 +1,28 @@ +//! Structs and logic related to aggregated information about an asset. + +use crate::{value::NoteValue, Note}; + +/// Represents aggregated information about an asset, including its supply, finalization status, +/// and reference note. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AssetRecord { + /// The amount of the asset. + pub amount: NoteValue, + + /// Whether or not the asset is finalized. + pub is_finalized: bool, + + /// A reference note + pub reference_note: Note, +} + +impl AssetRecord { + /// Creates a new [`AssetRecord`] instance. + pub fn new(amount: NoteValue, is_finalized: bool, reference_note: Note) -> Self { + Self { + amount, + is_finalized, + reference_note, + } + } +} diff --git a/src/issuance.rs b/src/issuance.rs index 28597e353..531db1811 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -1,19 +1,27 @@ -//! Structs related to issuance bundles and the associated logic. +//! Issuance logic for Zcash Shielded Assets (ZSAs). +//! +//! This module defines structures and methods for creating, authorizing, and verifying +//! issuance bundles, which introduce new shielded assets into the Orchard protocol. +//! +//! The core components include: +//! - `IssueBundle`: Represents a collection of issuance actions with authorization states. +//! - `IssueAction`: Defines an individual issuance event, including asset details and notes. +//! - `IssueAuth` variants: Track issuance states from creation to finalization. +//! - `verify_issue_bundle`: Ensures issuance validity and prevents unauthorized asset creation. +//! +//! Errors related to issuance, such as invalid signatures or supply overflows, +//! are handled through the `Error` enum. + use blake2b_simd::Hash as Blake2bHash; use group::Group; use k256::schnorr; use nonempty::NonEmpty; use rand::RngCore; -use std::collections::HashSet; +use std::collections::HashMap; use std::fmt; use crate::bundle::commitments::{hash_issue_bundle_auth_data, hash_issue_bundle_txid_data}; use crate::constants::reference_keys::ReferenceKeys; -use crate::issuance::Error::{ - AssetBaseCannotBeIdentityPoint, CannotBeFirstIssuance, IssueActionNotFound, - IssueActionPreviouslyFinalizedAssetBase, IssueActionWithoutNoteNotFinalized, - IssueBundleIkMismatchAssetBase, IssueBundleInvalidSignature, ValueOverflow, WrongAssetDescSize, -}; use crate::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey}; use crate::note::asset_base::is_asset_desc_of_valid_size; use crate::note::{AssetBase, Nullifier, Rho}; @@ -21,7 +29,14 @@ use crate::note::{AssetBase, Nullifier, Rho}; use crate::value::NoteValue; use crate::{Address, Note}; -use crate::supply_info::{AssetSupply, SupplyInfo}; +use crate::asset_record::AssetRecord; + +use Error::{ + AssetBaseCannotBeIdentityPoint, CannotBeFirstIssuance, IssueActionNotFound, + IssueActionPreviouslyFinalizedAssetBase, IssueActionWithoutNoteNotFinalized, + IssueBundleIkMismatchAssetBase, IssueBundleInvalidSignature, + MissingReferenceNoteOnFirstIssuance, ValueOverflow, WrongAssetDescSize, +}; /// Checks if a given note is a reference note. /// @@ -108,8 +123,8 @@ impl IssueAction { /// /// This function calculates the total value (supply) of the asset by summing the values /// of all its notes and ensures that all note types are equal. It returns the asset and - /// its supply as a tuple (`AssetBase`, `AssetSupply`) or an error if the asset was not - /// properly derived or an overflow occurred during the supply amount calculation. + /// its supply as a tuple (`AssetBase`, `NoteValue`) or an error if the asset was not + /// properly derived or an overflow occurred during the supply amount calculation. /// /// # Arguments /// @@ -117,19 +132,22 @@ impl IssueAction { /// /// # Returns /// - /// A `Result` containing a tuple with an `AssetBase` and an `AssetSupply`, or an `Error`. + /// A `Result` containing a tuple with an `AssetBase` and an `NoteValue`, or an `Error`. /// /// # Errors /// /// This function may return an error in any of the following cases: /// - /// * `ValueOverflow`: If the total amount value of all notes in the `IssueAction` overflows. - /// - /// * `IssueBundleIkMismatchAssetBase`: If the provided `ik` is not used to derive the + /// * `WrongAssetDescSize`: The asset description size is invalid. + /// * `ValueOverflow`: The total amount value of all notes in the `IssueAction` overflows. + /// * `IssueBundleIkMismatchAssetBase`: The provided `ik` is not used to derive the /// `AssetBase` for **all** internal notes. - /// - /// * `IssueActionWithoutNoteNotFinalized`:If the `IssueAction` contains no note and is not finalized. - fn verify_supply(&self, ik: &IssuanceValidatingKey) -> Result<(AssetBase, AssetSupply), Error> { + /// * `IssueActionWithoutNoteNotFinalized`: The `IssueAction` contains no notes and is not finalized. + fn verify(&self, ik: &IssuanceValidatingKey) -> Result<(AssetBase, NoteValue), Error> { + if !is_asset_desc_of_valid_size(self.asset_desc()) { + return Err(WrongAssetDescSize); + } + if self.notes.is_empty() && !self.is_finalized() { return Err(IssueActionWithoutNoteNotFinalized); } @@ -148,23 +166,15 @@ impl IssueAction { } // All assets should be derived correctly - note.asset() - .eq(&issue_asset) - .then_some(()) - .ok_or(IssueBundleIkMismatchAssetBase)?; + if note.asset() != issue_asset { + return Err(IssueBundleIkMismatchAssetBase); + } // The total amount should not overflow (value_sum + note.value()).ok_or(ValueOverflow) })?; - Ok(( - issue_asset, - AssetSupply::new( - value_sum, - self.is_finalized(), - self.get_reference_note().cloned(), - ), - )) + Ok((issue_asset, value_sum)) } /// Serialize `finalize` flag to a byte @@ -275,8 +285,8 @@ impl IssueBundle { /// /// # Returns /// - /// If a single matching action is found, it is returned as `Some(&IssueAction)`. - /// If no action matches the given Asset Base `asset`, it returns `None`. + /// Returns `Some(&IssueAction)` if a single matching action is found. + /// Returns `None` if no action matches the given asset base. /// /// # Panics /// @@ -343,7 +353,7 @@ impl IssueBundle { /// /// This function may return an error in any of the following cases: /// - /// * `WrongAssetDescSize`: If `asset_desc` is empty or longer than 512 bytes. + /// * `WrongAssetDescSize`: The `asset_desc` is empty or longer than 512 bytes. pub fn new( ik: IssuanceValidatingKey, asset_desc: Vec, @@ -407,7 +417,7 @@ impl IssueBundle { /// /// This function may return an error in any of the following cases: /// - /// * `WrongAssetDescSize`: If `asset_desc` is empty or longer than 512 bytes. + /// * `WrongAssetDescSize`: The `asset_desc` is empty or longer than 512 bytes. pub fn add_recipient( &mut self, asset_desc: &[u8], @@ -539,7 +549,7 @@ impl IssueBundle { // Make sure the `expected_ik` matches the `asset` for all notes. self.actions.iter().try_for_each(|action| { - action.verify_supply(&expected_ik)?; + action.verify(&expected_ik)?; Ok(()) })?; @@ -584,72 +594,88 @@ impl IssueBundle { } } -/// Validation for Orchard IssueBundles +/// Validates an [`IssueBundle`] by performing the following checks: /// -/// A set of previously finalized asset types must be provided in `finalized` argument. +/// - **IssueBundle Auth signature verification**: +/// - Ensures the signature on the provided `sighash` matches the bundle’s authorization. +/// - **Static IssueAction verification**: +/// - Runs checks using the `IssueAction::verify` method. +/// - **Node global state related verification**: +/// - Ensures the total supply value does not overflow when adding the new amount to the existing supply. +/// - Verifies that the `AssetBase` has not already been finalized. +/// - Requires a reference note for the *first issuance* of an asset; subsequent issuance may omit it. /// -/// The following checks are performed: -/// * For the `IssueBundle`: -/// * the Signature on top of the provided `sighash` verifies correctly. -/// * For each `IssueAction`: -/// * Asset description size is correct. -/// * `AssetBase` for the `IssueAction` has not been previously finalized. -/// * For each `Note` inside an `IssueAction`: -/// * All notes have the same, correct `AssetBase`. +/// # Arguments +/// +/// * `bundle`: A reference to the [`IssueBundle`] to be validated. +/// * `sighash`: A 32-byte array representing the `sighash` used to verify the bundle's signature. +/// * `get_global_asset_state`: A closure that takes a reference to an [`AssetBase`] and returns an +/// [`Option`], representing the current state of the asset from a global store +/// of previously issued assets. /// /// # Returns /// -/// A Result containing a SupplyInfo struct, which stores supply information in a HashMap. -/// The HashMap `assets` uses AssetBase as the key, and an AssetSupply struct as the -/// value. The AssetSupply contains a NoteValue (representing the total value of all notes for -/// the asset), a bool indicating whether the asset is finalized and a Note (the reference note -/// for this asset). +/// A `Result` containing a [`HashMap`] upon success, where each key-value +/// pair represents the new or updated state of an asset. The key is an [`AssetBase`], and the value +/// is the corresponding updated [`AssetRecord`]. /// /// # Errors /// -/// * `IssueBundleInvalidSignature`: This error occurs if the signature verification -/// for the provided `sighash` fails. -/// * `WrongAssetDescSize`: This error is raised if the asset description size for any -/// asset in the bundle is incorrect. -/// * `IssueActionPreviouslyFinalizedAssetBase`: This error occurs if the asset has already been -/// finalized (inserted into the `finalized` collection). -/// * `ValueOverflow`: This error occurs if an overflow happens during the calculation of -/// the value sum for the notes in the asset. -/// * `IssueBundleIkMismatchAssetBase`: This error is raised if the `AssetBase` derived from -/// the `ik` (Issuance Validating Key) and the `asset_desc` (Asset Description) does not match -/// the expected `AssetBase`. +/// * `IssueBundleInvalidSignature`: Signature verification for the provided `sighash` fails. +/// * `ValueOverflow`: adding the new amount to the existing total supply causes an overflow. +/// * `IssueActionPreviouslyFinalizedAssetBase`: An action is attempted on an asset that has +/// already been finalized. +/// * `MissingReferenceNoteOnFirstIssuance`: No reference note is provided for the first +/// issuance of a new asset. +/// * **Other Errors**: Any additional errors returned by the `IssueAction::verify` method are +/// propagated pub fn verify_issue_bundle( bundle: &IssueBundle, sighash: [u8; 32], - finalized: &HashSet, // The finalization set. -) -> Result { + get_global_records: impl Fn(&AssetBase) -> Option, +) -> Result, Error> { bundle - .ik - .verify(&sighash, &bundle.authorization.signature) + .ik() + .verify(&sighash, bundle.authorization().signature()) .map_err(|_| IssueBundleInvalidSignature)?; - let supply_info = - bundle - .actions() - .iter() - .try_fold(SupplyInfo::new(), |mut supply_info, action| { - if !is_asset_desc_of_valid_size(action.asset_desc()) { - return Err(WrongAssetDescSize); - } - - let (asset, supply) = action.verify_supply(bundle.ik())?; - - // Fail if the asset was previously finalized. - if finalized.contains(&asset) { - return Err(IssueActionPreviouslyFinalizedAssetBase(asset)); + bundle + .actions() + .iter() + .try_fold(HashMap::new(), |mut new_records, action| { + let (asset, amount) = action.verify(bundle.ik())?; + + let is_finalized = action.is_finalized(); + let ref_note = action.get_reference_note(); + + let new_asset_record = match new_records + .get(&asset) + .cloned() + .or_else(|| get_global_records(&asset)) + { + // The first issuance of the asset + None => AssetRecord::new( + amount, + is_finalized, + *ref_note.ok_or(MissingReferenceNoteOnFirstIssuance)?, + ), + + // Subsequent issuance of the asset + Some(current_record) => { + let amount = (current_record.amount + amount).ok_or(ValueOverflow)?; + + if current_record.is_finalized { + return Err(IssueActionPreviouslyFinalizedAssetBase); + } + + AssetRecord::new(amount, is_finalized, current_record.reference_note) } + }; - supply_info.add_supply(asset, supply)?; + new_records.insert(asset, new_asset_record); - Ok(supply_info) - })?; - - Ok(supply_info) + Ok(new_records) + }) } /// Errors produced during the issuance process @@ -672,10 +698,13 @@ pub enum Error { /// Invalid signature. IssueBundleInvalidSignature, /// The provided `AssetBase` has been previously finalized. - IssueActionPreviouslyFinalizedAssetBase(AssetBase), + IssueActionPreviouslyFinalizedAssetBase, /// Overflow error occurred while calculating the value of the asset ValueOverflow, + + /// No reference note is provided for the first issuance of a new asset. + MissingReferenceNoteOnFirstIssuance, } impl fmt::Display for Error { @@ -714,7 +743,7 @@ impl fmt::Display for Error { IssueBundleInvalidSignature => { write!(f, "invalid signature") } - IssueActionPreviouslyFinalizedAssetBase(_) => { + IssueActionPreviouslyFinalizedAssetBase => { write!(f, "the provided `AssetBase` has been previously finalized") } ValueOverflow => { @@ -723,13 +752,19 @@ impl fmt::Display for Error { "overflow error occurred while calculating the value of the asset" ) } + MissingReferenceNoteOnFirstIssuance => { + write!( + f, + "no reference note is provided for the first issuance of a new asset." + ) + } } } } #[cfg(test)] mod tests { - use super::{AssetSupply, IssueBundle, IssueInfo}; + use super::{AssetRecord, IssueBundle, IssueInfo}; use crate::{ builder::{Builder, BundleType}, circuit::ProvingKey, @@ -757,7 +792,7 @@ mod tests { use pasta_curves::pallas::{Point, Scalar}; use rand::rngs::OsRng; use rand::RngCore; - use std::collections::HashSet; + use std::collections::HashMap; /// Validation for reference note /// @@ -770,14 +805,17 @@ mod tests { assert_eq!(note.asset(), asset); } - fn setup_params() -> ( - OsRng, - IssuanceAuthorizingKey, - IssuanceValidatingKey, - Address, - [u8; 32], - Nullifier, - ) { + #[derive(Clone)] + struct TestParams { + rng: OsRng, + isk: IssuanceAuthorizingKey, + ik: IssuanceValidatingKey, + recipient: Address, + sighash: [u8; 32], + first_nullifier: Nullifier, + } + + fn setup_params() -> TestParams { let mut rng = OsRng; let isk = IssuanceAuthorizingKey::random(); @@ -791,21 +829,33 @@ mod tests { let first_nullifier = Nullifier::dummy(&mut rng); - (rng, isk, ik, recipient, sighash, first_nullifier) + TestParams { + rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } } - /// Sets up test parameters for supply tests. + /// Sets up test parameters for action verification tests. /// /// This function generates two notes with the specified values and asset descriptions, /// and returns the issuance validating key, the asset base, and the issue action. - fn supply_test_params( + fn action_verify_test_params( note1_value: u64, note2_value: u64, note1_asset_desc: &[u8], note2_asset_desc: Option<&[u8]>, // if None, both notes use the same asset finalize: bool, ) -> (IssuanceValidatingKey, AssetBase, IssueAction) { - let (mut rng, _, ik, recipient, _, _) = setup_params(); + let TestParams { + mut rng, + ik, + recipient, + .. + } = setup_params(); let asset = AssetBase::derive(&ik, note1_asset_desc); let note2_asset = note2_asset_desc.map_or(asset, |desc| AssetBase::derive(&ik, desc)); @@ -853,7 +903,14 @@ mod tests { [u8; 32], Nullifier, ) { - let (mut rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + mut rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let note1 = Note::new( recipient, @@ -880,69 +937,70 @@ mod tests { } #[test] - fn verify_supply_valid() { - let (ik, test_asset, action) = supply_test_params(10, 20, b"Asset 1", None, false); + fn action_verify_valid() { + let (ik, test_asset, action) = action_verify_test_params(10, 20, b"Asset 1", None, false); - let result = action.verify_supply(&ik); + let result = action.verify(&ik); assert!(result.is_ok()); - let (asset, supply) = result.unwrap(); + let (asset, amount) = result.unwrap(); assert_eq!(asset, test_asset); - assert_eq!(supply.amount, NoteValue::from_raw(30)); - assert!(!supply.is_finalized); + assert_eq!(amount, NoteValue::from_raw(30)); + assert!(!action.is_finalized()); } #[test] - fn verify_supply_invalid_for_asset_base_as_identity() { + fn action_verify_invalid_for_asset_base_as_identity() { let (_, bundle, _, _) = identity_point_test_params(10, 20); assert_eq!( - bundle.actions.head.verify_supply(&bundle.ik), + bundle.actions.head.verify(&bundle.ik), Err(AssetBaseCannotBeIdentityPoint) ); } #[test] - fn verify_supply_finalized() { - let (ik, test_asset, action) = supply_test_params(10, 20, b"Asset 1", None, true); + fn action_verify_finalized() { + let (ik, test_asset, action) = action_verify_test_params(10, 20, b"Asset 1", None, true); - let result = action.verify_supply(&ik); + let result = action.verify(&ik); assert!(result.is_ok()); - let (asset, supply) = result.unwrap(); + let (asset, amount) = result.unwrap(); assert_eq!(asset, test_asset); - assert_eq!(supply.amount, NoteValue::from_raw(30)); - assert!(supply.is_finalized); + assert_eq!(amount, NoteValue::from_raw(30)); + assert!(action.is_finalized()); } #[test] - fn verify_supply_incorrect_asset_base() { - let (ik, _, action) = supply_test_params(10, 20, b"Asset 1", Some(b"Asset 2"), false); + fn action_verify_incorrect_asset_base() { + let (ik, _, action) = + action_verify_test_params(10, 20, b"Asset 1", Some(b"Asset 2"), false); - assert_eq!( - action.verify_supply(&ik), - Err(IssueBundleIkMismatchAssetBase) - ); + assert_eq!(action.verify(&ik), Err(IssueBundleIkMismatchAssetBase)); } #[test] - fn verify_supply_ik_mismatch_asset_base() { - let (_, _, action) = supply_test_params(10, 20, b"Asset 1", None, false); - let (_, _, ik, _, _, _) = setup_params(); + fn action_verify_ik_mismatch_asset_base() { + let (_, _, action) = action_verify_test_params(10, 20, b"Asset 1", None, false); + let TestParams { ik, .. } = setup_params(); - assert_eq!( - action.verify_supply(&ik), - Err(IssueBundleIkMismatchAssetBase) - ); + assert_eq!(action.verify(&ik), Err(IssueBundleIkMismatchAssetBase)); } #[test] fn issue_bundle_basic() { - let (rng, _, ik, recipient, _, first_nullifier) = setup_params(); + let TestParams { + rng, + ik, + recipient, + first_nullifier, + .. + } = setup_params(); let str = "Halo".to_string(); let str2 = "Halo2".to_string(); @@ -1058,7 +1116,9 @@ mod tests { #[test] fn issue_bundle_finalize_asset() { - let (rng, _, ik, recipient, _, _) = setup_params(); + let TestParams { + rng, ik, recipient, .. + } = setup_params(); let (mut bundle, _) = IssueBundle::new( ik, @@ -1091,7 +1151,14 @@ mod tests { #[test] fn issue_bundle_prepare() { - let (rng, _, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + rng, + ik, + recipient, + sighash, + first_nullifier, + .. + } = setup_params(); let (bundle, _) = IssueBundle::new( ik, @@ -1111,7 +1178,14 @@ mod tests { #[test] fn issue_bundle_sign() { - let (rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let (bundle, _) = IssueBundle::new( ik.clone(), @@ -1137,7 +1211,13 @@ mod tests { #[test] fn issue_bundle_invalid_isk_for_signature() { - let (rng, _, ik, recipient, _, first_nullifier) = setup_params(); + let TestParams { + rng, + ik, + recipient, + first_nullifier, + .. + } = setup_params(); let (bundle, _) = IssueBundle::new( ik, @@ -1164,7 +1244,14 @@ mod tests { #[test] fn issue_bundle_incorrect_asset_for_signature() { - let (mut rng, isk, ik, recipient, _, first_nullifier) = setup_params(); + let TestParams { + mut rng, + isk, + ik, + recipient, + first_nullifier, + .. + } = setup_params(); // Create a bundle with "normal" note let (mut bundle, _) = IssueBundle::new( @@ -1200,10 +1287,17 @@ mod tests { #[test] fn issue_bundle_verify() { - let (rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let (bundle, _) = IssueBundle::new( - ik, + ik.clone(), b"Verify".to_vec(), Some(IssueInfo { recipient, @@ -1219,18 +1313,29 @@ mod tests { .prepare(sighash) .sign(&isk) .unwrap(); - let prev_finalized = &mut HashSet::new(); - let supply_info = verify_issue_bundle(&signed, sighash, prev_finalized).unwrap(); + let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); - supply_info.update_finalization_set(prev_finalized); - - assert!(prev_finalized.is_empty()); + let first_note = *signed.actions().first().notes().first().unwrap(); + assert_eq!( + issued_assets, + HashMap::from([( + AssetBase::derive(&ik, b"Verify"), + AssetRecord::new(NoteValue::from_raw(5), false, first_note) + )]) + ); } #[test] fn issue_bundle_verify_with_finalize() { - let (rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let (mut bundle, _) = IssueBundle::new( ik.clone(), @@ -1251,23 +1356,33 @@ mod tests { .prepare(sighash) .sign(&isk) .unwrap(); - let prev_finalized = &mut HashSet::new(); - - let supply_info = verify_issue_bundle(&signed, sighash, prev_finalized).unwrap(); - supply_info.update_finalization_set(prev_finalized); + let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); - assert_eq!(prev_finalized.len(), 1); - assert!(prev_finalized.contains(&AssetBase::derive(&ik, b"Verify with finalize"))); + let first_note = *signed.actions().first().notes().first().unwrap(); + assert_eq!( + issued_assets, + HashMap::from([( + AssetBase::derive(&ik, b"Verify with finalize"), + AssetRecord::new(NoteValue::from_raw(7), true, first_note) + )]) + ); } #[test] - fn issue_bundle_verify_with_supply_info() { - let (rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + fn issue_bundle_verify_with_issued_assets() { + let TestParams { + rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); - let asset1_desc = b"Verify with supply info 1".to_vec(); - let asset2_desc = b"Verify with supply info 2".to_vec(); - let asset3_desc = b"Verify with supply info 3".to_vec(); + let asset1_desc = b"Verify with issued assets 1".to_vec(); + let asset2_desc = b"Verify with issued assets 2".to_vec(); + let asset3_desc = b"Verify with issued assets 3".to_vec(); let asset1_base = AssetBase::derive(&ik, &asset1_desc); let asset2_base = AssetBase::derive(&ik, &asset2_desc); @@ -1306,53 +1421,51 @@ mod tests { .prepare(sighash) .sign(&isk) .unwrap(); - let prev_finalized = &mut HashSet::new(); - let supply_info = verify_issue_bundle(&signed, sighash, prev_finalized).unwrap(); + let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); - supply_info.update_finalization_set(prev_finalized); - - assert_eq!(prev_finalized.len(), 2); - - assert!(prev_finalized.contains(&asset1_base)); - assert!(prev_finalized.contains(&asset2_base)); - assert!(!prev_finalized.contains(&asset3_base)); - - assert_eq!(supply_info.assets.len(), 3); + assert_eq!(issued_assets.keys().len(), 3); let reference_note1 = signed.actions()[0].notes()[0]; let reference_note2 = signed.actions()[1].notes()[0]; let reference_note3 = signed.actions()[2].notes()[0]; assert_eq!( - supply_info.assets.get(&asset1_base), - Some(&AssetSupply::new( + issued_assets.get(&asset1_base), + Some(&AssetRecord::new( NoteValue::from_raw(15), true, - Some(reference_note1) + reference_note1 )) ); assert_eq!( - supply_info.assets.get(&asset2_base), - Some(&AssetSupply::new( + issued_assets.get(&asset2_base), + Some(&AssetRecord::new( NoteValue::from_raw(10), true, - Some(reference_note2) + reference_note2 )) ); assert_eq!( - supply_info.assets.get(&asset3_base), - Some(&AssetSupply::new( + issued_assets.get(&asset3_base), + Some(&AssetRecord::new( NoteValue::from_raw(5), false, - Some(reference_note3) + reference_note3 )) ); } #[test] fn issue_bundle_verify_fail_previously_finalized() { - let (rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + mut rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let (bundle, _) = IssueBundle::new( ik.clone(), @@ -1371,15 +1484,30 @@ mod tests { .prepare(sighash) .sign(&isk) .unwrap(); - let prev_finalized = &mut HashSet::new(); let final_type = AssetBase::derive(&ik, b"already final"); - prev_finalized.insert(final_type); + let issued_assets = [( + final_type, + AssetRecord::new( + NoteValue::from_raw(20), + true, + Note::new( + recipient, + NoteValue::from_raw(10), + final_type, + Rho::zero(), + &mut rng, + ), + ), + )] + .into_iter() + .collect::>(); assert_eq!( - verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(), - IssueActionPreviouslyFinalizedAssetBase(final_type) + verify_issue_bundle(&signed, sighash, |asset| issued_assets.get(asset).copied()) + .unwrap_err(), + IssueActionPreviouslyFinalizedAssetBase ); } @@ -1392,7 +1520,14 @@ mod tests { } } - let (rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let (bundle, _) = IssueBundle::new( ik, @@ -1418,17 +1553,23 @@ mod tests { signature: wrong_isk.try_sign(&sighash).unwrap(), }); - let prev_finalized = &HashSet::new(); - assert_eq!( - verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), IssueBundleInvalidSignature ); } #[test] fn issue_bundle_verify_fail_wrong_sighash() { - let (rng, isk, ik, recipient, random_sighash, first_nullifier) = setup_params(); + let TestParams { + rng, + isk, + ik, + recipient, + sighash: random_sighash, + first_nullifier, + } = setup_params(); + let (bundle, _) = IssueBundle::new( ik, b"Asset description".to_vec(), @@ -1447,17 +1588,23 @@ mod tests { .prepare(sighash) .sign(&isk) .unwrap(); - let prev_finalized = &HashSet::new(); assert_eq!( - verify_issue_bundle(&signed, random_sighash, prev_finalized).unwrap_err(), + verify_issue_bundle(&signed, random_sighash, |_| None).unwrap_err(), IssueBundleInvalidSignature ); } #[test] fn issue_bundle_verify_fail_incorrect_asset_description() { - let (mut rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + mut rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let (bundle, _) = IssueBundle::new( ik, @@ -1488,10 +1635,8 @@ mod tests { signed.actions.first_mut().notes.push(note); - let prev_finalized = &HashSet::new(); - assert_eq!( - verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), IssueBundleIkMismatchAssetBase ); } @@ -1500,7 +1645,14 @@ mod tests { fn issue_bundle_verify_fail_incorrect_ik() { let asset_description = b"Asset".to_vec(); - let (mut rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + mut rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let (bundle, _) = IssueBundle::new( ik, @@ -1534,10 +1686,8 @@ mod tests { signed.actions.first_mut().notes = vec![note]; - let prev_finalized = &HashSet::new(); - assert_eq!( - verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), IssueBundleIkMismatchAssetBase ); } @@ -1551,7 +1701,14 @@ mod tests { } } - let (rng, isk, ik, recipient, sighash, first_nullifier) = setup_params(); + let TestParams { + rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); let (bundle, _) = IssueBundle::new( ik, @@ -1570,13 +1727,12 @@ mod tests { .prepare(sighash) .sign(&isk) .unwrap(); - let prev_finalized = HashSet::new(); // 1. Try a description that is too long signed.actions.first_mut().modify_descr(vec![b'X'; 513]); assert_eq!( - verify_issue_bundle(&signed, sighash, &prev_finalized).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), WrongAssetDescSize ); @@ -1584,7 +1740,7 @@ mod tests { signed.actions.first_mut().modify_descr(b"".to_vec()); assert_eq!( - verify_issue_bundle(&signed, sighash, &prev_finalized).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), WrongAssetDescSize ); } @@ -1608,7 +1764,7 @@ mod tests { let (isk, bundle, sighash, _) = identity_point_test_params(10, 20); let signed = IssueBundle { - ik: bundle.ik, + ik: bundle.ik().clone(), actions: bundle.actions, authorization: Signed { signature: isk.try_sign(&sighash).unwrap(), @@ -1616,7 +1772,7 @@ mod tests { }; assert_eq!( - verify_issue_bundle(&signed, sighash, &HashSet::new()).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), AssetBaseCannotBeIdentityPoint ); } @@ -1640,7 +1796,9 @@ mod tests { #[test] fn issue_bundle_asset_desc_roundtrip() { - let (rng, _, ik, recipient, _, _) = setup_params(); + let TestParams { + rng, ik, recipient, .. + } = setup_params(); // Generated using https://onlinetools.com/utf8/generate-random-utf8 let asset_desc_1 = "󅞞 򬪗YV8𱈇m0{둛򙎠[㷊V֤]9Ծ̖l󾓨2닯򗏟iȰ䣄˃Oߺ񗗼🦄" diff --git a/src/lib.rs b/src/lib.rs index 543c91825..52387f82c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ mod action; mod address; +pub mod asset_record; pub mod builder; pub mod bundle; pub mod circuit; @@ -29,7 +30,6 @@ pub mod note; pub mod orchard_flavor; pub mod primitives; mod spec; -pub mod supply_info; pub mod tree; pub mod value; pub mod zip32; diff --git a/src/supply_info.rs b/src/supply_info.rs deleted file mode 100644 index 6444201ec..000000000 --- a/src/supply_info.rs +++ /dev/null @@ -1,207 +0,0 @@ -//! Structs and logic related to supply information management for assets. - -use std::collections::{hash_map, HashMap, HashSet}; - -use crate::{issuance::Error, note::AssetBase, value::NoteValue, Note}; - -/// Represents the amount of an asset, its finalization status and reference note. -#[derive(Debug, Clone, Copy)] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub struct AssetSupply { - /// The amount of the asset. - pub amount: NoteValue, - - /// Whether or not the asset is finalized. - pub is_finalized: bool, - - /// The reference note, `None` if this `AssetSupply` instance is created from an issue bundle that does not include - /// a reference note (a non-first issuance) - pub reference_note: Option, -} - -impl AssetSupply { - /// Creates a new AssetSupply instance with the given amount, finalization status and reference - /// note. - pub fn new(amount: NoteValue, is_finalized: bool, reference_note: Option) -> Self { - Self { - amount, - is_finalized, - reference_note, - } - } -} - -/// Contains information about the supply of assets. -#[derive(Debug, Clone)] -pub struct SupplyInfo { - /// A map of asset bases to their respective supply information. - pub assets: HashMap, -} - -impl SupplyInfo { - /// Creates a new, empty `SupplyInfo` instance. - pub fn new() -> Self { - Self { - assets: HashMap::new(), - } - } - - /// Inserts or updates an asset's supply information in the supply info map. - /// If the asset exists, adds the amounts (unconditionally) and updates the finalization status - /// (only if the new supply is finalized). If the asset is not found, inserts the new supply. - pub fn add_supply(&mut self, asset: AssetBase, new_supply: AssetSupply) -> Result<(), Error> { - match self.assets.entry(asset) { - hash_map::Entry::Occupied(entry) => { - let supply = entry.into_mut(); - supply.amount = (supply.amount + new_supply.amount).ok_or(Error::ValueOverflow)?; - supply.is_finalized |= new_supply.is_finalized; - supply.reference_note = supply.reference_note.or(new_supply.reference_note); - } - hash_map::Entry::Vacant(entry) => { - entry.insert(new_supply); - } - } - - Ok(()) - } - - /// Updates the set of finalized assets based on the supply information stored in - /// the `SupplyInfo` instance. - pub fn update_finalization_set(&self, finalization_set: &mut HashSet) { - finalization_set.extend( - self.assets - .iter() - .filter_map(|(asset, supply)| supply.is_finalized.then_some(asset)), - ); - } -} - -impl Default for SupplyInfo { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn create_test_asset(asset_desc: &[u8]) -> AssetBase { - use crate::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey}; - - let isk = IssuanceAuthorizingKey::from_bytes([1u8; 32]).unwrap(); - - AssetBase::derive(&IssuanceValidatingKey::from(&isk), asset_desc) - } - - fn sum<'a, T: IntoIterator>(supplies: T) -> Option { - supplies - .into_iter() - .map(|supply| supply.amount) - .try_fold(NoteValue::from_raw(0), |sum, value| sum + value) - } - - #[test] - fn test_add_supply_valid() { - let mut supply_info = SupplyInfo::new(); - - let asset1 = create_test_asset(b"Asset 1"); - let asset2 = create_test_asset(b"Asset 2"); - - let supply1 = AssetSupply::new(NoteValue::from_raw(20), false, None); - let supply2 = AssetSupply::new(NoteValue::from_raw(30), true, None); - let supply3 = AssetSupply::new(NoteValue::from_raw(10), false, None); - let supply4 = AssetSupply::new(NoteValue::from_raw(10), true, None); - let supply5 = AssetSupply::new(NoteValue::from_raw(50), false, None); - - assert_eq!(supply_info.assets.len(), 0); - - // Add supply1 - assert!(supply_info.add_supply(asset1, supply1).is_ok()); - assert_eq!(supply_info.assets.len(), 1); - assert_eq!( - supply_info.assets.get(&asset1), - Some(&AssetSupply::new(sum([&supply1]).unwrap(), false, None)) - ); - - // Add supply2 - assert!(supply_info.add_supply(asset1, supply2).is_ok()); - assert_eq!(supply_info.assets.len(), 1); - assert_eq!( - supply_info.assets.get(&asset1), - Some(&AssetSupply::new( - sum([&supply1, &supply2]).unwrap(), - true, - None - )) - ); - - // Add supply3 - assert!(supply_info.add_supply(asset1, supply3).is_ok()); - assert_eq!(supply_info.assets.len(), 1); - assert_eq!( - supply_info.assets.get(&asset1), - Some(&AssetSupply::new( - sum([&supply1, &supply2, &supply3]).unwrap(), - true, - None - )) - ); - - // Add supply4 - assert!(supply_info.add_supply(asset1, supply4).is_ok()); - assert_eq!(supply_info.assets.len(), 1); - assert_eq!( - supply_info.assets.get(&asset1), - Some(&AssetSupply::new( - sum([&supply1, &supply2, &supply3, &supply4]).unwrap(), - true, - None - )) - ); - - // Add supply5 - assert!(supply_info.add_supply(asset2, supply5).is_ok()); - assert_eq!(supply_info.assets.len(), 2); - assert_eq!( - supply_info.assets.get(&asset1), - Some(&AssetSupply::new( - sum([&supply1, &supply2, &supply3, &supply4]).unwrap(), - true, - None - )) - ); - assert_eq!( - supply_info.assets.get(&asset2), - Some(&AssetSupply::new(sum([&supply5]).unwrap(), false, None)) - ); - } - - #[test] - fn test_update_finalization_set() { - let mut supply_info = SupplyInfo::new(); - - let asset1 = create_test_asset(b"Asset 1"); - let asset2 = create_test_asset(b"Asset 2"); - let asset3 = create_test_asset(b"Asset 3"); - - let supply1 = AssetSupply::new(NoteValue::from_raw(10), false, None); - let supply2 = AssetSupply::new(NoteValue::from_raw(20), true, None); - let supply3 = AssetSupply::new(NoteValue::from_raw(40), false, None); - let supply4 = AssetSupply::new(NoteValue::from_raw(50), true, None); - - assert!(supply_info.add_supply(asset1, supply1).is_ok()); - assert!(supply_info.add_supply(asset1, supply2).is_ok()); - assert!(supply_info.add_supply(asset2, supply3).is_ok()); - assert!(supply_info.add_supply(asset3, supply4).is_ok()); - - let mut finalization_set = HashSet::new(); - - supply_info.update_finalization_set(&mut finalization_set); - - assert_eq!(finalization_set.len(), 2); - - assert!(finalization_set.contains(&asset1)); - assert!(finalization_set.contains(&asset3)); - } -} diff --git a/tests/issuance_global_state.rs b/tests/issuance_global_state.rs new file mode 100644 index 000000000..a6170a21e --- /dev/null +++ b/tests/issuance_global_state.rs @@ -0,0 +1,320 @@ +use std::collections::HashMap; + +use rand::{rngs::OsRng, RngCore}; + +use orchard::{ + asset_record::AssetRecord, + issuance::{ + verify_issue_bundle, + Error::{ + IssueActionPreviouslyFinalizedAssetBase, MissingReferenceNoteOnFirstIssuance, + ValueOverflow, + }, + IssueBundle, IssueInfo, Signed, + }, + keys::{FullViewingKey, IssuanceAuthorizingKey, IssuanceValidatingKey, Scope, SpendingKey}, + note::{AssetBase, Nullifier}, + value::NoteValue, + Address, Note, +}; + +fn random_bytes(mut rng: OsRng) -> [u8; N] { + let mut bytes = [0; N]; + rng.fill_bytes(&mut bytes); + bytes +} + +#[derive(Clone)] +struct TestParams { + rng: OsRng, + isk: IssuanceAuthorizingKey, + ik: IssuanceValidatingKey, + recipient: Address, + sighash: [u8; 32], + first_nullifier: Nullifier, +} + +// For testing global state only - should not be used in an actual setting. +fn setup_params() -> TestParams { + use group::{ff::PrimeField, Curve, Group}; + use pasta_curves::{arithmetic::CurveAffine, pallas}; + + let rng = OsRng; + + let isk = IssuanceAuthorizingKey::from_bytes(random_bytes(rng)).unwrap(); + let ik: IssuanceValidatingKey = (&isk).into(); + + let fvk = FullViewingKey::from(&SpendingKey::from_bytes(random_bytes(rng)).unwrap()); + let recipient = fvk.address_at(0u32, Scope::External); + + let sighash = random_bytes(rng); + + // For testing purposes only: replicate the behavior of orchard's `Nullifier::dummy` + // and `extract_p` functions, which are marked as `pub(crate)` in orchard and are therefore + // not visible here. + let first_nullifier = { + let point = pallas::Point::random(rng); + + let base = point + .to_affine() + .coordinates() + .map(|c| *c.x()) + .unwrap_or_else(pallas::Base::zero); + + Nullifier::from_bytes(&base.to_repr()).unwrap() + }; + + TestParams { + rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } +} + +fn build_state_entry( + asset_base: &AssetBase, + amount: u64, + is_finalized: bool, + reference_note: &Note, +) -> (AssetBase, AssetRecord) { + ( + *asset_base, + AssetRecord::new(NoteValue::from_raw(amount), is_finalized, *reference_note), + ) +} + +#[derive(Clone)] +struct IssueTestNote { + asset_desc: Vec, + amount: u64, + is_finalized: bool, + first_issuance: bool, +} + +impl IssueTestNote { + fn new(asset_desc: &Vec, amount: u64, is_finalized: bool, first_issuance: bool) -> Self { + Self { + asset_desc: asset_desc.clone(), + amount, + is_finalized, + first_issuance, + } + } +} + +fn get_first_note(bundle: &IssueBundle, action_index: usize) -> &Note { + bundle.actions()[action_index].notes().first().unwrap() +} + +fn build_issue_bundle(params: &TestParams, data: &[IssueTestNote]) -> IssueBundle { + let TestParams { + rng, + ref isk, + ref ik, + recipient, + sighash, + ref first_nullifier, + } = *params; + + let IssueTestNote { + asset_desc, + amount, + is_finalized, + first_issuance, + } = data.first().unwrap().clone(); + + let (mut bundle, _) = IssueBundle::new( + ik.clone(), + asset_desc.clone(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(amount), + }), + first_issuance, + rng, + ) + .unwrap(); + + if is_finalized { + bundle.finalize_action(&asset_desc).unwrap(); + } + + for IssueTestNote { + asset_desc, + amount, + is_finalized, + first_issuance, + } in data.into_iter().skip(1).cloned() + { + bundle + .add_recipient( + &asset_desc, + recipient, + NoteValue::from_raw(amount), + first_issuance, + rng, + ) + .unwrap(); + + if is_finalized { + bundle.finalize_action(&asset_desc).unwrap(); + } + } + + bundle + .update_rho(&first_nullifier) + .prepare(sighash) + .sign(&isk) + .unwrap() +} + +// Issuance workflow test: performs a series of bundle creations and verifications, +// with a global state simulation +#[test] +fn issue_bundle_verify_with_global_state() { + let params = setup_params(); + + let TestParams { ik, sighash, .. } = params.clone(); + + let asset1_desc = b"Verify with issued assets 1".to_vec(); + let asset2_desc = b"Verify with issued assets 2".to_vec(); + let asset3_desc = b"Verify with issued assets 3".to_vec(); + let asset4_desc = b"Verify with issued assets 4".to_vec(); + + let asset1_base = AssetBase::derive(&ik, &asset1_desc); + let asset2_base = AssetBase::derive(&ik, &asset2_desc); + let asset3_base = AssetBase::derive(&ik, &asset3_desc); + let asset4_base = AssetBase::derive(&ik, &asset4_desc); + + let mut global_state = HashMap::new(); + + // We'll issue and verify a series of bundles. For valid bundles, the global + // state is updated and must match the expected result. For invalid bundles, + // we check the expected error, leaving the state unchanged. + + // ** Bundle1 (valid) ** + + let bundle1 = build_issue_bundle( + ¶ms, + &[ + IssueTestNote::new(&asset1_desc, 7, false, true), + IssueTestNote::new(&asset1_desc, 8, false, false), + IssueTestNote::new(&asset2_desc, 10, true, true), + IssueTestNote::new(&asset3_desc, 5, false, true), + ], + ); + + let expected_global_state1 = HashMap::from([ + build_state_entry(&asset1_base, 15, false, get_first_note(&bundle1, 0)), + build_state_entry(&asset2_base, 10, true, get_first_note(&bundle1, 1)), + build_state_entry(&asset3_base, 5, false, get_first_note(&bundle1, 2)), + ]); + + global_state.extend( + verify_issue_bundle(&bundle1, sighash, |asset| global_state.get(asset).cloned()).unwrap(), + ); + assert_eq!(global_state, expected_global_state1); + + // ** Bundle2 (valid) ** + + let bundle2 = build_issue_bundle( + ¶ms, + &[ + IssueTestNote::new(&asset1_desc, 3, true, true), + IssueTestNote::new(&asset3_desc, 20, false, false), + ], + ); + + let expected_global_state2 = HashMap::from([ + build_state_entry(&asset1_base, 18, true, get_first_note(&bundle1, 0)), + build_state_entry(&asset2_base, 10, true, get_first_note(&bundle1, 1)), + build_state_entry(&asset3_base, 25, false, get_first_note(&bundle1, 2)), + ]); + + global_state.extend( + verify_issue_bundle(&bundle2, sighash, |asset| global_state.get(asset).cloned()).unwrap(), + ); + assert_eq!(global_state, expected_global_state2); + + // ** Bundle3 (invalid) ** + + let bundle3 = build_issue_bundle( + ¶ms, + &[ + IssueTestNote::new(&asset1_desc, 3, false, true), + IssueTestNote::new(&asset3_desc, 20, false, false), + ], + ); + + let expected_global_state3 = expected_global_state2; + + assert_eq!( + verify_issue_bundle(&bundle3, sighash, |asset| global_state.get(asset).cloned()) + .unwrap_err(), + IssueActionPreviouslyFinalizedAssetBase, + ); + assert_eq!(global_state, expected_global_state3); + + // ** Bundle4 (invalid) ** + + let bundle4 = build_issue_bundle( + ¶ms, + &[ + IssueTestNote::new(&asset3_desc, 50, true, true), + IssueTestNote::new(&asset4_desc, 77, false, false), + ], + ); + + let expected_global_state4 = expected_global_state3; + + assert_eq!( + verify_issue_bundle(&bundle4, sighash, |asset| global_state.get(asset).cloned()) + .unwrap_err(), + MissingReferenceNoteOnFirstIssuance, + ); + assert_eq!(global_state, expected_global_state4); + + // ** Bundle5 (invalid) ** + + let bundle5 = build_issue_bundle( + ¶ms, + &[ + IssueTestNote::new(&asset3_desc, u64::MAX - 20, true, true), + IssueTestNote::new(&asset4_desc, 77, false, true), + ], + ); + + let expected_global_state5 = expected_global_state4; + + assert_eq!( + verify_issue_bundle(&bundle5, sighash, |asset| global_state.get(asset).cloned()) + .unwrap_err(), + ValueOverflow, + ); + assert_eq!(global_state, expected_global_state5); + + // ** Bundle6 (valid) ** + + let bundle6 = build_issue_bundle( + ¶ms, + &[ + IssueTestNote::new(&asset3_desc, 50, true, true), + IssueTestNote::new(&asset4_desc, 77, false, true), + ], + ); + + let expected_global_state6 = HashMap::from([ + build_state_entry(&asset1_base, 18, true, get_first_note(&bundle1, 0)), + build_state_entry(&asset2_base, 10, true, get_first_note(&bundle1, 1)), + build_state_entry(&asset3_base, 75, true, get_first_note(&bundle1, 2)), + build_state_entry(&asset4_base, 77, false, get_first_note(&bundle6, 1)), + ]); + + global_state.extend( + verify_issue_bundle(&bundle6, sighash, |asset| global_state.get(asset).cloned()).unwrap(), + ); + assert_eq!(global_state, expected_global_state6); +} diff --git a/tests/zsa.rs b/tests/zsa.rs index 73a3a5d20..337c6fbf8 100644 --- a/tests/zsa.rs +++ b/tests/zsa.rs @@ -19,7 +19,6 @@ use orchard::{ Address, Anchor, Bundle, Note, }; use rand::rngs::OsRng; -use std::collections::HashSet; use zcash_note_encryption_zsa::try_note_decryption; #[derive(Debug)] @@ -183,12 +182,7 @@ fn issue_zsa_notes( AssetBase::derive(&keys.ik().clone(), asset_descr), ); - assert!(verify_issue_bundle( - &issue_bundle, - issue_bundle.commitment().into(), - &HashSet::new(), - ) - .is_ok()); + assert!(verify_issue_bundle(&issue_bundle, issue_bundle.commitment().into(), |_| None).is_ok()); (*reference_note, *note1, *note2) }