diff --git a/src/bundle.rs b/src/bundle.rs index 4f67591a8..3d90b4de9 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -1,7 +1,6 @@ //! Structs related to bundles of Orchard actions. mod batch; -pub mod burn_validation; pub mod commitments; pub use batch::BatchValidator; diff --git a/src/issuance.rs b/src/issuance.rs index 78832fe7c..4fba8bf8f 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -4,7 +4,6 @@ use group::Group; use k256::schnorr; use nonempty::NonEmpty; use rand::RngCore; -use std::collections::HashSet; use std::fmt; use crate::bundle::commitments::{hash_issue_bundle_auth_data, hash_issue_bundle_txid_data}; @@ -21,8 +20,6 @@ use crate::note::{AssetBase, Nullifier, Rho}; use crate::value::NoteValue; use crate::{Address, Note}; -use crate::supply_info::{AssetSupply, SupplyInfo}; - /// Checks if a given note is a reference note. /// /// A reference note satisfies the following conditions: @@ -32,15 +29,45 @@ fn is_reference_note(note: &Note) -> bool { note.value() == NoteValue::zero() && note.recipient() == ReferenceKeys::recipient() } +/// Represents the amount of an asset, its finalization status and reference note. +#[derive(Debug, Clone, Copy, Default)] +#[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, + } + } +} + /// A bundle of actions to be applied to the ledger. #[derive(Debug, Clone, PartialEq, Eq)] pub struct IssueBundle { /// The issuer key for the note being created. - ik: IssuanceValidatingKey, + // FIXME: pub(crate) is needed for verification/issuance.rs tests only + pub(crate) ik: IssuanceValidatingKey, /// The list of issue actions that make up this bundle. - actions: NonEmpty, + // FIXME: pub(crate) is needed for verification/issuance.rs tests only + pub(crate) actions: NonEmpty, /// The authorization for this action. - authorization: T, + // FIXME: pub(crate) is needed for verification/issuance.rs tests only + pub(crate) authorization: T, } /// An issue action applied to the global ledger. @@ -49,9 +76,11 @@ pub struct IssueBundle { #[derive(Debug, Clone, PartialEq, Eq)] pub struct IssueAction { /// Asset description for verification. - asset_desc: Vec, + // FIXME: pub(crate) is needed for verification/issuance.rs tests only + pub(crate) asset_desc: Vec, /// The newly issued notes. - notes: Vec, + // FIXME: pub(crate) is needed for verification/issuance.rs tests only + pub(crate) notes: Vec, /// `finalize` will prevent further issuance of the same asset type. finalize: bool, } @@ -129,7 +158,10 @@ impl IssueAction { /// `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> { + pub(crate) fn verify_supply( + &self, + ik: &IssuanceValidatingKey, + ) -> Result<(AssetBase, AssetSupply), Error> { if self.notes.is_empty() && !self.is_finalized() { return Err(IssueActionWithoutNoteNotFinalized); } @@ -203,7 +235,8 @@ pub struct Prepared { /// Marker for an authorized bundle. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Signed { - signature: schnorr::Signature, + // FIXME: pub(crate) is needed for verification/issuance.rs tests only + pub(crate) signature: schnorr::Signature, } impl Signed { @@ -553,74 +586,6 @@ impl IssueBundle { } } -/// Validation for Orchard IssueBundles -/// -/// A set of previously finalized asset types must be provided in `finalized` argument. -/// -/// 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`. -/// -/// # 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). -/// -/// # 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`. -pub fn verify_issue_bundle( - bundle: &IssueBundle, - sighash: [u8; 32], - finalized: &HashSet, // The finalization set. -) -> Result { - bundle - .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)); - } - - supply_info.add_supply(asset, supply)?; - - Ok(supply_info) - })?; - - Ok(supply_info) -} - /// Errors produced during the issuance process #[derive(Debug, PartialEq, Eq)] pub enum Error { @@ -688,17 +653,18 @@ impl fmt::Display for Error { } } +// FIXME: Place setup_params and identity_point_test_params somewhere in a common +// test module (to use it from verification/issuance.rs tests), and remove +// pub(crate) before mod tests and before setup_params and identity_point_test_params +// here. #[cfg(test)] -mod tests { - use super::{AssetSupply, IssueBundle, IssueInfo}; +pub(crate) mod tests { + use super::{IssueBundle, IssueInfo}; use crate::issuance::Error::{ - AssetBaseCannotBeIdentityPoint, IssueActionNotFound, - IssueActionPreviouslyFinalizedAssetBase, IssueBundleIkMismatchAssetBase, - IssueBundleInvalidSignature, WrongAssetDescSize, - }; - use crate::issuance::{ - is_reference_note, verify_issue_bundle, IssueAction, Signed, Unauthorized, + AssetBaseCannotBeIdentityPoint, IssueActionNotFound, IssueBundleIkMismatchAssetBase, + WrongAssetDescSize, }; + use crate::issuance::{is_reference_note, IssueAction, Unauthorized}; use crate::keys::{ FullViewingKey, IssuanceAuthorizingKey, IssuanceValidatingKey, Scope, SpendingKey, }; @@ -710,7 +676,6 @@ mod tests { use pasta_curves::pallas::{Point, Scalar}; use rand::rngs::OsRng; use rand::RngCore; - use std::collections::HashSet; /// Validation for reference note /// @@ -723,7 +688,7 @@ mod tests { assert_eq!(note.asset(), asset); } - fn setup_params() -> ( + pub(crate) fn setup_params() -> ( OsRng, IssuanceAuthorizingKey, IssuanceValidatingKey, @@ -794,7 +759,7 @@ mod tests { /// This function generates two notes with the identity point as their asset base, /// and returns the issuance authorizing key, an unauthorized issue bundle containing /// the notes, and a sighash - fn identity_point_test_params( + pub(crate) fn identity_point_test_params( note1_value: u64, note2_value: u64, ) -> (IssuanceAuthorizingKey, IssueBundle, [u8; 32]) { @@ -1121,389 +1086,6 @@ mod tests { assert_eq!(err, IssueBundleIkMismatchAssetBase); } - #[test] - fn issue_bundle_verify() { - let (rng, isk, ik, recipient, sighash) = setup_params(); - - let (bundle, _) = IssueBundle::new( - ik, - b"Verify".to_vec(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(5), - }), - true, - rng, - ) - .unwrap(); - - let signed = bundle.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); - - assert!(prev_finalized.is_empty()); - } - - #[test] - fn issue_bundle_verify_with_finalize() { - let (rng, isk, ik, recipient, sighash) = setup_params(); - - let (mut bundle, _) = IssueBundle::new( - ik.clone(), - b"Verify with finalize".to_vec(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(7), - }), - true, - rng, - ) - .unwrap(); - - bundle.finalize_action(b"Verify with finalize").unwrap(); - - let signed = bundle.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); - - assert_eq!(prev_finalized.len(), 1); - assert!(prev_finalized.contains(&AssetBase::derive(&ik, b"Verify with finalize"))); - } - - #[test] - fn issue_bundle_verify_with_supply_info() { - let (rng, isk, ik, recipient, sighash) = 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_base = AssetBase::derive(&ik, &asset1_desc); - let asset2_base = AssetBase::derive(&ik, &asset2_desc); - let asset3_base = AssetBase::derive(&ik, &asset3_desc); - - let (mut bundle, _) = IssueBundle::new( - ik, - asset1_desc.clone(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(7), - }), - true, - rng, - ) - .unwrap(); - - bundle - .add_recipient(&asset1_desc, recipient, NoteValue::from_raw(8), false, rng) - .unwrap(); - - bundle.finalize_action(&asset1_desc).unwrap(); - - bundle - .add_recipient(&asset2_desc, recipient, NoteValue::from_raw(10), true, rng) - .unwrap(); - - bundle.finalize_action(&asset2_desc).unwrap(); - - bundle - .add_recipient(&asset3_desc, recipient, NoteValue::from_raw(5), true, rng) - .unwrap(); - - let signed = bundle.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); - - 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); - - 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( - NoteValue::from_raw(15), - true, - Some(reference_note1) - )) - ); - assert_eq!( - supply_info.assets.get(&asset2_base), - Some(&AssetSupply::new( - NoteValue::from_raw(10), - true, - Some(reference_note2) - )) - ); - assert_eq!( - supply_info.assets.get(&asset3_base), - Some(&AssetSupply::new( - NoteValue::from_raw(5), - false, - Some(reference_note3) - )) - ); - } - - #[test] - fn issue_bundle_verify_fail_previously_finalized() { - let (rng, isk, ik, recipient, sighash) = setup_params(); - - let (bundle, _) = IssueBundle::new( - ik.clone(), - b"already final".to_vec(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(5), - }), - true, - rng, - ) - .unwrap(); - - let signed = bundle.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); - - assert_eq!( - verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(), - IssueActionPreviouslyFinalizedAssetBase(final_type) - ); - } - - #[test] - fn issue_bundle_verify_fail_bad_signature() { - // we want to inject "bad" signatures for test purposes. - impl IssueBundle { - pub fn set_authorization(&mut self, authorization: Signed) { - self.authorization = authorization; - } - } - - let (rng, isk, ik, recipient, sighash) = setup_params(); - - let (bundle, _) = IssueBundle::new( - ik, - b"bad sig".to_vec(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(5), - }), - true, - rng, - ) - .unwrap(); - - let wrong_isk: IssuanceAuthorizingKey = IssuanceAuthorizingKey::random(); - - let mut signed = bundle.prepare(sighash).sign(&isk).unwrap(); - - signed.set_authorization(Signed { - signature: wrong_isk.try_sign(&sighash).unwrap(), - }); - - let prev_finalized = &HashSet::new(); - - assert_eq!( - verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(), - IssueBundleInvalidSignature - ); - } - - #[test] - fn issue_bundle_verify_fail_wrong_sighash() { - let (rng, isk, ik, recipient, random_sighash) = setup_params(); - let (bundle, _) = IssueBundle::new( - ik, - b"Asset description".to_vec(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(5), - }), - true, - rng, - ) - .unwrap(); - - let sighash: [u8; 32] = bundle.commitment().into(); - let signed = bundle.prepare(sighash).sign(&isk).unwrap(); - let prev_finalized = &HashSet::new(); - - assert_eq!( - verify_issue_bundle(&signed, random_sighash, prev_finalized).unwrap_err(), - IssueBundleInvalidSignature - ); - } - - #[test] - fn issue_bundle_verify_fail_incorrect_asset_description() { - let (mut rng, isk, ik, recipient, sighash) = setup_params(); - - let (bundle, _) = IssueBundle::new( - ik, - b"Asset description".to_vec(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(5), - }), - true, - rng, - ) - .unwrap(); - - let mut signed = bundle.prepare(sighash).sign(&isk).unwrap(); - - // Add "bad" note - let note = Note::new( - recipient, - NoteValue::from_raw(5), - AssetBase::derive(signed.ik(), b"zsa_asset"), - Rho::from_nf_old(Nullifier::dummy(&mut rng)), - &mut rng, - ); - - signed.actions.first_mut().notes.push(note); - - let prev_finalized = &HashSet::new(); - - assert_eq!( - verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(), - IssueBundleIkMismatchAssetBase - ); - } - - #[test] - fn issue_bundle_verify_fail_incorrect_ik() { - let asset_description = b"Asset".to_vec(); - - let (mut rng, isk, ik, recipient, sighash) = setup_params(); - - let (bundle, _) = IssueBundle::new( - ik, - asset_description.clone(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(5), - }), - true, - rng, - ) - .unwrap(); - - let mut signed = bundle.prepare(sighash).sign(&isk).unwrap(); - - let incorrect_isk = IssuanceAuthorizingKey::random(); - let incorrect_ik: IssuanceValidatingKey = (&incorrect_isk).into(); - - // Add "bad" note - let note = Note::new( - recipient, - NoteValue::from_raw(55), - AssetBase::derive(&incorrect_ik, &asset_description), - Rho::from_nf_old(Nullifier::dummy(&mut rng)), - &mut rng, - ); - - signed.actions.first_mut().notes = vec![note]; - - let prev_finalized = &HashSet::new(); - - assert_eq!( - verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(), - IssueBundleIkMismatchAssetBase - ); - } - - #[test] - fn issue_bundle_verify_fail_wrong_asset_descr_size() { - // we want to inject a "malformed" description for test purposes. - impl IssueAction { - pub fn modify_descr(&mut self, new_descr: Vec) { - self.asset_desc = new_descr; - } - } - - let (rng, isk, ik, recipient, sighash) = setup_params(); - - let (bundle, _) = IssueBundle::new( - ik, - b"Asset description".to_vec(), - Some(IssueInfo { - recipient, - value: NoteValue::from_raw(5), - }), - true, - rng, - ) - .unwrap(); - - let mut signed = bundle.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(), - WrongAssetDescSize - ); - - // 2. Try a description that is empty - signed.actions.first_mut().modify_descr(b"".to_vec()); - - assert_eq!( - verify_issue_bundle(&signed, sighash, &prev_finalized).unwrap_err(), - WrongAssetDescSize - ); - } - - #[test] - fn issue_bundle_cannot_be_signed_with_asset_base_identity_point() { - let (isk, bundle, sighash) = identity_point_test_params(10, 20); - - assert_eq!( - bundle.prepare(sighash).sign(&isk).unwrap_err(), - AssetBaseCannotBeIdentityPoint - ); - } - - #[test] - fn issue_bundle_verify_fail_asset_base_identity_point() { - let (isk, bundle, sighash) = identity_point_test_params(10, 20); - - let signed = IssueBundle { - ik: bundle.ik, - actions: bundle.actions, - authorization: Signed { - signature: isk.try_sign(&sighash).unwrap(), - }, - }; - - assert_eq!( - verify_issue_bundle(&signed, sighash, &HashSet::new()).unwrap_err(), - AssetBaseCannotBeIdentityPoint - ); - } - #[test] fn finalize_flag_serialization() { let mut rng = OsRng; diff --git a/src/lib.rs b/src/lib.rs index 543c91825..99f167ef7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,9 +29,9 @@ pub mod note; pub mod orchard_flavor; pub mod primitives; mod spec; -pub mod supply_info; pub mod tree; pub mod value; +pub mod verification; pub mod zip32; #[cfg(test)] 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/src/verification.rs b/src/verification.rs new file mode 100644 index 000000000..bd8ba4117 --- /dev/null +++ b/src/verification.rs @@ -0,0 +1,4 @@ +//! Issuance and burn verification functions + +pub mod burn; +pub mod issuance; diff --git a/src/bundle/burn_validation.rs b/src/verification/burn.rs similarity index 100% rename from src/bundle/burn_validation.rs rename to src/verification/burn.rs diff --git a/src/verification/issuance.rs b/src/verification/issuance.rs new file mode 100644 index 000000000..dfc9a32c5 --- /dev/null +++ b/src/verification/issuance.rs @@ -0,0 +1,506 @@ +//! Issuance verification functions + +use std::collections::HashMap; + +use crate::{ + issuance::{ + AssetSupply, + Error::{ + self, IssueActionPreviouslyFinalizedAssetBase, IssueBundleInvalidSignature, + ValueOverflow, WrongAssetDescSize, + }, + IssueBundle, Signed, + }, + note::{asset_base::is_asset_desc_of_valid_size, AssetBase}, +}; + +/// Validation for Orchard IssueBundles +/// +/// The following checks are performed: +/// - **IssueBundle Verification**: +/// - The signature on the provided `sighash` is verified correctly. +/// - **IssueAction Verification**: +/// - The asset description size is correct. +/// - The `verify_supply` method checks pass, +/// - The new amount does not overflow the previous total supply value. +/// - The `AssetBase` for the `IssueAction` has not been previously finalized. +/// +/// # 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_asset_state` - A closure that takes a reference to an `AssetBase` and returns an +/// `Option`, representing the current state of the asset in the global store of +/// previously issued assets. +/// +/// # Returns +/// +/// A `Result` with a `HashMap` on success, which contains new values for updated or newly added +/// items in the global state. Each key is an `AssetBase`, and the corresponding value is a new +/// (updated) `AssetSupply`. +/// +/// # Errors +/// +/// - `IssueBundleInvalidSignature`: Occurs if the signature verification for the provided `sighash` +/// fails. +/// - `WrongAssetDescSize`: Raised if the asset description size for any asset in the bundle is +/// incorrect. +/// - `ValueOverflow`: Occurs if an overflow happens during the calculation of the total value for +/// the notes. +/// - `IssueActionPreviouslyFinalizedAssetBase`: Occurs if the asset has already been finalized. +/// - **Other Errors**: Any additional errors returned by the `verify_supply` method of `IssueAction` +/// will also be propagated. +pub fn verify_issue_bundle( + bundle: &IssueBundle, + sighash: [u8; 32], + // FIXME: consider using AssetStateStore trait with get_asset method instead + get_asset_state: impl Fn(&AssetBase) -> Option, +) -> Result, Error> { + bundle + .ik() + .verify(&sighash, bundle.authorization().signature()) + .map_err(|_| IssueBundleInvalidSignature)?; + + let verified_asset_states = + bundle + .actions() + .iter() + .try_fold(HashMap::new(), |mut verified_asset_states, action| { + if !is_asset_desc_of_valid_size(action.asset_desc()) { + return Err(WrongAssetDescSize); + } + + let (asset, action_asset_state) = action.verify_supply(bundle.ik())?; + + let old_asset_state = verified_asset_states + .get(&asset) + .cloned() + .or_else(|| get_asset_state(&asset)) + .unwrap_or_default(); + + let amount = + (old_asset_state.amount + action_asset_state.amount).ok_or(ValueOverflow)?; + + let is_finalized = (!old_asset_state.is_finalized) + .then_some(action_asset_state.is_finalized) + .ok_or(IssueActionPreviouslyFinalizedAssetBase(asset))?; + + let reference_note = old_asset_state + .reference_note + .or(action_asset_state.reference_note); + + verified_asset_states.insert( + asset, + AssetSupply::new(amount, is_finalized, reference_note), + ); + + Ok(verified_asset_states) + })?; + + Ok(verified_asset_states) +} + +// FIXME: Add more tests for issued_assets (i.e. "global state") change after verify_issue_bundle +// is called: 1) check for expected output, 2) check for processing of existing assets etc. +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + + use crate::{ + issuance::{ + tests::{identity_point_test_params, setup_params}, + Error::{ + AssetBaseCannotBeIdentityPoint, IssueActionPreviouslyFinalizedAssetBase, + IssueBundleIkMismatchAssetBase, IssueBundleInvalidSignature, WrongAssetDescSize, + }, + IssueAction, IssueInfo, Signed, + }, + keys::{IssuanceAuthorizingKey, IssuanceValidatingKey}, + note::{AssetBase, Nullifier, Rho}, + value::NoteValue, + Note, + }; + + use super::{verify_issue_bundle, AssetSupply, IssueBundle}; + + fn get_finalization_set(issued_assets: &HashMap) -> HashSet { + issued_assets + .iter() + .filter_map(|(asset, asset_supply)| asset_supply.is_finalized.then(|| asset.clone())) + .collect::>() + } + + #[test] + fn issue_bundle_verify() { + let (rng, isk, ik, recipient, sighash) = setup_params(); + + let (bundle, _) = IssueBundle::new( + ik, + b"Verify".to_vec(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(5), + }), + true, + rng, + ) + .unwrap(); + + let signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); + let prev_finalized = get_finalization_set(&issued_assets); + + assert!(prev_finalized.is_empty()); + } + + #[test] + fn issue_bundle_verify_with_finalize() { + let (rng, isk, ik, recipient, sighash) = setup_params(); + + let (mut bundle, _) = IssueBundle::new( + ik.clone(), + b"Verify with finalize".to_vec(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(7), + }), + true, + rng, + ) + .unwrap(); + + bundle.finalize_action(b"Verify with finalize").unwrap(); + + let signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); + let prev_finalized = get_finalization_set(&issued_assets); + + assert_eq!(prev_finalized.len(), 1); + assert!(prev_finalized.contains(&AssetBase::derive(&ik, b"Verify with finalize"))); + } + + #[test] + fn issue_bundle_verify_with_issued_assets() { + let (rng, isk, ik, recipient, sighash) = 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_base = AssetBase::derive(&ik, &asset1_desc); + let asset2_base = AssetBase::derive(&ik, &asset2_desc); + let asset3_base = AssetBase::derive(&ik, &asset3_desc); + + let (mut bundle, _) = IssueBundle::new( + ik, + asset1_desc.clone(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(7), + }), + true, + rng, + ) + .unwrap(); + + bundle + .add_recipient(&asset1_desc, recipient, NoteValue::from_raw(8), false, rng) + .unwrap(); + + bundle.finalize_action(&asset1_desc).unwrap(); + + bundle + .add_recipient(&asset2_desc, recipient, NoteValue::from_raw(10), true, rng) + .unwrap(); + + bundle.finalize_action(&asset2_desc).unwrap(); + + bundle + .add_recipient(&asset3_desc, recipient, NoteValue::from_raw(5), true, rng) + .unwrap(); + + let signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); + let prev_finalized = get_finalization_set(&issued_assets); + + 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!(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!( + issued_assets.get(&asset1_base), + Some(&AssetSupply::new( + NoteValue::from_raw(15), + true, + Some(reference_note1) + )) + ); + assert_eq!( + issued_assets.get(&asset2_base), + Some(&AssetSupply::new( + NoteValue::from_raw(10), + true, + Some(reference_note2) + )) + ); + assert_eq!( + issued_assets.get(&asset3_base), + Some(&AssetSupply::new( + NoteValue::from_raw(5), + false, + Some(reference_note3) + )) + ); + } + + #[test] + fn issue_bundle_verify_fail_previously_finalized() { + let (rng, isk, ik, recipient, sighash) = setup_params(); + + let (bundle, _) = IssueBundle::new( + ik.clone(), + b"already final".to_vec(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(5), + }), + true, + rng, + ) + .unwrap(); + + let signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + let final_type = AssetBase::derive(&ik, b"already final"); + + let issued_assets = [( + final_type, + AssetSupply::new(NoteValue::from_raw(0), true, None), + )] + .into_iter() + .collect::>(); + + assert_eq!( + verify_issue_bundle(&signed, sighash, |asset| issued_assets.get(asset).copied()) + .unwrap_err(), + IssueActionPreviouslyFinalizedAssetBase(final_type) + ); + } + + #[test] + fn issue_bundle_verify_fail_bad_signature() { + // we want to inject "bad" signatures for test purposes. + impl IssueBundle { + pub fn set_authorization(&mut self, authorization: Signed) { + self.authorization = authorization; + } + } + + let (rng, isk, ik, recipient, sighash) = setup_params(); + + let (bundle, _) = IssueBundle::new( + ik, + b"bad sig".to_vec(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(5), + }), + true, + rng, + ) + .unwrap(); + + let wrong_isk: IssuanceAuthorizingKey = IssuanceAuthorizingKey::random(); + + let mut signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + signed.set_authorization(Signed { + signature: wrong_isk.try_sign(&sighash).unwrap(), + }); + + assert_eq!( + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), + IssueBundleInvalidSignature + ); + } + + #[test] + fn issue_bundle_verify_fail_wrong_sighash() { + let (rng, isk, ik, recipient, random_sighash) = setup_params(); + let (bundle, _) = IssueBundle::new( + ik, + b"Asset description".to_vec(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(5), + }), + true, + rng, + ) + .unwrap(); + + let sighash: [u8; 32] = bundle.commitment().into(); + let signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + assert_eq!( + 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) = setup_params(); + + let (bundle, _) = IssueBundle::new( + ik, + b"Asset description".to_vec(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(5), + }), + true, + rng, + ) + .unwrap(); + + let mut signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + // Add "bad" note + let note = Note::new( + recipient, + NoteValue::from_raw(5), + AssetBase::derive(signed.ik(), b"zsa_asset"), + Rho::from_nf_old(Nullifier::dummy(&mut rng)), + &mut rng, + ); + + signed.actions.first_mut().notes.push(note); + + assert_eq!( + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), + IssueBundleIkMismatchAssetBase + ); + } + + #[test] + fn issue_bundle_verify_fail_incorrect_ik() { + let asset_description = b"Asset".to_vec(); + + let (mut rng, isk, ik, recipient, sighash) = setup_params(); + + let (bundle, _) = IssueBundle::new( + ik, + asset_description.clone(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(5), + }), + true, + rng, + ) + .unwrap(); + + let mut signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + let incorrect_isk = IssuanceAuthorizingKey::random(); + let incorrect_ik: IssuanceValidatingKey = (&incorrect_isk).into(); + + // Add "bad" note + let note = Note::new( + recipient, + NoteValue::from_raw(55), + AssetBase::derive(&incorrect_ik, &asset_description), + Rho::from_nf_old(Nullifier::dummy(&mut rng)), + &mut rng, + ); + + signed.actions.first_mut().notes = vec![note]; + + assert_eq!( + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), + IssueBundleIkMismatchAssetBase + ); + } + + #[test] + fn issue_bundle_verify_fail_wrong_asset_descr_size() { + // we want to inject a "malformed" description for test purposes. + impl IssueAction { + pub fn modify_descr(&mut self, new_descr: Vec) { + self.asset_desc = new_descr; + } + } + + let (rng, isk, ik, recipient, sighash) = setup_params(); + + let (bundle, _) = IssueBundle::new( + ik, + b"Asset description".to_vec(), + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(5), + }), + true, + rng, + ) + .unwrap(); + + let mut signed = bundle.prepare(sighash).sign(&isk).unwrap(); + + // 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, |_| None).unwrap_err(), + WrongAssetDescSize + ); + + // 2. Try a description that is empty + signed.actions.first_mut().modify_descr(b"".to_vec()); + + assert_eq!( + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), + WrongAssetDescSize + ); + } + + #[test] + fn issue_bundle_cannot_be_signed_with_asset_base_identity_point() { + let (isk, bundle, sighash) = identity_point_test_params(10, 20); + + assert_eq!( + bundle.prepare(sighash).sign(&isk).unwrap_err(), + AssetBaseCannotBeIdentityPoint + ); + } + + #[test] + fn issue_bundle_verify_fail_asset_base_identity_point() { + let (isk, bundle, sighash) = identity_point_test_params(10, 20); + + let signed = IssueBundle { + ik: bundle.ik().clone(), + actions: bundle.actions, + authorization: Signed { + signature: isk.try_sign(&sighash).unwrap(), + }, + }; + + assert_eq!( + verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), + AssetBaseCannotBeIdentityPoint + ); + } +} diff --git a/tests/zsa.rs b/tests/zsa.rs index 1efbeda38..032a33d7c 100644 --- a/tests/zsa.rs +++ b/tests/zsa.rs @@ -4,9 +4,10 @@ use crate::builder::verify_bundle; use bridgetree::BridgeTree; use incrementalmerkletree::Hashable; use orchard::bundle::Authorized; -use orchard::issuance::{verify_issue_bundle, IssueBundle, IssueInfo, Signed, Unauthorized}; +use orchard::issuance::{IssueBundle, IssueInfo, Signed, Unauthorized}; use orchard::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey}; use orchard::note::{AssetBase, ExtractedNoteCommitment}; +use orchard::verification::issuance::verify_issue_bundle; use orchard::tree::{MerkleHashOrchard, MerklePath}; use orchard::{ @@ -19,7 +20,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)] @@ -177,12 +177,7 @@ fn issue_zsa_notes(asset_descr: &[u8], keys: &Keychain) -> (Note, Note, Note) { 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) }