ソースを参照

feat(wallet): premint secrets from seed

thesimplekid 1 年間 前
コミット
4d4467df58

+ 2 - 1
crates/cashu-sdk/Cargo.toml

@@ -14,11 +14,12 @@ default = ["mint", "wallet", "all-nuts", "redb"]
 mint = ["cashu/mint"]
 wallet = ["cashu/wallet", "dep:minreq", "dep:once_cell"]
 gloo = ["dep:gloo"]
-all-nuts = ["nut07", "nut08", "nut10", "nut11"]
+all-nuts = ["nut07", "nut08", "nut10", "nut11", "nut13"]
 nut07 = ["cashu/nut07"]
 nut08 = ["cashu/nut08"]
 nut10 = ["cashu/nut10"]
 nut11 = ["cashu/nut11"]
+nut13 = ["cashu/nut13"]
 redb = ["dep:redb"]
 
 

+ 17 - 0
crates/cashu-sdk/src/wallet/localstore/memory.rs

@@ -18,6 +18,8 @@ pub struct MemoryLocalStore {
     mint_keys: Arc<Mutex<HashMap<Id, Keys>>>,
     proofs: Arc<Mutex<HashMap<UncheckedUrl, HashSet<Proof>>>>,
     pending_proofs: Arc<Mutex<HashMap<UncheckedUrl, HashSet<Proof>>>>,
+    #[cfg(feature = "nut13")]
+    keyset_counter: Arc<Mutex<HashMap<Id, u64>>>,
 }
 
 impl MemoryLocalStore {
@@ -25,6 +27,7 @@ impl MemoryLocalStore {
         mint_quotes: Vec<MintQuote>,
         melt_quotes: Vec<MeltQuote>,
         mint_keys: Vec<Keys>,
+        keyset_counter: HashMap<Id, u64>,
     ) -> Self {
         Self {
             mints: Arc::new(Mutex::new(HashMap::new())),
@@ -40,6 +43,8 @@ impl MemoryLocalStore {
             )),
             proofs: Arc::new(Mutex::new(HashMap::new())),
             pending_proofs: Arc::new(Mutex::new(HashMap::new())),
+            #[cfg(feature = "nut13")]
+            keyset_counter: Arc::new(Mutex::new(keyset_counter)),
         }
     }
 }
@@ -205,4 +210,16 @@ impl LocalStore for MemoryLocalStore {
 
         Ok(())
     }
+
+    async fn add_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error> {
+        self.keyset_counter
+            .lock()
+            .await
+            .insert(keyset_id.clone(), count);
+        Ok(())
+    }
+
+    async fn get_keyset_counter(&self, id: &Id) -> Result<Option<u64>, Error> {
+        Ok(self.keyset_counter.lock().await.get(id).cloned())
+    }
 }

+ 5 - 0
crates/cashu-sdk/src/wallet/localstore/mod.rs

@@ -82,4 +82,9 @@ pub trait LocalStore {
         mint_url: UncheckedUrl,
         proofs: &Proofs,
     ) -> Result<(), Error>;
+
+    #[cfg(feature = "nut13")]
+    async fn add_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error>;
+    #[cfg(feature = "nut13")]
+    async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Error>;
 }

+ 31 - 0
crates/cashu-sdk/src/wallet/localstore/redb_store.rs

@@ -22,6 +22,8 @@ const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_
 const PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("proofs");
 const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> =
     MultimapTableDefinition::new("pending_proofs");
+#[cfg(feature = "nut13")]
+const KEYSET_COUNTER: TableDefinition<&str, u64> = TableDefinition::new("keyset_counter");
 
 #[derive(Debug, Clone)]
 pub struct RedbLocalStore {
@@ -40,6 +42,8 @@ impl RedbLocalStore {
             let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
             let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
             let _ = write_txn.open_multimap_table(PROOFS_TABLE)?;
+            #[cfg(feature = "nut13")]
+            let _ = write_txn.open_table(KEYSET_COUNTER)?;
         }
         write_txn.commit()?;
 
@@ -383,4 +387,31 @@ impl LocalStore for RedbLocalStore {
 
         Ok(())
     }
+
+    #[cfg(feature = "nut13")]
+    async fn add_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error> {
+        let db = self.db.lock().await;
+
+        let write_txn = db.begin_write()?;
+
+        {
+            let mut table = write_txn.open_table(KEYSET_COUNTER)?;
+
+            table.insert(keyset_id.to_string().as_str(), count)?;
+        }
+        write_txn.commit()?;
+
+        Ok(())
+    }
+
+    #[cfg(feature = "nut13")]
+    async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Error> {
+        let db = self.db.lock().await;
+        let read_txn = db.begin_read()?;
+        let table = read_txn.open_table(KEYSET_COUNTER)?;
+
+        let counter = table.get(keyset_id.to_string().as_str())?;
+
+        Ok(counter.map(|c| c.value()))
+    }
 }

+ 115 - 40
crates/cashu-sdk/src/wallet/mod.rs

@@ -52,22 +52,16 @@ pub enum Error {
 }
 
 #[derive(Clone, Debug)]
-pub struct BackupInfo {
-    mnemonic: Mnemonic,
-    counter: HashMap<Id, u64>,
-}
-
-#[derive(Clone, Debug)]
 pub struct Wallet<C: Client, L: LocalStore> {
     pub client: C,
     localstore: L,
-    backup_info: Option<BackupInfo>,
+    mnemonic: Option<Mnemonic>,
 }
 
 impl<C: Client, L: LocalStore> Wallet<C, L> {
-    pub async fn new(client: C, localstore: L, backup_info: Option<BackupInfo>) -> Self {
+    pub async fn new(client: C, localstore: L, mnemonic: Option<Mnemonic>) -> Self {
         Self {
-            backup_info,
+            mnemonic,
             client,
             localstore,
         }
@@ -75,12 +69,7 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
 
     /// Back up seed
     pub fn mnemonic(&self) -> Option<Mnemonic> {
-        self.backup_info.clone().map(|b| b.mnemonic)
-    }
-
-    /// Back up keyset counters
-    pub fn keyset_counters(&self) -> Option<HashMap<Id, u64>> {
-        self.backup_info.clone().map(|b| b.counter)
+        self.mnemonic.clone().map(|b| b)
     }
 
     pub async fn mint_balances(&self) -> Result<HashMap<UncheckedUrl, Amount>, Error> {
@@ -303,13 +292,25 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
 
         let active_keyset_id = self.active_mint_keyset(&mint_url, &quote_info.unit).await?;
 
-        let premint_secrets = match &self.backup_info {
-            Some(backup_info) => PreMintSecrets::from_seed(
-                active_keyset_id,
-                *backup_info.counter.get(&active_keyset_id).unwrap_or(&0),
-                &backup_info.mnemonic,
-                quote_info.amount,
-            )?,
+        let mut counter = None;
+
+        let premint_secrets = match &self.mnemonic {
+            Some(mnemonic) => {
+                let count = self
+                    .localstore
+                    .get_keyset_counter(&active_keyset_id)
+                    .await?
+                    .unwrap_or(0);
+
+                counter = Some(count);
+                PreMintSecrets::from_seed(
+                    active_keyset_id,
+                    count,
+                    &mnemonic,
+                    quote_info.amount,
+                    false,
+                )?
+            }
             None => PreMintSecrets::random(active_keyset_id, quote_info.amount)?,
         };
 
@@ -336,6 +337,14 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
         // Remove filled quote from store
         self.localstore.remove_mint_quote(&quote_info.id).await?;
 
+        // Update counter for keyset
+        if let Some(counter) = counter {
+            let count = counter + proofs.len() as u64;
+            self.localstore
+                .add_keyset_counter(&active_keyset_id, count)
+                .await?;
+        }
+
         // Add new proofs to store
         self.localstore.add_proofs(mint_url, proofs).await?;
 
@@ -407,27 +416,52 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
     ) -> Result<PreSwap, Error> {
         let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
 
-        let pre_mint_secrets = if let Some(amount) = amount {
-            let mut desired_messages = PreMintSecrets::random(active_keyset_id, amount)?;
+        // Desired amount is either amount passwed or value of all proof
+        let proofs_total = proofs.iter().map(|p| p.amount).sum();
 
-            let change_amount = proofs.iter().map(|p| p.amount).sum::<Amount>() - amount;
+        let desired_amount = amount.unwrap_or(proofs_total);
+
+        let mut counter = None;
+
+        let mut desired_messages = if let Some(mnemonic) = &self.mnemonic {
+            let count = self
+                .localstore
+                .get_keyset_counter(&active_keyset_id)
+                .await?
+                .unwrap_or(0);
+            let premint_secrets = PreMintSecrets::from_seed(
+                active_keyset_id,
+                count,
+                mnemonic,
+                desired_amount,
+                false,
+            )?;
+
+            counter = Some(count + premint_secrets.len() as u64);
+
+            premint_secrets
+        } else {
+            PreMintSecrets::random(active_keyset_id, desired_amount)?
+        };
 
-            let change_messages = PreMintSecrets::random(active_keyset_id, change_amount)?;
+        if let (Some(amt), Some(mnemonic)) = (amount, &self.mnemonic) {
+            let change_amount = proofs_total - amt;
+
+            let change_messages = if let Some(count) = counter {
+                PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, desired_amount, false)?
+            } else {
+                PreMintSecrets::random(active_keyset_id, change_amount)?
+            };
             // Combine the BlindedMessages totoalling the desired amount with change
             desired_messages.combine(change_messages);
             // Sort the premint secrets to avoid finger printing
             desired_messages.sort_secrets();
-            desired_messages
-        } else {
-            let amount = proofs.iter().map(|p| p.amount).sum();
-
-            PreMintSecrets::random(active_keyset_id, amount)?
         };
 
-        let swap_request = SwapRequest::new(proofs, pre_mint_secrets.blinded_messages());
+        let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages());
 
         Ok(PreSwap {
-            pre_mint_secrets,
+            pre_mint_secrets: desired_messages,
             swap_request,
         })
     }
@@ -439,6 +473,8 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
     ) -> Result<Proofs, Error> {
         let mut proofs = vec![];
 
+        let mut proof_count: HashMap<Id, u64> = HashMap::new();
+
         for (promise, premint) in promises.iter().zip(blinded_messages) {
             let a = self
                 .localstore
@@ -452,6 +488,10 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
             let blinded_c = promise.c.clone();
 
             let unblinded_sig = unblind_message(blinded_c, premint.r.into(), a).unwrap();
+
+            let count = proof_count.get(&promise.keyset_id).unwrap_or(&0);
+            proof_count.insert(promise.keyset_id, count + 1);
+
             let proof = Proof::new(
                 promise.amount,
                 promise.keyset_id,
@@ -462,6 +502,20 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
             proofs.push(proof);
         }
 
+        if self.mnemonic.is_some() {
+            for (keyset_id, count) in proof_count {
+                let counter = self
+                    .localstore
+                    .get_keyset_counter(&keyset_id)
+                    .await?
+                    .unwrap_or(0);
+
+                self.localstore
+                    .add_keyset_counter(&keyset_id, counter + count)
+                    .await?;
+            }
+        }
+
         Ok(proofs)
     }
 
@@ -637,10 +691,23 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
 
         let proofs_amount = proofs.iter().map(|p| p.amount).sum();
 
-        let blinded = PreMintSecrets::blank(
-            self.active_mint_keyset(mint_url, &quote_info.unit).await?,
-            proofs_amount,
-        )?;
+        let mut counter = None;
+
+        let active_keyset_id = self.active_mint_keyset(mint_url, &quote_info.unit).await?;
+
+        let premint_secrets = match &self.mnemonic {
+            Some(mnemonic) => {
+                let count = self
+                    .localstore
+                    .get_keyset_counter(&active_keyset_id)
+                    .await?
+                    .unwrap_or(0);
+
+                counter = Some(count);
+                PreMintSecrets::from_seed(active_keyset_id, count, &mnemonic, proofs_amount, true)?
+            }
+            None => PreMintSecrets::blank(active_keyset_id, proofs_amount)?,
+        };
 
         let melt_response = self
             .client
@@ -648,15 +715,15 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
                 mint_url.clone().try_into()?,
                 quote_id.to_string(),
                 proofs.clone(),
-                Some(blinded.blinded_messages()),
+                Some(premint_secrets.blinded_messages()),
             )
             .await?;
 
         let change_proofs = match melt_response.change {
             Some(change) => Some(construct_proofs(
                 change,
-                blinded.rs(),
-                blinded.secrets(),
+                premint_secrets.rs(),
+                premint_secrets.secrets(),
                 &self.active_keys(mint_url, &quote_info.unit).await?.unwrap(),
             )?),
             None => None,
@@ -669,6 +736,14 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
         };
 
         if let Some(change_proofs) = change_proofs {
+            // Update counter for keyset
+            if let Some(counter) = counter {
+                let count = counter + change_proofs.len() as u64;
+                self.localstore
+                    .add_keyset_counter(&active_keyset_id, count)
+                    .await?;
+            }
+
             self.localstore
                 .add_proofs(mint_url.clone(), change_proofs)
                 .await?;

+ 4 - 1
crates/cashu/src/nuts/nut13.rs

@@ -55,6 +55,7 @@ mod wallet {
             counter: u64,
             mnemonic: &Mnemonic,
             amount: Amount,
+            zero_amount: bool,
         ) -> Result<Self, wallet::Error> {
             let mut pre_mint_secrets = PreMintSecrets::default();
 
@@ -66,13 +67,15 @@ mod wallet {
 
                 let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor.into()))?;
 
+                let amount = if zero_amount { Amount::ZERO } else { amount };
+
                 let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
 
                 let pre_mint = PreMint {
                     blinded_message,
                     secret: secret.clone(),
                     r: r.into(),
-                    amount: Amount::ZERO,
+                    amount,
                 };
 
                 pre_mint_secrets.secrets.push(pre_mint);