瀏覽代碼

feat: derive secret from path and seed

thesimplekid 1 年之前
父節點
當前提交
9b5e9b2ea4

+ 24 - 3
crates/cashu-sdk/src/wallet.rs

@@ -2,11 +2,12 @@
 use std::collections::HashMap;
 use std::str::FromStr;
 
+use bip39::Mnemonic;
 use cashu::dhke::{construct_proofs, unblind_message};
 #[cfg(feature = "nut07")]
 use cashu::nuts::nut00::mint;
 use cashu::nuts::{
-    BlindedSignature, CurrencyUnit, Keys, PreMintSecrets, PreSwap, Proof, Proofs, SwapRequest,
+    BlindedSignature, CurrencyUnit, Id, Keys, PreMintSecrets, PreSwap, Proof, Proofs, SwapRequest,
     Token,
 };
 #[cfg(feature = "nut07")]
@@ -24,7 +25,7 @@ use crate::utils::unix_time;
 #[derive(Debug, Error)]
 pub enum Error {
     /// Insufficient Funds
-    #[error("Insuddicient Funds")]
+    #[error("Insufficient Funds")]
     InsufficientFunds,
     #[error("`{0}`")]
     Cashu(#[from] cashu::error::wallet::Error),
@@ -42,7 +43,14 @@ pub enum Error {
 }
 
 #[derive(Clone, Debug)]
+pub struct BackupInfo {
+    mnemonic: Mnemonic,
+    counter: HashMap<Id, u64>,
+}
+
+#[derive(Clone, Debug)]
 pub struct Wallet<C: Client> {
+    backup_info: Option<BackupInfo>,
     pub client: C,
     pub mint_url: UncheckedUrl,
     pub mint_quotes: HashMap<String, MintQuote>,
@@ -57,9 +65,11 @@ impl<C: Client> Wallet<C> {
         mint_url: UncheckedUrl,
         mint_quotes: Vec<MintQuote>,
         melt_quotes: Vec<MeltQuote>,
+        backup_info: Option<BackupInfo>,
         mint_keys: Keys,
     ) -> Self {
         Self {
+            backup_info,
             client,
             mint_url,
             mint_keys,
@@ -158,7 +168,18 @@ impl<C: Client> Wallet<C> {
             return Err(Error::QuoteUnknown);
         };
 
-        let premint_secrets = PreMintSecrets::random((&self.mint_keys).into(), quote_info.amount)?;
+        let premint_secrets = match &self.backup_info {
+            Some(backup_info) => PreMintSecrets::from_seed(
+                Id::from(&self.mint_keys),
+                *backup_info
+                    .counter
+                    .get(&Id::from(&self.mint_keys))
+                    .unwrap_or(&0),
+                &backup_info.mnemonic,
+                quote_info.amount,
+            )?,
+            None => PreMintSecrets::random((&self.mint_keys).into(), quote_info.amount)?,
+        };
 
         let mint_res = self
             .client

+ 3 - 0
crates/cashu/Cargo.toml

@@ -23,6 +23,9 @@ nut08 = []
 [dependencies]
 base64 = "0.21.0"
 bitcoin = { version = "0.30.0", features=["serde",  "rand"] }
+# TODO: Should be optional
+bip39 = "2.0.0"
+bip32 = "0.5.1"
 hex = "0.4.3"
 k256 = { version = "0.13.1", features=["arithmetic", "serde", "schnorr"] }
 lightning-invoice = { version = "0.25.0", features=["serde"] }

+ 6 - 0
crates/cashu/src/amount.rs

@@ -30,6 +30,12 @@ impl Default for Amount {
     }
 }
 
+impl Default for &Amount {
+    fn default() -> Self {
+        &Amount::ZERO
+    }
+}
+
 impl From<u64> for Amount {
     fn from(value: u64) -> Self {
         Self(value)

+ 40 - 0
crates/cashu/src/nuts/nut00.rs

@@ -92,6 +92,7 @@ pub mod wallet {
 
     use base64::engine::{general_purpose, GeneralPurpose};
     use base64::{alphabet, Engine as _};
+    use bip39::Mnemonic;
     use serde::{Deserialize, Serialize};
     use url::Url;
 
@@ -209,6 +210,45 @@ pub mod wallet {
             Ok(PreMintSecrets { secrets: output })
         }
 
+        /// Generate blinded messages from predetermined secrets and blindings
+        /// factor
+        /// TODO: Put behind feature
+        pub fn from_seed(
+            keyset_id: Id,
+            counter: u64,
+            mnemonic: &Mnemonic,
+            amount: Amount,
+        ) -> Result<Self, wallet::Error> {
+            let mut pre_mint_secrets = PreMintSecrets::default();
+
+            let mut counter = counter;
+
+            for amount in amount.split() {
+                let secret = Secret::from_seed(&mnemonic, keyset_id, counter);
+                let blinding_factor = SecretKey::from_seed(&mnemonic, keyset_id, counter);
+
+                let (blinded, r) = blind_message(secret.as_bytes(), Some(blinding_factor.into()))?;
+
+                let blinded_message = BlindedMessage {
+                    keyset_id,
+                    amount,
+                    b: blinded,
+                };
+
+                let pre_mint = PreMint {
+                    blinded_message,
+                    secret: secret.clone(),
+                    r: r.into(),
+                    amount: Amount::ZERO,
+                };
+
+                pre_mint_secrets.secrets.push(pre_mint);
+                counter += 1;
+            }
+
+            Ok(pre_mint_secrets)
+        }
+
         pub fn iter(&self) -> impl Iterator<Item = &PreMint> {
             self.secrets.iter()
         }

+ 20 - 1
crates/cashu/src/nuts/nut01.rs

@@ -2,10 +2,13 @@
 // https://github.com/cashubtc/nuts/blob/main/01.md
 
 use std::collections::{BTreeMap, HashMap};
+use std::str::FromStr;
 
+use bip32::{DerivationPath, XPrv};
+use bip39::Mnemonic;
 use serde::{Deserialize, Serialize};
 
-use super::KeySet;
+use super::{Id, KeySet};
 use crate::error::Error;
 use crate::Amount;
 
@@ -79,6 +82,22 @@ impl SecretKey {
     pub fn public_key(&self) -> PublicKey {
         self.0.public_key().into()
     }
+
+    // TODO: put behind feature
+    pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Self {
+        let path = DerivationPath::from_str(&format!(
+            "m/129372'/0'/{}'/{}'/1",
+            u64::from(keyset_id),
+            counter
+        ))
+        .unwrap();
+
+        let signing_key = XPrv::derive_from_path(mnemonic.to_seed(""), &path).unwrap();
+
+        let private_key = signing_key.private_key();
+
+        Self(private_key.into())
+    }
 }
 
 /// Mint Keys [NUT-01]

+ 9 - 0
crates/cashu/src/nuts/nut02.rs

@@ -46,6 +46,15 @@ impl Id {
     const STRLEN: usize = 14;
 }
 
+impl From<Id> for u64 {
+    fn from(value: Id) -> Self {
+        value
+            .id
+            .iter()
+            .fold(0, |acc, &byte| (acc << 8) | u64::from(byte))
+    }
+}
+
 impl std::fmt::Display for Id {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.write_str(&format!(

+ 18 - 3
crates/cashu/src/secret.rs

@@ -1,12 +1,14 @@
-// MIT License
-// Copyright (c) 2023 Clark Moody
-// https://github.com/clarkmoody/cashu-rs/blob/master/src/secret.rs
+//! Secret
 
 use std::str::FromStr;
 
+use bip32::{DerivationPath, XPrv};
+use bip39::Mnemonic;
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
+use crate::nuts::Id;
+
 /// The secret data that allows spending ecash
 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 #[serde(transparent)]
@@ -41,6 +43,19 @@ impl Secret {
         Self(secret)
     }
 
+    pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Self {
+        let path = DerivationPath::from_str(&format!(
+            "m/129372'/0'/{}'/{}'/0",
+            u64::from(keyset_id),
+            counter
+        ))
+        .unwrap();
+
+        let xpriv = XPrv::derive_from_path(mnemonic.to_seed(""), &path).unwrap();
+
+        Self(hex::encode(xpriv.private_key().to_bytes()))
+    }
+
     pub fn as_bytes(&self) -> &[u8] {
         self.0.as_bytes()
     }