From a0a3a80377837713e99dc2159b4a8ca475c1d2ba Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 4 Mar 2024 22:36:16 +0000 Subject: [PATCH] feat: r and secret from seed --- crates/cashu/Cargo.toml | 8 +- crates/cashu/src/nuts/mod.rs | 2 + crates/cashu/src/nuts/nut00.rs | 38 +--------- crates/cashu/src/nuts/nut01.rs | 22 +----- crates/cashu/src/nuts/nut02.rs | 24 ++++-- crates/cashu/src/nuts/nut13.rs | 134 +++++++++++++++++++++++++++++++++ crates/cashu/src/secret.rs | 19 +---- 7 files changed, 162 insertions(+), 85 deletions(-) create mode 100644 crates/cashu/src/nuts/nut13.rs diff --git a/crates/cashu/Cargo.toml b/crates/cashu/Cargo.toml index b42a3904..9e01e5f7 100644 --- a/crates/cashu/Cargo.toml +++ b/crates/cashu/Cargo.toml @@ -15,20 +15,20 @@ description = "Cashu rust wallet and mint library" default = ["mint", "wallet", "all-nuts"] mint = [] wallet = [] -all-nuts = ["nut07", "nut08", "nut09", "nut10", "nut11"] +all-nuts = ["nut07", "nut08", "nut09", "nut10", "nut11", "nut13"] nut07 = [] nut08 = [] nut09 = [] nut10 = [] nut11 = ["nut10"] +nut13 = ["dep:bip39", "dep:bip32", "nut09"] [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" +bip39 = { version = "2.0.0", optional = true } +bip32 = { version = "0.5.1", optional = true } 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/nuts/mod.rs b/crates/cashu/src/nuts/mod.rs index ca299de5..201424d3 100644 --- a/crates/cashu/src/nuts/mod.rs +++ b/crates/cashu/src/nuts/mod.rs @@ -15,6 +15,8 @@ pub mod nut09; pub mod nut10; #[cfg(feature = "nut11")] pub mod nut11; +#[cfg(feature = "nut13")] +pub mod nut13; #[cfg(feature = "wallet")] pub use nut00::wallet::{PreMint, PreMintSecrets, Token}; diff --git a/crates/cashu/src/nuts/nut00.rs b/crates/cashu/src/nuts/nut00.rs index f1dd5826..e7516dba 100644 --- a/crates/cashu/src/nuts/nut00.rs +++ b/crates/cashu/src/nuts/nut00.rs @@ -116,7 +116,6 @@ pub mod wallet { use base64::engine::{general_purpose, GeneralPurpose}; use base64::{alphabet, Engine as _}; - use bip39::Mnemonic; use serde::{Deserialize, Serialize}; use url::Url; @@ -156,7 +155,7 @@ pub mod wallet { #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] pub struct PreMintSecrets { - secrets: Vec, + pub secrets: Vec, } impl PreMintSecrets { @@ -229,41 +228,6 @@ 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.to_bytes(), Some(blinding_factor.into()))?; - - let blinded_message = BlindedMessage::new(amount, keyset_id, 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) - } - #[cfg(feature = "nut11")] pub fn with_p2pk_conditions( keyset_id: Id, diff --git a/crates/cashu/src/nuts/nut01.rs b/crates/cashu/src/nuts/nut01.rs index ebd6614e..0fdf3158 100644 --- a/crates/cashu/src/nuts/nut01.rs +++ b/crates/cashu/src/nuts/nut01.rs @@ -4,14 +4,12 @@ use std::collections::{BTreeMap, HashMap}; use std::str::FromStr; -use bip32::{DerivationPath, XPrv}; -use bip39::Mnemonic; use k256::elliptic_curve::generic_array::GenericArray; #[cfg(feature = "nut11")] use k256::schnorr::{SigningKey, VerifyingKey}; use serde::{Deserialize, Serialize}; -use super::{Id, KeySet}; +use super::KeySet; use crate::error::Error; use crate::Amount; @@ -99,7 +97,7 @@ impl std::fmt::Display for PublicKey { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] -pub struct SecretKey(#[serde(with = "crate::serde_utils::serde_secret_key")] k256::SecretKey); +pub struct SecretKey(#[serde(with = "crate::serde_utils::serde_secret_key")] pub k256::SecretKey); impl From for k256::SecretKey { fn from(value: SecretKey) -> k256::SecretKey { @@ -136,22 +134,6 @@ impl SecretKey { 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()) - } - pub fn random() -> Self { let mut rng = rand::thread_rng(); SecretKey(k256::SecretKey::random(&mut rng)) diff --git a/crates/cashu/src/nuts/nut02.rs b/crates/cashu/src/nuts/nut02.rs index 74ef777f..292e8ca1 100644 --- a/crates/cashu/src/nuts/nut02.rs +++ b/crates/cashu/src/nuts/nut02.rs @@ -47,12 +47,16 @@ 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 TryFrom for u64 { + type Error = Error; + fn try_from(value: Id) -> Result { + let hex_bytes: [u8; 8] = hex::decode(value.to_string())? + .try_into() + .map_err(|_| Error::Length)?; + + let int = u64::from_be_bytes(hex_bytes); + + Ok(int % (2_u64.pow(31) - 1)) } } @@ -406,4 +410,12 @@ mod test { let _keyset_response: KeysetResponse = serde_json::from_str(h).unwrap(); } + + #[test] + fn test_to_int() { + let id = Id::from_str("009a1f293253e41e").unwrap(); + + let id_int = u64::try_from(id).unwrap(); + assert_eq!(864559728, id_int) + } } diff --git a/crates/cashu/src/nuts/nut13.rs b/crates/cashu/src/nuts/nut13.rs new file mode 100644 index 00000000..429f6a14 --- /dev/null +++ b/crates/cashu/src/nuts/nut13.rs @@ -0,0 +1,134 @@ +use std::str::FromStr; + +use bip32::{DerivationPath, XPrv}; +use bip39::Mnemonic; + +use super::{Id, SecretKey}; +use crate::error::Error; +use crate::secret::Secret; + +impl Secret { + pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Result { + let path = DerivationPath::from_str(&format!( + "m/129372'/0'/{}'/{}'/0", + u64::try_from(keyset_id).unwrap(), + counter + )) + .unwrap(); + + let xpriv = XPrv::derive_from_path(mnemonic.to_seed(""), &path).unwrap(); + + Ok(Self(hex::encode(xpriv.private_key().to_bytes()))) + } +} + +impl SecretKey { + pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Result { + let path = DerivationPath::from_str(&format!( + "m/129372'/0'/{}'/{}'/1", + u64::try_from(keyset_id).unwrap(), + counter + )) + .unwrap(); + + let signing_key = XPrv::derive_from_path(mnemonic.to_seed(""), &path).unwrap(); + + let private_key = signing_key.private_key(); + + Ok(Self(private_key.into())) + } +} + +#[cfg(feature = "wallet")] +mod wallet { + use bip39::Mnemonic; + + use crate::dhke::blind_message; + use crate::error::wallet; + use crate::nuts::{BlindedMessage, Id, PreMint, PreMintSecrets, SecretKey}; + use crate::secret::Secret; + use crate::Amount; + + impl PreMintSecrets { + /// Generate blinded messages from predetermined secrets and blindings + /// factor + 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.to_bytes(), Some(blinding_factor.into()))?; + + let blinded_message = BlindedMessage::new(amount, keyset_id, 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) + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_secret_from_seed() { + let seed = + "half depart obvious quality work element tank gorilla view sugar picture humble"; + let mnemonic = Mnemonic::from_str(seed).unwrap(); + let keyset_id = Id::from_str("009a1f293253e41e").unwrap(); + + let test_secrets = [ + "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae", + "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270", + "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8", + "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf", + "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0", + ]; + + for (i, test_secret) in test_secrets.iter().enumerate() { + let secret = Secret::from_seed(&mnemonic, keyset_id, i.try_into().unwrap()).unwrap(); + assert_eq!(secret, Secret::from_str(test_secret).unwrap()) + } + } + #[test] + fn test_r_from_seed() { + let seed = + "half depart obvious quality work element tank gorilla view sugar picture humble"; + let mnemonic = Mnemonic::from_str(seed).unwrap(); + let keyset_id = Id::from_str("009a1f293253e41e").unwrap(); + + let test_rs = [ + "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679", + "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248", + "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899", + "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29", + "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9", + ]; + + for (i, test_r) in test_rs.iter().enumerate() { + let r = SecretKey::from_seed(&mnemonic, keyset_id, i.try_into().unwrap()).unwrap(); + assert_eq!(r, SecretKey::from_hex(test_r).unwrap()) + } + } +} diff --git a/crates/cashu/src/secret.rs b/crates/cashu/src/secret.rs index bdc6380d..188031f0 100644 --- a/crates/cashu/src/secret.rs +++ b/crates/cashu/src/secret.rs @@ -2,17 +2,13 @@ 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)] -pub struct Secret(String); +pub struct Secret(pub String); #[derive(Debug, Error)] pub enum Error { @@ -45,19 +41,6 @@ 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 to_bytes(&self) -> Vec { self.0.clone().into_bytes() }