diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e6a6c3ae..8099ff630 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,9 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] steps: + - name: Install dependencies (needed for yeslogic-fontconfig-sys crate) + if: runner.os == 'Linux' + run: sudo apt update && sudo apt install -y libfontconfig1-dev - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable id: toolchain @@ -91,6 +94,9 @@ jobs: name: Intra-doc links runs-on: ubuntu-latest steps: + - name: Install dependencies (needed for yeslogic-fontconfig-sys crate) + if: runner.os == 'Linux' + run: sudo apt update && sudo apt install -y libfontconfig1-dev - uses: actions/checkout@v4 - run: cargo fetch # Requires #![deny(rustdoc::broken_intra_doc_links)] in crates. diff --git a/src/bundle/burn_validation.rs b/src/bundle/burn_validation.rs index d05c43294..a3e660f71 100644 --- a/src/bundle/burn_validation.rs +++ b/src/bundle/burn_validation.rs @@ -2,9 +2,9 @@ //! //! The module provides a function `validate_bundle_burn` that can be used to validate the burn values for the bundle. //! -use std::fmt; +use std::{collections::HashMap, fmt}; -use crate::note::AssetBase; +use crate::{note::AssetBase, value::NoteValue}; /// Possible errors that can occur during bundle burn validation. #[derive(Debug)] @@ -14,40 +14,41 @@ pub enum BurnError { DuplicateAsset, /// Cannot burn a native asset. NativeAsset, - /// Cannot burn an asset with a non-positive value. - NonPositiveAmount, + /// Cannot burn an asset with a zero value. + ZeroAmount, } -/// Validates burn for a bundle by ensuring each asset is unique, non-native, and has a positive value. +/// Validates burn for a bundle by ensuring each asset is unique, non-native, and has a non-zero value value. /// -/// Each burn element is represented as a tuple of `AssetBase` and `i64` (value for the burn). +/// Each burn element is represented as a tuple of `AssetBase` and `NoteValue` (value for the burn). /// /// # Arguments /// -/// * `burn` - A vector of assets, where each asset is represented as a tuple of `AssetBase` and `i64` (value the burn). +/// * `burns` - A vector of assets, where each asset is represented as a tuple of `AssetBase` and `NoteValue` (value the burn). /// /// # Errors /// /// Returns a `BurnError` if: /// * Any asset in the `burn` vector is not unique (`BurnError::DuplicateAsset`). /// * Any asset in the `burn` vector is native (`BurnError::NativeAsset`). -/// * Any asset in the `burn` vector has a non-positive value (`BurnError::NonPositiveAmount`). -pub fn validate_bundle_burn(bundle_burn: &Vec<(AssetBase, i64)>) -> Result<(), BurnError> { - let mut asset_set = std::collections::HashSet::<&AssetBase>::new(); +pub fn validate_bundle_burn<'a, I: IntoIterator>( + burn: I, +) -> Result, BurnError> { + let mut burn_set = HashMap::::new(); - for (asset, value) in bundle_burn { - if !asset_set.insert(asset) { - return Err(BurnError::DuplicateAsset); - } + for (asset, value) in burn.into_iter().cloned() { if asset.is_native().into() { return Err(BurnError::NativeAsset); } - if *value <= 0 { - return Err(BurnError::NonPositiveAmount); + if value.inner() == 0 { + return Err(BurnError::ZeroAmount); + } + if burn_set.insert(asset, value).is_some() { + return Err(BurnError::DuplicateAsset); } } - Ok(()) + Ok(burn_set) } impl fmt::Display for BurnError { @@ -55,8 +56,8 @@ impl fmt::Display for BurnError { match *self { BurnError::DuplicateAsset => write!(f, "Encountered a duplicate asset to burn."), BurnError::NativeAsset => write!(f, "Cannot burn a native asset."), - BurnError::NonPositiveAmount => { - write!(f, "Cannot burn an asset with a non-positive value.") + BurnError::ZeroAmount => { + write!(f, "Cannot burn an asset with a zero value.") } } } @@ -64,6 +65,8 @@ impl fmt::Display for BurnError { #[cfg(test)] mod tests { + use crate::value::NoteValue; + use super::*; /// Creates an item of bundle burn list for a given asset description and value. @@ -80,14 +83,14 @@ mod tests { /// /// A tuple `(AssetBase, Amount)` representing the burn list item. /// - pub fn get_burn_tuple(asset_desc: &[u8], value: i64) -> (AssetBase, i64) { + pub fn get_burn_tuple(asset_desc: &[u8], value: u64) -> (AssetBase, NoteValue) { use crate::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey}; let isk = IssuanceAuthorizingKey::from_bytes([1u8; 32]).unwrap(); ( AssetBase::derive(&IssuanceValidatingKey::from(&isk), asset_desc), - value, + NoteValue::from_raw(value), ) } @@ -121,7 +124,7 @@ mod tests { fn validate_bundle_burn_native_asset() { let bundle_burn = vec![ get_burn_tuple(b"Asset 1", 10), - (AssetBase::native(), 20), + (AssetBase::native(), NoteValue::from_raw(20)), get_burn_tuple(b"Asset 3", 10), ]; @@ -140,6 +143,6 @@ mod tests { let result = validate_bundle_burn(&bundle_burn); - assert_eq!(result, Err(BurnError::NonPositiveAmount)); + assert_eq!(result, Err(BurnError::ZeroAmount)); } } diff --git a/src/issuance.rs b/src/issuance.rs index d2d2b2aaf..1a49d7a81 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -12,13 +12,13 @@ use crate::constants::reference_keys::ReferenceKeys; use crate::issuance::Error::{ AssetBaseCannotBeIdentityPoint, IssueActionNotFound, IssueActionPreviouslyFinalizedAssetBase, IssueActionWithoutNoteNotFinalized, IssueBundleIkMismatchAssetBase, - IssueBundleInvalidSignature, ValueSumOverflow, WrongAssetDescSize, + 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}; -use crate::value::{NoteValue, ValueSum}; +use crate::value::NoteValue; use crate::{Address, Note}; use crate::supply_info::{AssetSupply, SupplyInfo}; @@ -114,7 +114,7 @@ impl IssueAction { /// /// This function may return an error in any of the following cases: /// - /// * `ValueSumOverflow`: If the total amount value of all notes in the `IssueAction` overflows. + /// * `ValueOverflow`: If the total amount value of all notes in the `IssueAction` overflows. /// /// * `IssueBundleIkMismatchAssetBase`: If the provided `ik` is not used to derive the /// `AssetBase` for **all** internal notes. @@ -132,7 +132,7 @@ impl IssueAction { let value_sum = self .notes .iter() - .try_fold(ValueSum::zero(), |value_sum, ¬e| { + .try_fold(NoteValue::zero(), |value_sum, ¬e| { //The asset base should not be the identity point of the Pallas curve. if bool::from(note.asset().cv_base().is_identity()) { return Err(AssetBaseCannotBeIdentityPoint); @@ -145,7 +145,7 @@ impl IssueAction { .ok_or(IssueBundleIkMismatchAssetBase)?; // The total amount should not overflow - (value_sum + note.value()).ok_or(ValueSumOverflow) + (value_sum + note.value()).ok_or(ValueOverflow) })?; Ok(( @@ -298,6 +298,21 @@ impl IssueBundle { authorization: map_auth(authorization), } } + + /// Returns the reference notes for the `IssueBundle`. + pub fn get_reference_notes(&self) -> HashMap { + let mut reference_notes = HashMap::new(); + self.actions.iter().for_each(|action| { + action.notes.iter().for_each(|note| { + if (note.recipient() == ReferenceKeys::recipient()) + && (note.value() == NoteValue::zero()) + { + reference_notes.insert(note.asset(), *note); + } + }) + }); + reference_notes + } } impl IssueBundle { @@ -468,23 +483,6 @@ impl IssueBundle { } } -impl IssueBundle { - /// Returns the reference notes for the `IssueBundle`. - pub fn get_reference_notes(self) -> HashMap { - let mut reference_notes = HashMap::new(); - self.actions.iter().for_each(|action| { - action.notes.iter().for_each(|note| { - if (note.recipient() == ReferenceKeys::recipient()) - && (note.value() == NoteValue::zero()) - { - reference_notes.insert(note.asset(), *note); - } - }) - }); - reference_notes - } -} - fn create_reference_note(asset: AssetBase, mut rng: impl RngCore) -> Note { Note::new( ReferenceKeys::recipient(), @@ -561,12 +559,14 @@ impl IssueBundle { /// * For each `Note` inside an `IssueAction`: /// * All notes have the same, correct `AssetBase`. /// -// # Returns +/// # Returns /// -/// A Result containing a SupplyInfo struct, which stores supply information in a HashMap. -/// The HashMap uses AssetBase as the key, and an AssetSupply struct as the value. The -/// AssetSupply contains a ValueSum (representing the total value of all notes for the asset) -/// and a bool indicating whether the asset is finalized. +/// A Result containing a SupplyInfo struct, which stores supply information and reference notes in +/// two HashMaps. 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) and a bool indicating whether the asset is finalized. The HashMap `reference_notes` uses +/// AssetBase as the key, and a Note struct as the value. The Note is the reference note for this +/// asset. /// /// # Errors /// @@ -576,7 +576,7 @@ impl IssueBundle { /// asset in the bundle is incorrect. /// * `IssueActionPreviouslyFinalizedAssetBase`: This error occurs if the asset has already been /// finalized (inserted into the `finalized` collection). -/// * `ValueSumOverflow`: This error occurs if an overflow happens during the calculation of +/// * `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 @@ -591,26 +591,25 @@ pub fn verify_issue_bundle( .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 supply_info = bundle.actions().iter().try_fold( + SupplyInfo::new(bundle.get_reference_notes()), + |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())?; + let (asset, supply) = action.verify_supply(bundle.ik())?; - // Fail if the asset was previously finalized. - if finalized.contains(&asset) { - return Err(IssueActionPreviouslyFinalizedAssetBase(asset)); - } + // Fail if the asset was previously finalized. + if finalized.contains(&asset) { + return Err(IssueActionPreviouslyFinalizedAssetBase(asset)); + } - supply_info.add_supply(asset, supply)?; + supply_info.add_supply(asset, supply)?; - Ok(supply_info) - })?; + Ok(supply_info) + }, + )?; Ok(supply_info) } @@ -636,7 +635,7 @@ pub enum Error { IssueActionPreviouslyFinalizedAssetBase(AssetBase), /// Overflow error occurred while calculating the value of the asset - ValueSumOverflow, + ValueOverflow, } impl fmt::Display for Error { @@ -672,7 +671,7 @@ impl fmt::Display for Error { IssueActionPreviouslyFinalizedAssetBase(_) => { write!(f, "the provided `AssetBase` has been previously finalized") } - ValueSumOverflow => { + ValueOverflow => { write!( f, "overflow error occurred while calculating the value of the asset" @@ -696,7 +695,7 @@ mod tests { FullViewingKey, IssuanceAuthorizingKey, IssuanceValidatingKey, Scope, SpendingKey, }; use crate::note::{AssetBase, Nullifier, Rho}; - use crate::value::{NoteValue, ValueSum}; + use crate::value::NoteValue; use crate::{Address, Note}; use group::{Group, GroupEncoding}; use nonempty::NonEmpty; @@ -829,7 +828,7 @@ mod tests { let (asset, supply) = result.unwrap(); assert_eq!(asset, test_asset); - assert_eq!(supply.amount, ValueSum::from_raw(30)); + assert_eq!(supply.amount, NoteValue::from_raw(30)); assert!(!supply.is_finalized); } @@ -854,7 +853,7 @@ mod tests { let (asset, supply) = result.unwrap(); assert_eq!(asset, test_asset); - assert_eq!(supply.amount, ValueSum::from_raw(30)); + assert_eq!(supply.amount, NoteValue::from_raw(30)); assert!(supply.is_finalized); } @@ -1229,15 +1228,15 @@ mod tests { assert_eq!( supply_info.assets.get(&asset1_base), - Some(&AssetSupply::new(ValueSum::from_raw(15), true)) + Some(&AssetSupply::new(NoteValue::from_raw(15), true)) ); assert_eq!( supply_info.assets.get(&asset2_base), - Some(&AssetSupply::new(ValueSum::from_raw(10), true)) + Some(&AssetSupply::new(NoteValue::from_raw(10), true)) ); assert_eq!( supply_info.assets.get(&asset3_base), - Some(&AssetSupply::new(ValueSum::from_raw(5), false)) + Some(&AssetSupply::new(NoteValue::from_raw(5), false)) ); } diff --git a/src/supply_info.rs b/src/supply_info.rs index 6fb69d516..4b5f4cabc 100644 --- a/src/supply_info.rs +++ b/src/supply_info.rs @@ -2,21 +2,21 @@ use std::collections::{hash_map, HashMap, HashSet}; -use crate::{issuance::Error, note::AssetBase, value::ValueSum}; +use crate::{issuance::Error, note::AssetBase, value::NoteValue, Note}; /// Represents the amount of an asset and its finalization status. #[derive(Debug, Clone, Copy)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct AssetSupply { /// The amount of the asset. - pub amount: ValueSum, + pub amount: NoteValue, /// Whether or not the asset is finalized. pub is_finalized: bool, } impl AssetSupply { /// Creates a new AssetSupply instance with the given amount and finalization status. - pub fn new(amount: ValueSum, is_finalized: bool) -> Self { + pub fn new(amount: NoteValue, is_finalized: bool) -> Self { Self { amount, is_finalized, @@ -29,13 +29,16 @@ impl AssetSupply { pub struct SupplyInfo { /// A map of asset bases to their respective supply information. pub assets: HashMap, + /// A map of asset bases to their respective reference note. + pub reference_notes: HashMap, } impl SupplyInfo { /// Creates a new, empty `SupplyInfo` instance. - pub fn new() -> Self { + pub fn new(reference_notes: HashMap) -> Self { Self { assets: HashMap::new(), + reference_notes, } } @@ -46,8 +49,7 @@ impl SupplyInfo { 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::ValueSumOverflow)?; + supply.amount = (supply.amount + new_supply.amount).ok_or(Error::ValueOverflow)?; supply.is_finalized |= new_supply.is_finalized; } hash_map::Entry::Vacant(entry) => { @@ -71,7 +73,7 @@ impl SupplyInfo { impl Default for SupplyInfo { fn default() -> Self { - Self::new() + Self::new(HashMap::new()) } } @@ -87,25 +89,25 @@ mod tests { AssetBase::derive(&IssuanceValidatingKey::from(&isk), asset_desc) } - fn sum<'a, T: IntoIterator>(supplies: T) -> Option { + fn sum<'a, T: IntoIterator>(supplies: T) -> Option { supplies .into_iter() .map(|supply| supply.amount) - .try_fold(ValueSum::from_raw(0), |sum, value| sum + value) + .try_fold(NoteValue::from_raw(0), |sum, value| sum + value) } #[test] fn test_add_supply_valid() { - let mut supply_info = SupplyInfo::new(); + let mut supply_info = SupplyInfo::new(HashMap::new()); let asset1 = create_test_asset(b"Asset 1"); let asset2 = create_test_asset(b"Asset 2"); - let supply1 = AssetSupply::new(ValueSum::from_raw(20), false); - let supply2 = AssetSupply::new(ValueSum::from_raw(30), true); - let supply3 = AssetSupply::new(ValueSum::from_raw(10), false); - let supply4 = AssetSupply::new(ValueSum::from_raw(10), true); - let supply5 = AssetSupply::new(ValueSum::from_raw(50), false); + let supply1 = AssetSupply::new(NoteValue::from_raw(20), false); + let supply2 = AssetSupply::new(NoteValue::from_raw(30), true); + let supply3 = AssetSupply::new(NoteValue::from_raw(10), false); + let supply4 = AssetSupply::new(NoteValue::from_raw(10), true); + let supply5 = AssetSupply::new(NoteValue::from_raw(50), false); assert_eq!(supply_info.assets.len(), 0); @@ -165,16 +167,16 @@ mod tests { #[test] fn test_update_finalization_set() { - let mut supply_info = SupplyInfo::new(); + let mut supply_info = SupplyInfo::new(HashMap::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(ValueSum::from_raw(10), false); - let supply2 = AssetSupply::new(ValueSum::from_raw(20), true); - let supply3 = AssetSupply::new(ValueSum::from_raw(40), false); - let supply4 = AssetSupply::new(ValueSum::from_raw(50), true); + let supply1 = AssetSupply::new(NoteValue::from_raw(10), false); + let supply2 = AssetSupply::new(NoteValue::from_raw(20), true); + let supply3 = AssetSupply::new(NoteValue::from_raw(40), false); + let supply4 = AssetSupply::new(NoteValue::from_raw(50), true); assert!(supply_info.add_supply(asset1, supply1).is_ok()); assert!(supply_info.add_supply(asset1, supply2).is_ok()); diff --git a/src/value.rs b/src/value.rs index a9cb843d0..b509122a2 100644 --- a/src/value.rs +++ b/src/value.rs @@ -153,6 +153,15 @@ impl Sub for NoteValue { } } +impl Add for NoteValue { + type Output = Option; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn add(self, rhs: Self) -> Self::Output { + self.0.checked_add(rhs.0).map(NoteValue) + } +} + pub(crate) enum Sign { Positive, Negative,