From 9b5e9b2ea470db5d27ea229df84fa5e1db61e9e8 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 31 Dec 2023 16:24:22 +0000 Subject: [PATCH] feat: derive secret from path and seed --- crates/cashu-sdk/src/wallet.rs | 27 ++++++++++++++++++++--- crates/cashu/Cargo.toml | 3 +++ crates/cashu/src/amount.rs | 6 +++++ crates/cashu/src/nuts/nut00.rs | 40 ++++++++++++++++++++++++++++++++++ crates/cashu/src/nuts/nut01.rs | 21 +++++++++++++++++- crates/cashu/src/nuts/nut02.rs | 9 ++++++++ crates/cashu/src/secret.rs | 21 +++++++++++++++--- 7 files changed, 120 insertions(+), 7 deletions(-) diff --git a/crates/cashu-sdk/src/wallet.rs b/crates/cashu-sdk/src/wallet.rs index 8c4d542e..2f0592ca 100644 --- a/crates/cashu-sdk/src/wallet.rs +++ b/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), @@ -41,8 +42,15 @@ pub enum Error { Custom(String), } +#[derive(Clone, Debug)] +pub struct BackupInfo { + mnemonic: Mnemonic, + counter: HashMap, +} + #[derive(Clone, Debug)] pub struct Wallet { + backup_info: Option, pub client: C, pub mint_url: UncheckedUrl, pub mint_quotes: HashMap, @@ -57,9 +65,11 @@ impl Wallet { mint_url: UncheckedUrl, mint_quotes: Vec, melt_quotes: Vec, + backup_info: Option, mint_keys: Keys, ) -> Self { Self { + backup_info, client, mint_url, mint_keys, @@ -158,7 +168,18 @@ impl Wallet { 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 diff --git a/crates/cashu/Cargo.toml b/crates/cashu/Cargo.toml index 92dd31ae..7ba33f60 100644 --- a/crates/cashu/Cargo.toml +++ b/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"] } diff --git a/crates/cashu/src/amount.rs b/crates/cashu/src/amount.rs index cd5a1e52..8639b1cf 100644 --- a/crates/cashu/src/amount.rs +++ b/crates/cashu/src/amount.rs @@ -30,6 +30,12 @@ impl Default for Amount { } } +impl Default for &Amount { + fn default() -> Self { + &Amount::ZERO + } +} + impl From for Amount { fn from(value: u64) -> Self { Self(value) diff --git a/crates/cashu/src/nuts/nut00.rs b/crates/cashu/src/nuts/nut00.rs index 2fdb5527..786fed83 100644 --- a/crates/cashu/src/nuts/nut00.rs +++ b/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 { + 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 { self.secrets.iter() } diff --git a/crates/cashu/src/nuts/nut01.rs b/crates/cashu/src/nuts/nut01.rs index 87f39804..0825ca9b 100644 --- a/crates/cashu/src/nuts/nut01.rs +++ b/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] diff --git a/crates/cashu/src/nuts/nut02.rs b/crates/cashu/src/nuts/nut02.rs index 78147015..95819a19 100644 --- a/crates/cashu/src/nuts/nut02.rs +++ b/crates/cashu/src/nuts/nut02.rs @@ -46,6 +46,15 @@ impl Id { const STRLEN: usize = 14; } +impl From 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!( diff --git a/crates/cashu/src/secret.rs b/crates/cashu/src/secret.rs index e3785146..b089d1f8 100644 --- a/crates/cashu/src/secret.rs +++ b/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() }