From 435e96440f32060ab6938f06e8f5b409f2586f3f Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Wed, 20 Aug 2025 16:04:28 +0200 Subject: [PATCH 1/4] Add rho derivation check in verify_issue_bundle --- src/issuance.rs | 77 +++++++++++++++++++++++++--------- tests/issuance_global_state.rs | 51 ++++++++++++++++++---- tests/zsa.rs | 8 +++- 3 files changed, 107 insertions(+), 29 deletions(-) diff --git a/src/issuance.rs b/src/issuance.rs index e32c94086..e97254ea5 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -25,16 +25,16 @@ use crate::{ bundle::commitments::{hash_issue_bundle_auth_data, hash_issue_bundle_txid_data}, constants::reference_keys::ReferenceKeys, keys::{IssuanceAuthorizingKey, IssuanceValidatingKey}, - note::{AssetBase, Nullifier, Rho}, + note::{rho_for_issuance_note, AssetBase, Nullifier, Rho}, value::NoteValue, Address, Note, }; use Error::{ - AssetBaseCannotBeIdentityPoint, CannotBeFirstIssuance, IssueActionNotFound, - IssueActionPreviouslyFinalizedAssetBase, IssueActionWithoutNoteNotFinalized, - IssueBundleIkMismatchAssetBase, IssueBundleInvalidSignature, - MissingReferenceNoteOnFirstIssuance, ValueOverflow, + AssetBaseCannotBeIdentityPoint, CannotBeFirstIssuance, IncorrectRhoDerivation, + IssueActionNotFound, IssueActionPreviouslyFinalizedAssetBase, + IssueActionWithoutNoteNotFinalized, IssueBundleIkMismatchAssetBase, + IssueBundleInvalidSignature, MissingReferenceNoteOnFirstIssuance, ValueOverflow, }; /// Checks if a given note is a reference note. @@ -599,6 +599,9 @@ impl IssueBundle { /// - 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. +/// - **Rho computation**: +/// - Ensures that the `rho` value of each issuance note is correctly computed from the given +/// `first_nullifier`. /// /// # Arguments /// @@ -607,6 +610,8 @@ impl IssueBundle { /// * `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. +/// * `first_nullifier`: A reference to a [`Nullifier`] that is used to compute the `rho` value of +/// each issuance note. /// /// # Returns /// @@ -622,22 +627,42 @@ impl IssueBundle { /// already been finalized. /// * `MissingReferenceNoteOnFirstIssuance`: No reference note is provided for the first /// issuance of a new asset. +/// * `IncorrectRhoDerivation`: If the `rho` value of any issuance note is not correctly derived +/// from the `first_nullifier`. /// * **Other Errors**: Any additional errors returned by the `IssueAction::verify` method are /// propagated pub fn verify_issue_bundle( bundle: &IssueBundle, sighash: [u8; 32], get_global_records: impl Fn(&AssetBase) -> Option, + first_nullifier: &Nullifier, ) -> Result, Error> { bundle .ik() .verify(&sighash, bundle.authorization().signature()) .map_err(|_| IssueBundleInvalidSignature)?; - bundle - .actions() - .iter() - .try_fold(BTreeMap::new(), |mut new_records, action| { + bundle.actions().iter().enumerate().try_fold( + BTreeMap::new(), + |mut new_records, (index_action, action)| { + // Check rho derivation for each note. + action + .notes + .iter() + .enumerate() + .try_for_each(|(index_note, note)| { + if note.rho() + != rho_for_issuance_note( + first_nullifier, + index_action.try_into().unwrap(), + index_note.try_into().unwrap(), + ) + { + return Err(IncorrectRhoDerivation); + } + Ok(()) + })?; + let (asset, amount) = action.verify(bundle.ik())?; let is_finalized = action.is_finalized(); @@ -670,7 +695,8 @@ pub fn verify_issue_bundle( new_records.insert(asset, new_asset_record); Ok(new_records) - }) + }, + ) } /// Errors produced during the issuance process @@ -692,6 +718,8 @@ pub enum Error { IssueBundleInvalidSignature, /// The provided `AssetBase` has been previously finalized. IssueActionPreviouslyFinalizedAssetBase, + /// The rho value of an issuance note is not correctly derived from the first nullifier. + IncorrectRhoDerivation, /// Overflow error occurred while calculating the value of the asset ValueOverflow, @@ -736,6 +764,9 @@ impl fmt::Display for Error { IssueActionPreviouslyFinalizedAssetBase => { write!(f, "the provided `AssetBase` has been previously finalized") } + IncorrectRhoDerivation => { + write!(f, "incorrect rho value") + } ValueOverflow => { write!( f, @@ -1232,7 +1263,8 @@ mod tests { .sign(&isk) .unwrap(); - let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); + let issued_assets = + verify_issue_bundle(&signed, sighash, |_| None, &first_nullifier).unwrap(); let first_note = *signed.actions().first().notes().first().unwrap(); assert_eq!( @@ -1277,7 +1309,8 @@ mod tests { .sign(&isk) .unwrap(); - let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); + let issued_assets = + verify_issue_bundle(&signed, sighash, |_| None, &first_nullifier).unwrap(); let first_note = *signed.actions().first().notes().first().unwrap(); assert_eq!( @@ -1362,7 +1395,8 @@ mod tests { .sign(&isk) .unwrap(); - let issued_assets = verify_issue_bundle(&signed, sighash, |_| None).unwrap(); + let issued_assets = + verify_issue_bundle(&signed, sighash, |_| None, &first_nullifier).unwrap(); assert_eq!(issued_assets.keys().len(), 3); @@ -1447,8 +1481,13 @@ mod tests { .collect::>(); assert_eq!( - verify_issue_bundle(&signed, sighash, |asset| issued_assets.get(asset).copied()) - .unwrap_err(), + verify_issue_bundle( + &signed, + sighash, + |asset| issued_assets.get(asset).copied(), + &first_nullifier + ) + .unwrap_err(), IssueActionPreviouslyFinalizedAssetBase ); } @@ -1495,7 +1534,7 @@ mod tests { }); assert_eq!( - verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None, &first_nullifier).unwrap_err(), IssueBundleInvalidSignature ); } @@ -1530,7 +1569,7 @@ mod tests { .unwrap(); assert_eq!( - verify_issue_bundle(&signed, random_sighash, |_| None).unwrap_err(), + verify_issue_bundle(&signed, random_sighash, |_| None, &first_nullifier).unwrap_err(), IssueBundleInvalidSignature ); } @@ -1578,7 +1617,7 @@ mod tests { signed.actions.first_mut().notes.push(note); assert_eq!( - verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None, &first_nullifier).unwrap_err(), IssueBundleIkMismatchAssetBase ); } @@ -1628,7 +1667,7 @@ mod tests { signed.actions.first_mut().notes = vec![note]; assert_eq!( - verify_issue_bundle(&signed, sighash, |_| None).unwrap_err(), + verify_issue_bundle(&signed, sighash, |_| None, &first_nullifier).unwrap_err(), IssueBundleIkMismatchAssetBase ); } diff --git a/tests/issuance_global_state.rs b/tests/issuance_global_state.rs index 94b51df56..753c00f56 100644 --- a/tests/issuance_global_state.rs +++ b/tests/issuance_global_state.rs @@ -230,7 +230,13 @@ fn issue_bundle_verify_with_global_state() { ]); global_state.extend( - verify_issue_bundle(&bundle1, sighash, |asset| global_state.get(asset).cloned()).unwrap(), + verify_issue_bundle( + &bundle1, + sighash, + |asset| global_state.get(asset).cloned(), + ¶ms.first_nullifier, + ) + .unwrap(), ); assert_eq!(global_state, expected_global_state1); @@ -251,7 +257,13 @@ fn issue_bundle_verify_with_global_state() { ]); global_state.extend( - verify_issue_bundle(&bundle2, sighash, |asset| global_state.get(asset).cloned()).unwrap(), + verify_issue_bundle( + &bundle2, + sighash, + |asset| global_state.get(asset).cloned(), + ¶ms.first_nullifier, + ) + .unwrap(), ); assert_eq!(global_state, expected_global_state2); @@ -268,8 +280,13 @@ fn issue_bundle_verify_with_global_state() { let expected_global_state3 = expected_global_state2; assert_eq!( - verify_issue_bundle(&bundle3, sighash, |asset| global_state.get(asset).cloned()) - .unwrap_err(), + verify_issue_bundle( + &bundle3, + sighash, + |asset| global_state.get(asset).cloned(), + ¶ms.first_nullifier + ) + .unwrap_err(), IssueActionPreviouslyFinalizedAssetBase, ); assert_eq!(global_state, expected_global_state3); @@ -287,8 +304,13 @@ fn issue_bundle_verify_with_global_state() { let expected_global_state4 = expected_global_state3; assert_eq!( - verify_issue_bundle(&bundle4, sighash, |asset| global_state.get(asset).cloned()) - .unwrap_err(), + verify_issue_bundle( + &bundle4, + sighash, + |asset| global_state.get(asset).cloned(), + ¶ms.first_nullifier + ) + .unwrap_err(), MissingReferenceNoteOnFirstIssuance, ); assert_eq!(global_state, expected_global_state4); @@ -306,8 +328,13 @@ fn issue_bundle_verify_with_global_state() { let expected_global_state5 = expected_global_state4; assert_eq!( - verify_issue_bundle(&bundle5, sighash, |asset| global_state.get(asset).cloned()) - .unwrap_err(), + verify_issue_bundle( + &bundle5, + sighash, + |asset| global_state.get(asset).cloned(), + ¶ms.first_nullifier + ) + .unwrap_err(), ValueOverflow, ); assert_eq!(global_state, expected_global_state5); @@ -330,7 +357,13 @@ fn issue_bundle_verify_with_global_state() { ]); global_state.extend( - verify_issue_bundle(&bundle6, sighash, |asset| global_state.get(asset).cloned()).unwrap(), + verify_issue_bundle( + &bundle6, + sighash, + |asset| global_state.get(asset).cloned(), + ¶ms.first_nullifier, + ) + .unwrap(), ); assert_eq!(global_state, expected_global_state6); } diff --git a/tests/zsa.rs b/tests/zsa.rs index d9b95bce4..27b5b358f 100644 --- a/tests/zsa.rs +++ b/tests/zsa.rs @@ -186,7 +186,13 @@ fn issue_zsa_notes( AssetBase::derive(&keys.ik().clone(), &asset_desc_hash), ); - assert!(verify_issue_bundle(&issue_bundle, issue_bundle.commitment().into(), |_| None).is_ok()); + assert!(verify_issue_bundle( + &issue_bundle, + issue_bundle.commitment().into(), + |_| None, + first_nullifier + ) + .is_ok()); (*reference_note, *note1, *note2) } From 759d6187a9a9ff0121567d2061d4473e00ed85c1 Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Wed, 20 Aug 2025 16:24:44 +0200 Subject: [PATCH 2/4] Fix tests --- src/issuance.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/issuance.rs b/src/issuance.rs index e97254ea5..034489255 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -1610,7 +1610,7 @@ mod tests { signed.ik(), &compute_asset_desc_hash(&NonEmpty::from_slice(b"zsa_asset").unwrap()), ), - Rho::zero(), + rho_for_issuance_note(&first_nullifier, 0, 2), &mut rng, ); @@ -1660,7 +1660,7 @@ mod tests { recipient, NoteValue::from_raw(55), AssetBase::derive(&incorrect_ik, &asset_desc_hash), - Rho::zero(), + rho_for_issuance_note(&first_nullifier, 0, 0), &mut rng, ); From a9c9d81baa9c270c78d356557a37769d27b09bdf Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Wed, 20 Aug 2025 16:24:56 +0200 Subject: [PATCH 3/4] Add test --- src/issuance.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/issuance.rs b/src/issuance.rs index 034489255..9c3198c41 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -790,7 +790,7 @@ mod tests { builder::{Builder, BundleType}, circuit::ProvingKey, issuance::Error::{ - IssueActionNotFound, IssueActionPreviouslyFinalizedAssetBase, + IncorrectRhoDerivation, IssueActionNotFound, IssueActionPreviouslyFinalizedAssetBase, IssueBundleIkMismatchAssetBase, IssueBundleInvalidSignature, }, issuance::{ @@ -1236,7 +1236,7 @@ mod tests { #[test] fn issue_bundle_verify() { let TestParams { - rng, + mut rng, isk, ik, recipient, @@ -1266,6 +1266,12 @@ mod tests { let issued_assets = verify_issue_bundle(&signed, sighash, |_| None, &first_nullifier).unwrap(); + // Verify that `verify_issue_bundle` returns an error if `first_nullifier` is incorrect. + assert_eq!( + verify_issue_bundle(&signed, sighash, |_| None, &Nullifier::dummy(&mut rng)), + Err(IncorrectRhoDerivation) + ); + let first_note = *signed.actions().first().notes().first().unwrap(); assert_eq!( issued_assets, From d332deb9d800ef39102b72eb8f921545815f59c6 Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Tue, 26 Aug 2025 09:43:10 +0200 Subject: [PATCH 4/4] Create issue_bundle_verify_fail_incorrect_rho_derivation test --- src/issuance.rs | 69 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/src/issuance.rs b/src/issuance.rs index 9c3198c41..abc94f4d6 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -646,22 +646,13 @@ pub fn verify_issue_bundle( BTreeMap::new(), |mut new_records, (index_action, action)| { // Check rho derivation for each note. - action - .notes - .iter() - .enumerate() - .try_for_each(|(index_note, note)| { - if note.rho() - != rho_for_issuance_note( - first_nullifier, - index_action.try_into().unwrap(), - index_note.try_into().unwrap(), - ) - { - return Err(IncorrectRhoDerivation); - } - Ok(()) - })?; + for (index_note, note) in action.notes.iter().enumerate() { + let expected_rho = + rho_for_issuance_note(first_nullifier, index_action as u32, index_note as u32); + if note.rho() != expected_rho { + return Err(IncorrectRhoDerivation); + } + } let (asset, amount) = action.verify(bundle.ik())?; @@ -1236,7 +1227,7 @@ mod tests { #[test] fn issue_bundle_verify() { let TestParams { - mut rng, + rng, isk, ik, recipient, @@ -1266,12 +1257,6 @@ mod tests { let issued_assets = verify_issue_bundle(&signed, sighash, |_| None, &first_nullifier).unwrap(); - // Verify that `verify_issue_bundle` returns an error if `first_nullifier` is incorrect. - assert_eq!( - verify_issue_bundle(&signed, sighash, |_| None, &Nullifier::dummy(&mut rng)), - Err(IncorrectRhoDerivation) - ); - let first_note = *signed.actions().first().notes().first().unwrap(); assert_eq!( issued_assets, @@ -1436,6 +1421,44 @@ mod tests { ); } + #[test] + fn issue_bundle_verify_fail_incorrect_rho_derivation() { + let TestParams { + mut rng, + isk, + ik, + recipient, + sighash, + first_nullifier, + } = setup_params(); + + let asset_desc_hash = + compute_asset_desc_hash(&NonEmpty::from_slice(b"asset desc").unwrap()); + + let (bundle, _) = IssueBundle::new( + ik.clone(), + asset_desc_hash, + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(5), + }), + true, + rng, + ); + + let signed = bundle + .update_rho(&first_nullifier) + .prepare(sighash) + .sign(&isk) + .unwrap(); + + // Verify that `verify_issue_bundle` returns an error if `first_nullifier` is incorrect. + assert_eq!( + verify_issue_bundle(&signed, sighash, |_| None, &Nullifier::dummy(&mut rng)), + Err(IncorrectRhoDerivation) + ); + } + #[test] fn issue_bundle_verify_fail_previously_finalized() { let TestParams {