From 3b5c8b5c5e0ece577d36fd0fff872497bd0aee1d Mon Sep 17 00:00:00 2001 From: "thesimplekid (aider)" Date: Thu, 10 Apr 2025 23:02:43 +0100 Subject: [PATCH] refactor: Ensure unique proofs when calculating token value --- crates/cashu/src/nuts/nut00/mod.rs | 3 + crates/cashu/src/nuts/nut00/token.rs | 91 +++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/crates/cashu/src/nuts/nut00/mod.rs b/crates/cashu/src/nuts/nut00/mod.rs index ab79255f..7143a376 100644 --- a/crates/cashu/src/nuts/nut00/mod.rs +++ b/crates/cashu/src/nuts/nut00/mod.rs @@ -150,6 +150,9 @@ pub enum Error { /// Unsupported token #[error("Unsupported payment method")] UnsupportedPaymentMethod, + /// Duplicate proofs in token + #[error("Duplicate proofs in token")] + DuplicateProofs, /// Serde Json error #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), diff --git a/crates/cashu/src/nuts/nut00/token.rs b/crates/cashu/src/nuts/nut00/token.rs index fa8a961f..01d47aad 100644 --- a/crates/cashu/src/nuts/nut00/token.rs +++ b/crates/cashu/src/nuts/nut00/token.rs @@ -233,15 +233,21 @@ impl TokenV3 { .collect() } - /// Value + /// Value - errors if duplicate proofs are found #[inline] pub fn value(&self) -> Result { - Ok(Amount::try_sum( - self.token - .iter() - .map(|t| t.proofs.total_amount()) - .collect::, _>>()?, - )?) + let proofs = self.proofs(); + let unique_count = proofs + .iter() + .collect::>() + .len(); + + // Check if there are any duplicate proofs + if unique_count != proofs.len() { + return Err(Error::DuplicateProofs); + } + + proofs.total_amount() } /// Memo @@ -336,15 +342,21 @@ impl TokenV4 { .collect() } - /// Value + /// Value - errors if duplicate proofs are found #[inline] pub fn value(&self) -> Result { - Ok(Amount::try_sum( - self.token - .iter() - .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount))) - .collect::, _>>()?, - )?) + let proofs = self.proofs(); + let unique_count = proofs + .iter() + .collect::>() + .len(); + + // Check if there are any duplicate proofs + if unique_count != proofs.len() { + return Err(Error::DuplicateProofs); + } + + proofs.total_amount() } /// Memo @@ -483,6 +495,7 @@ mod tests { use super::*; use crate::mint_url::MintUrl; + use crate::secret::Secret; use crate::util::hex; #[test] @@ -621,4 +634,54 @@ mod tests { let tokenv4_bytes_ = tokenv4_.to_raw_bytes().expect("Serialization error"); assert!(tokenv4_bytes_ == tokenv4_bytes); } + + #[test] + fn test_token_with_duplicate_proofs() { + // Create a token with duplicate proofs + let mint_url = MintUrl::from_str("https://example.com").unwrap(); + let keyset_id = Id::from_str("009a1f293253e41e").unwrap(); + + let secret = Secret::generate(); + // Create two identical proofs + let proof1 = Proof { + amount: Amount::from(10), + keyset_id, + secret: secret.clone(), + c: "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea" + .parse() + .unwrap(), + witness: None, + dleq: None, + }; + + let proof2 = proof1.clone(); // Duplicate proof + + // Create a token with the duplicate proofs + let proofs = vec![proof1.clone(), proof2].into_iter().collect(); + let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat); + + // Verify that value() returns an error + let result = token.value(); + assert!(result.is_err()); + + // Create a token with unique proofs + let proof3 = Proof { + amount: Amount::from(10), + keyset_id, + secret: Secret::generate(), + c: "03bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea" + .parse() + .unwrap(), // Different C value + witness: None, + dleq: None, + }; + + let proofs = vec![proof1, proof3].into_iter().collect(); + let token = Token::new(mint_url, proofs, None, CurrencyUnit::Sat); + + // Verify that value() succeeds with unique proofs + let result = token.value(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Amount::from(20)); + } }