diff --git a/crates/cashu-sdk/src/wallet/localstore/memory.rs b/crates/cashu-sdk/src/wallet/localstore/memory.rs index 014f8ce6..9b9738e3 100644 --- a/crates/cashu-sdk/src/wallet/localstore/memory.rs +++ b/crates/cashu-sdk/src/wallet/localstore/memory.rs @@ -49,7 +49,7 @@ impl MemoryLocalStore { } } -#[async_trait(?Send)] +#[async_trait] impl LocalStore for MemoryLocalStore { async fn add_mint( &self, diff --git a/crates/cashu-sdk/src/wallet/localstore/mod.rs b/crates/cashu-sdk/src/wallet/localstore/mod.rs index a300c53e..c477e7c8 100644 --- a/crates/cashu-sdk/src/wallet/localstore/mod.rs +++ b/crates/cashu-sdk/src/wallet/localstore/mod.rs @@ -39,7 +39,7 @@ pub enum Error { Serde(#[from] serde_json::Error), } -#[async_trait(?Send)] +#[async_trait] pub trait LocalStore { async fn add_mint( &self, diff --git a/crates/cashu-sdk/src/wallet/localstore/redb_store.rs b/crates/cashu-sdk/src/wallet/localstore/redb_store.rs index 4ec4c47f..d1fd4b67 100644 --- a/crates/cashu-sdk/src/wallet/localstore/redb_store.rs +++ b/crates/cashu-sdk/src/wallet/localstore/redb_store.rs @@ -51,7 +51,7 @@ impl RedbLocalStore { } } -#[async_trait(?Send)] +#[async_trait] impl LocalStore for RedbLocalStore { async fn add_mint( &self, diff --git a/crates/cashu-sdk/src/wallet/mod.rs b/crates/cashu-sdk/src/wallet/mod.rs index 812b1c1b..38fb0ec9 100644 --- a/crates/cashu-sdk/src/wallet/mod.rs +++ b/crates/cashu-sdk/src/wallet/mod.rs @@ -1,6 +1,7 @@ //! Cashu Wallet use std::collections::{HashMap, HashSet}; use std::str::FromStr; +use std::sync::Arc; use bip39::Mnemonic; use cashu::dhke::{construct_proofs, unblind_message}; @@ -52,21 +53,29 @@ pub enum Error { Cashu(#[from] cashu::error::Error), #[error("Could not verify Dleq")] CouldNotVerifyDleq, + #[error("P2PK Condition Not met `{0}`")] + P2PKConditionsNotMet(String), + #[error("Invalid Spending Conditions: `{0}`")] + InvalidSpendConditions(String), #[error("Unknown Key")] UnknownKey, #[error("`{0}`")] Custom(String), } -#[derive(Clone, Debug)] -pub struct Wallet { - pub client: C, - localstore: L, +#[derive(Clone)] +pub struct Wallet { + pub client: Arc, + pub localstore: Arc, mnemonic: Option, } -impl Wallet { - pub async fn new(client: C, localstore: L, mnemonic: Option) -> Self { +impl Wallet { + pub async fn new( + client: Arc, + localstore: Arc, + mnemonic: Option, + ) -> Self { Self { mnemonic, client, @@ -1134,19 +1143,115 @@ impl Wallet { Ok(restored_value) } + /// Verify all proofs in token have meet the required spend + /// Can be used to allow a wallet to accept payments offline while reducing + /// the risk of claiming back to the limits let by the spending_conditions + #[cfg(feature = "nut11")] + pub fn verify_token_p2pk( + &self, + token: &Token, + spending_conditions: P2PKConditions, + ) -> Result<(), Error> { + use cashu::nuts::nut10; + + if spending_conditions.refund_keys.is_some() && spending_conditions.locktime.is_none() { + warn!( + "Invalid spending conditions set: Locktime must be set if refund keys are allowed" + ); + return Err(Error::InvalidSpendConditions( + "Must set locktime".to_string(), + )); + } + + for mint_proof in &token.token { + for proof in &mint_proof.proofs { + let secret: nut10::Secret = (&proof.secret).try_into().unwrap(); + + let proof_conditions: P2PKConditions = secret.try_into().unwrap(); + + if spending_conditions.num_sigs.ne(&proof_conditions.num_sigs) { + debug!( + "Spending condition requires: {:?} sigs proof secret specifies: {:?}", + spending_conditions.num_sigs, proof_conditions.num_sigs + ); + + return Err(Error::P2PKConditionsNotMet( + "Num sigs did not match spending condition".to_string(), + )); + } + + // Check the Proof has the required pubkeys + if proof_conditions + .pubkeys + .len() + .ne(&spending_conditions.pubkeys.len()) + || !proof_conditions + .pubkeys + .iter() + .all(|pubkey| spending_conditions.pubkeys.contains(pubkey)) + { + debug!("Proof did not included Publickeys meeting condition"); + return Err(Error::P2PKConditionsNotMet( + "Pubkeys in proof not allowed by spending condition".to_string(), + )); + } + + // If spending condition refund keys is allowed (Some(Empty Vec)) + // If spending conition refund keys is allowed to restricted set of keys check + // it is one of them Check that proof locktime is > condition + // locktime + + if let Some(proof_refund_keys) = proof_conditions.refund_keys { + let proof_locktime = proof_conditions.locktime.unwrap(); + + if let (Some(condition_refund_keys), Some(condition_locktime)) = ( + &spending_conditions.refund_keys, + spending_conditions.locktime, + ) { + // Proof locktime must be greater then condition locktime to ensure it + // cannot be claimed back + if proof_locktime.lt(&condition_locktime) { + return Err(Error::P2PKConditionsNotMet( + "Proof locktime less then required".to_string(), + )); + } + + // A non empty condition refund key list is used as a restricted set of keys + // returns are allowed to An empty list means the + // proof can be refunded to anykey set in the secret + if !condition_refund_keys.is_empty() + && !proof_refund_keys + .iter() + .all(|refund_key| condition_refund_keys.contains(refund_key)) + { + return Err(Error::P2PKConditionsNotMet( + "Refund Key not allowed".to_string(), + )); + } + } else { + // Spending conditions does not allow refund keys + return Err(Error::P2PKConditionsNotMet( + "Spending condition does not allow refund keys".to_string(), + )); + } + } + } + } + + Ok(()) + } + /// Verify all proofs in token have a valid DLEQ proof #[cfg(feature = "nut12")] - pub async fn verify_token_dleq(&self, token: Token) -> Result<(), Error> { + pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> { let mut keys_cache: HashMap = HashMap::new(); - for mint_proof in token.token { - let mint_url = mint_proof.mint; - - for proof in mint_proof.proofs { + for mint_proof in &token.token { + for proof in &mint_proof.proofs { let mint_pubkey = match keys_cache.get(&proof.keyset_id) { Some(keys) => keys.amount_key(proof.amount), None => { - let keys = self.get_keyset_keys(&mint_url, proof.keyset_id).await?; + let keys = self.localstore.get_keys(&proof.keyset_id).await?.unwrap(); let key = keys.amount_key(proof.amount); keys_cache.insert(proof.keyset_id, keys); diff --git a/crates/cashu/src/nuts/nut11.rs b/crates/cashu/src/nuts/nut11.rs index 7541611f..14806db8 100644 --- a/crates/cashu/src/nuts/nut11.rs +++ b/crates/cashu/src/nuts/nut11.rs @@ -85,12 +85,15 @@ impl Proof { return Ok(()); } - if let Some(locktime) = spending_conditions.locktime { + if let (Some(locktime), Some(refund_keys)) = ( + spending_conditions.locktime, + spending_conditions.refund_keys, + ) { // If lock time has passed check if refund witness signature is valid - if locktime.lt(&unix_time()) && !spending_conditions.refund_keys.is_empty() { + if locktime.lt(&unix_time()) { if let Some(signatures) = &self.witness { for s in &signatures.signatures { - for v in &spending_conditions.refund_keys { + for v in &refund_keys { let sig = Signature::try_from(hex::decode(s)?.as_slice()) .map_err(|_| Error::InvalidSignature)?; @@ -175,9 +178,8 @@ pub struct P2PKConditions { #[serde(skip_serializing_if = "Option::is_none")] pub locktime: Option, pub pubkeys: Vec, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub refund_keys: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub refund_keys: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub num_sigs: Option, pub sig_flag: SigFlag, @@ -187,7 +189,7 @@ impl P2PKConditions { pub fn new( locktime: Option, pubkeys: Vec, - refund_keys: Vec, + refund_keys: Option>, num_sigs: Option, sig_flag: Option, ) -> Result { @@ -241,10 +243,9 @@ impl TryFrom for Secret { tags.push(Tag::NSigs(num_sigs).as_vec()); } - if !refund_keys.is_empty() { + if let Some(refund_keys) = refund_keys { tags.push(Tag::Refund(refund_keys).as_vec()) } - tags.push(Tag::SigFlag(sig_flag).as_vec()); Ok(Secret { @@ -300,11 +301,11 @@ impl TryFrom for P2PKConditions { let refund_keys = if let Some(tag) = tags.get(&TagKind::Refund) { match tag { - Tag::Refund(keys) => keys.clone(), - _ => vec![], + Tag::Refund(keys) => Some(keys.clone()), + _ => None, } } else { - vec![] + None }; let sig_flag = if let Some(tag) = tags.get(&TagKind::SigFlag) { @@ -702,10 +703,10 @@ mod tests { ) .unwrap(), ], - refund_keys: vec![VerifyingKey::from_str( + refund_keys: Some(vec![VerifyingKey::from_str( "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", ) - .unwrap()], + .unwrap()]), num_sigs: Some(2), sig_flag: SigFlag::SigAll, }; @@ -742,7 +743,7 @@ mod tests { let conditions = P2PKConditions { locktime: Some(21), pubkeys: vec![v_key.clone(), v_key_two, v_key_three], - refund_keys: vec![v_key], + refund_keys: Some(vec![v_key]), num_sigs: Some(2), sig_flag: SigFlag::SigInputs, };