Переглянути джерело

feat(wallet): verify token dleq and p2pk conditions

thesimplekid 11 місяців тому
батько
коміт
ed117ef3a2

+ 1 - 1
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,

+ 1 - 1
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,

+ 1 - 1
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,

+ 117 - 12
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<C: Client, L: LocalStore> {
-    pub client: C,
-    localstore: L,
+#[derive(Clone)]
+pub struct Wallet {
+    pub client: Arc<dyn Client + Send + Sync>,
+    pub localstore: Arc<dyn LocalStore + Send + Sync>,
     mnemonic: Option<Mnemonic>,
 }
 
-impl<C: Client, L: LocalStore> Wallet<C, L> {
-    pub async fn new(client: C, localstore: L, mnemonic: Option<Mnemonic>) -> Self {
+impl Wallet {
+    pub async fn new(
+        client: Arc<dyn Client + Sync + Send>,
+        localstore: Arc<dyn LocalStore + Send + Sync>,
+        mnemonic: Option<Mnemonic>,
+    ) -> Self {
         Self {
             mnemonic,
             client,
@@ -1134,19 +1143,115 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
         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<Id, Keys> = 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);

+ 16 - 15
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<u64>,
     pub pubkeys: Vec<VerifyingKey>,
-    #[serde(default)]
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    pub refund_keys: Vec<VerifyingKey>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub refund_keys: Option<Vec<VerifyingKey>>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub num_sigs: Option<u64>,
     pub sig_flag: SigFlag,
@@ -187,7 +189,7 @@ impl P2PKConditions {
     pub fn new(
         locktime: Option<u64>,
         pubkeys: Vec<VerifyingKey>,
-        refund_keys: Vec<VerifyingKey>,
+        refund_keys: Option<Vec<VerifyingKey>>,
         num_sigs: Option<u64>,
         sig_flag: Option<SigFlag>,
     ) -> Result<Self, Error> {
@@ -241,10 +243,9 @@ impl TryFrom<P2PKConditions> 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<Secret> 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,
         };