From a5a50f281c8ad0bb429da1eebe137a11f9086d1d Mon Sep 17 00:00:00 2001 From: David Caseria Date: Wed, 24 Apr 2024 14:22:52 -0400 Subject: [PATCH] refactor: Mint to use bip32 derivation and not store priv keys --- crates/cdk-redb/src/mint.rs | 13 +- crates/cdk/src/cdk_database/mint_memory.rs | 12 +- crates/cdk/src/cdk_database/mod.rs | 25 ++- crates/cdk/src/mint.rs | 187 +++++++++++++-------- crates/cdk/src/nuts/mod.rs | 4 +- crates/cdk/src/nuts/nut02.rs | 86 +++++++--- 6 files changed, 206 insertions(+), 121 deletions(-) diff --git a/crates/cdk-redb/src/mint.rs b/crates/cdk-redb/src/mint.rs index 2b953766..a356d261 100644 --- a/crates/cdk-redb/src/mint.rs +++ b/crates/cdk-redb/src/mint.rs @@ -5,9 +5,8 @@ use std::sync::Arc; use async_trait::async_trait; use cdk::cdk_database::{self, MintDatabase}; use cdk::dhke::hash_to_curve; -use cdk::nuts::{ - BlindSignature, CurrencyUnit, Id, MintInfo, MintKeySet as KeySet, Proof, PublicKey, -}; +use cdk::mint::MintKeySetInfo; +use cdk::nuts::{BlindSignature, CurrencyUnit, Id, MintInfo, Proof, PublicKey}; use cdk::secret::Secret; use cdk::types::{MeltQuote, MintQuote}; use redb::{Database, ReadableTable, TableDefinition}; @@ -167,7 +166,7 @@ impl MintDatabase for MintRedbDatabase { Ok(active_keysets) } - async fn add_keyset(&self, keyset: KeySet) -> Result<(), Self::Err> { + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> { let db = self.db.lock().await; let write_txn = db.begin_write().map_err(Error::from)?; @@ -176,7 +175,7 @@ impl MintDatabase for MintRedbDatabase { let mut table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; table .insert( - Id::from(keyset.clone()).to_string().as_str(), + keyset.id.to_string().as_str(), serde_json::to_string(&keyset) .map_err(Error::from)? .as_str(), @@ -188,7 +187,7 @@ impl MintDatabase for MintRedbDatabase { Ok(()) } - async fn get_keyset(&self, keyset_id: &Id) -> Result, Self::Err> { + async fn get_keyset_info(&self, keyset_id: &Id) -> Result, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Error::from)?; let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; @@ -202,7 +201,7 @@ impl MintDatabase for MintRedbDatabase { } } - async fn get_keysets(&self) -> Result, Self::Err> { + async fn get_keyset_infos(&self) -> Result, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Error::from)?; let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; diff --git a/crates/cdk/src/cdk_database/mint_memory.rs b/crates/cdk/src/cdk_database/mint_memory.rs index c6a9ac61..1bb018a5 100644 --- a/crates/cdk/src/cdk_database/mint_memory.rs +++ b/crates/cdk/src/cdk_database/mint_memory.rs @@ -6,7 +6,7 @@ use tokio::sync::Mutex; use super::{Error, MintDatabase}; use crate::dhke::hash_to_curve; -use crate::nuts::nut02::MintKeySet; +use crate::mint::MintKeySetInfo; use crate::nuts::{BlindSignature, CurrencyUnit, Id, MintInfo, Proof, Proofs, PublicKey}; use crate::secret::Secret; use crate::types::{MeltQuote, MintQuote}; @@ -15,7 +15,7 @@ use crate::types::{MeltQuote, MintQuote}; pub struct MintMemoryDatabase { mint_info: Arc>, active_keysets: Arc>>, - keysets: Arc>>, + keysets: Arc>>, mint_quotes: Arc>>, melt_quotes: Arc>>, pending_proofs: Arc>>, @@ -28,7 +28,7 @@ impl MintMemoryDatabase { pub fn new( mint_info: MintInfo, active_keysets: HashMap, - keysets: Vec, + keysets: Vec, mint_quotes: Vec, melt_quotes: Vec, pending_proofs: Proofs, @@ -87,16 +87,16 @@ impl MintDatabase for MintMemoryDatabase { Ok(self.active_keysets.lock().await.clone()) } - async fn add_keyset(&self, keyset: MintKeySet) -> Result<(), Error> { + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Error> { self.keysets.lock().await.insert(keyset.id, keyset); Ok(()) } - async fn get_keyset(&self, keyset_id: &Id) -> Result, Error> { + async fn get_keyset_info(&self, keyset_id: &Id) -> Result, Error> { Ok(self.keysets.lock().await.get(keyset_id).cloned()) } - async fn get_keysets(&self) -> Result, Error> { + async fn get_keyset_infos(&self) -> Result, Error> { Ok(self.keysets.lock().await.values().cloned().collect()) } diff --git a/crates/cdk/src/cdk_database/mod.rs b/crates/cdk/src/cdk_database/mod.rs index a9424654..9f78748b 100644 --- a/crates/cdk/src/cdk_database/mod.rs +++ b/crates/cdk/src/cdk_database/mod.rs @@ -1,16 +1,25 @@ //! CDK Database +#[cfg(any(feature = "wallet", feature = "mint"))] use std::collections::HashMap; +#[cfg(any(feature = "wallet", feature = "mint"))] use async_trait::async_trait; use thiserror::Error; -use crate::nuts::{ - BlindSignature, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, MintKeySet, Proof, Proofs, - PublicKey, -}; +#[cfg(feature = "mint")] +use crate::mint::MintKeySetInfo; +#[cfg(feature = "mint")] +use crate::nuts::{BlindSignature, CurrencyUnit, Proof, PublicKey}; +#[cfg(any(feature = "wallet", feature = "mint"))] +use crate::nuts::{Id, MintInfo}; +#[cfg(feature = "wallet")] +use crate::nuts::{KeySetInfo, Keys, Proofs}; +#[cfg(feature = "mint")] use crate::secret::Secret; +#[cfg(any(feature = "wallet", feature = "mint"))] use crate::types::{MeltQuote, MintQuote}; +#[cfg(feature = "wallet")] use crate::url::UncheckedUrl; #[cfg(feature = "mint")] @@ -26,6 +35,7 @@ pub enum Error { Cdk(#[from] crate::error::Error), } +#[cfg(feature = "wallet")] #[async_trait] pub trait WalletDatabase { type Err: Into + From; @@ -81,6 +91,7 @@ pub trait WalletDatabase { async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err>; } +#[cfg(feature = "mint")] #[async_trait] pub trait MintDatabase { type Err: Into + From; @@ -102,9 +113,9 @@ pub trait MintDatabase { async fn get_melt_quotes(&self) -> Result, Self::Err>; async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>; - async fn add_keyset(&self, keyset: MintKeySet) -> Result<(), Self::Err>; - async fn get_keyset(&self, id: &Id) -> Result, Self::Err>; - async fn get_keysets(&self) -> Result, Self::Err>; + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>; + async fn get_keyset_info(&self, id: &Id) -> Result, Self::Err>; + async fn get_keyset_infos(&self) -> Result, Self::Err>; async fn add_spent_proof(&self, proof: Proof) -> Result<(), Self::Err>; async fn get_spent_proof_by_secret(&self, secret: &Secret) -> Result, Self::Err>; diff --git a/crates/cdk/src/mint.rs b/crates/cdk/src/mint.rs index fa20f455..b533e9e6 100644 --- a/crates/cdk/src/mint.rs +++ b/crates/cdk/src/mint.rs @@ -1,10 +1,12 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use bip39::Mnemonic; +use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}; +use bitcoin::secp256k1::{self, Secp256k1}; use http::StatusCode; use serde::{Deserialize, Serialize}; use thiserror::Error; +use tokio::sync::RwLock; use tracing::{debug, error, info}; use crate::cdk_database::{self, MintDatabase}; @@ -12,6 +14,7 @@ use crate::dhke::{hash_to_curve, sign_message, verify_message}; use crate::error::ErrorResponse; use crate::nuts::*; use crate::types::{MeltQuote, MintQuote}; +use crate::util::unix_time; use crate::Amount; #[derive(Debug, Error)] @@ -83,52 +86,43 @@ impl From for (StatusCode, ErrorResponse) { #[derive(Clone)] pub struct Mint { - // pub pubkey: PublicKey - mnemonic: Mnemonic, + keysets: Arc>>, + secp_ctx: Secp256k1, + xpriv: ExtendedPrivKey, pub fee_reserve: FeeReserve, pub localstore: Arc + Send + Sync>, } impl Mint { pub async fn new( + seed: &[u8], localstore: Arc + Send + Sync>, - mnemonic: Mnemonic, - keysets_info: HashSet, min_fee_reserve: Amount, percent_fee_reserve: f32, ) -> Result { - let mut active_units: HashSet = HashSet::default(); + let secp_ctx = Secp256k1::new(); + let xpriv = + ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); + let mut keysets = HashMap::new(); + let keysets_info = localstore.get_keyset_infos().await?; if keysets_info.is_empty() { - let keyset = - MintKeySet::generate(&mnemonic.to_seed_normalized(""), CurrencyUnit::Sat, "", 64); - - localstore - .add_active_keyset(CurrencyUnit::Sat, keyset.id) - .await?; - localstore.add_keyset(keyset).await?; - } else { - // Check that there is only one active keyset per unit - for keyset_info in keysets_info { - if keyset_info.active && !active_units.insert(keyset_info.unit.clone()) { - // TODO: Handle Error - todo!() - } - - let keyset = MintKeySet::generate( - &mnemonic.to_seed_normalized(""), - keyset_info.unit.clone(), - &keyset_info.derivation_path.clone(), - keyset_info.max_order, - ); - - localstore.add_keyset(keyset).await?; - } + let derivation_path = DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(0).expect("0 is a valid index") + ]); + let (keyset, keyset_info) = + create_new_keyset(&secp_ctx, xpriv, derivation_path, CurrencyUnit::Sat, 64); + let id = keyset_info.id; + localstore.add_active_keyset(CurrencyUnit::Sat, id).await?; + localstore.add_keyset_info(keyset_info).await?; + keysets.insert(id, keyset); } Ok(Self { + keysets: Arc::new(RwLock::new(keysets)), + secp_ctx, + xpriv, localstore, - mnemonic, fee_reserve: FeeReserve { min_fee_reserve, percent_fee_reserve, @@ -199,13 +193,9 @@ impl Mint { /// Retrieve the public keys of the active keyset for distribution to /// wallet clients pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result { - let keyset = match self.localstore.get_keyset(keyset_id).await? { - Some(keyset) => keyset.clone(), - None => { - return Err(Error::UnknownKeySet); - } - }; - + self.ensure_keyset_loaded(keyset_id).await?; + let keysets = self.keysets.read().await; + let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?.clone(); Ok(KeysResponse { keysets: vec![keyset.into()], }) @@ -214,16 +204,19 @@ impl Mint { /// Retrieve the public keys of the active keyset for distribution to /// wallet clients pub async fn pubkeys(&self) -> Result { - let keysets = self.localstore.get_keysets().await?; - + let keyset_infos = self.localstore.get_keyset_infos().await?; + for keyset_info in keyset_infos { + self.ensure_keyset_loaded(&keyset_info.id).await?; + } + let keysets = self.keysets.read().await; Ok(KeysResponse { - keysets: keysets.into_iter().map(|k| k.into()).collect(), + keysets: keysets.values().map(|k| k.clone().into()).collect(), }) } /// Return a list of all supported keysets pub async fn keysets(&self) -> Result { - let keysets = self.localstore.get_keysets().await?; + let keysets = self.localstore.get_keyset_infos().await?; let active_keysets: HashSet = self .localstore .get_active_keysets() @@ -245,11 +238,10 @@ impl Mint { } pub async fn keyset(&self, id: &Id) -> Result, Error> { - Ok(self - .localstore - .get_keyset(id) - .await? - .map(|ks| ks.clone().into())) + self.ensure_keyset_loaded(id).await?; + let keysets = self.keysets.read().await; + let keyset = keysets.get(id).map(|k| k.clone().into()); + Ok(keyset) } /// Add current keyset to inactive keysets @@ -257,21 +249,22 @@ impl Mint { pub async fn rotate_keyset( &mut self, unit: CurrencyUnit, - derivation_path: &str, + derivation_path: DerivationPath, max_order: u8, ) -> Result<(), Error> { - let new_keyset = MintKeySet::generate( - &self.mnemonic.to_seed_normalized(""), - unit.clone(), + let (keyset, keyset_info) = create_new_keyset( + &self.secp_ctx, + self.xpriv, derivation_path, + unit.clone(), max_order, ); + let id = keyset_info.id; + self.localstore.add_keyset_info(keyset_info).await?; + self.localstore.add_active_keyset(unit, id).await?; - self.localstore.add_keyset(new_keyset.clone()).await?; - - self.localstore - .add_active_keyset(unit, new_keyset.id) - .await?; + let mut keysets = self.keysets.write().await; + keysets.insert(id, keyset); Ok(()) } @@ -327,24 +320,27 @@ impl Mint { keyset_id, .. } = blinded_message; + self.ensure_keyset_loaded(keyset_id).await?; - let keyset = self + let keyset_info = self .localstore - .get_keyset(keyset_id) + .get_keyset_info(keyset_id) .await? .ok_or(Error::UnknownKeySet)?; let active = self .localstore - .get_active_keyset_id(&keyset.unit) + .get_active_keyset_id(&keyset_info.unit) .await? .ok_or(Error::InactiveKeyset)?; // Check that the keyset is active and should be used to sign - if keyset.id.ne(&active) { + if keyset_info.id.ne(&active) { return Err(Error::InactiveKeyset); } + let keysets = self.keysets.read().await; + let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?; let Some(key_pair) = keyset.keys.get(amount) else { // No key for amount return Err(Error::AmountKey); @@ -355,7 +351,7 @@ impl Mint { let blinded_signature = BlindSignature::new( *amount, c, - keyset.id, + keyset_info.id, &blinded_message.blinded_secret, key_pair.secret_key.clone(), )?; @@ -416,7 +412,7 @@ impl Mint { for id in input_keyset_ids { let keyset = self .localstore - .get_keyset(&id) + .get_keyset_info(&id) .await? .ok_or(Error::UnknownKeySet)?; keyset_units.insert(keyset.unit); @@ -428,7 +424,7 @@ impl Mint { for id in &output_keyset_ids { let keyset = self .localstore - .get_keyset(id) + .get_keyset_info(id) .await? .ok_or(Error::UnknownKeySet)?; @@ -483,12 +479,9 @@ impl Mint { return Err(Error::TokenPending); } - let keyset = self - .localstore - .get_keyset(&proof.keyset_id) - .await? - .ok_or(Error::UnknownKeySet)?; - + self.ensure_keyset_loaded(&proof.keyset_id).await?; + let keysets = self.keysets.read().await; + let keyset = keysets.get(&proof.keyset_id).ok_or(Error::UnknownKeySet)?; let Some(keypair) = keyset.keys.get(&proof.amount) else { return Err(Error::AmountKey); }; @@ -552,7 +545,7 @@ impl Mint { for id in input_keyset_ids { let keyset = self .localstore - .get_keyset(&id) + .get_keyset_info(&id) .await? .ok_or(Error::UnknownKeySet)?; keyset_units.insert(keyset.unit); @@ -563,7 +556,7 @@ impl Mint { for id in output_keysets_ids { let keyset = self .localstore - .get_keyset(&id) + .get_keyset_info(&id) .await? .ok_or(Error::UnknownKeySet)?; @@ -734,6 +727,27 @@ impl Mint { signatures, }) } + + async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> { + let keysets = self.keysets.read().await; + if keysets.contains_key(id) { + return Ok(()); + } + + let mut keysets = self.keysets.write().await; + let keyset_info = self + .localstore + .get_keyset_info(id) + .await? + .ok_or(Error::UnknownKeySet)?; + let id = keyset_info.id; + keysets.insert(id, self.generate_keyset(keyset_info)); + Ok(()) + } + + fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet { + MintKeySet::generate_from_xpriv(&self.secp_ctx, self.xpriv, keyset_info) + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -749,7 +763,7 @@ pub struct MintKeySetInfo { pub active: bool, pub valid_from: u64, pub valid_to: Option, - pub derivation_path: String, + pub derivation_path: DerivationPath, pub max_order: u8, } @@ -762,3 +776,30 @@ impl From for KeySetInfo { } } } + +fn create_new_keyset( + secp: &secp256k1::Secp256k1, + xpriv: ExtendedPrivKey, + derivation_path: DerivationPath, + unit: CurrencyUnit, + max_order: u8, +) -> (MintKeySet, MintKeySetInfo) { + let keyset = MintKeySet::generate( + secp, + xpriv + .derive_priv(secp, &derivation_path) + .expect("RNG busted"), + unit, + max_order, + ); + let keyset_info = MintKeySetInfo { + id: keyset.id, + unit: keyset.unit.clone(), + active: true, + valid_from: unix_time(), + valid_to: None, + derivation_path, + max_order, + }; + (keyset, keyset_info) +} diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index e97cf1c3..5a546352 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -19,7 +19,9 @@ pub use nut00::{ Proofs, Token, }; pub use nut01::{Keys, KeysResponse, PublicKey, SecretKey}; -pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse, MintKeySet}; +#[cfg(feature = "mint")] +pub use nut02::MintKeySet; +pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse}; #[cfg(feature = "wallet")] pub use nut03::PreSwap; pub use nut03::{SwapRequest, SwapResponse}; diff --git a/crates/cdk/src/nuts/nut02.rs b/crates/cdk/src/nuts/nut02.rs index 9f23af4d..77ad9eb9 100644 --- a/crates/cdk/src/nuts/nut02.rs +++ b/crates/cdk/src/nuts/nut02.rs @@ -5,17 +5,29 @@ use core::fmt; use core::str::FromStr; use std::array::TryFromSliceError; +#[cfg(feature = "mint")] use std::collections::BTreeMap; +#[cfg(feature = "mint")] +use bitcoin::bip32::{ChildNumber, ExtendedPrivKey}; use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::{Hash, HashEngine}; +use bitcoin::hashes::Hash; +#[cfg(feature = "mint")] +use bitcoin::key::Secp256k1; +#[cfg(feature = "mint")] +use bitcoin::secp256k1; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::{serde_as, VecSkipError}; use thiserror::Error; -use super::nut01::{Keys, MintKeyPair, MintKeys, SecretKey}; +use super::nut01::Keys; +#[cfg(feature = "mint")] +use super::nut01::{MintKeyPair, MintKeys}; +#[cfg(feature = "mint")] +use crate::mint::MintKeySetInfo; use crate::nuts::nut00::CurrencyUnit; use crate::util::hex; +#[cfg(feature = "mint")] use crate::Amount; #[derive(Debug, Error)] @@ -220,6 +232,7 @@ pub struct KeySet { pub keys: Keys, } +#[cfg(feature = "mint")] impl From for KeySet { fn from(keyset: MintKeySet) -> Self { Self { @@ -247,6 +260,7 @@ impl From for KeySetInfo { } } +#[cfg(feature = "mint")] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MintKeySet { pub id: Id, @@ -254,49 +268,66 @@ pub struct MintKeySet { pub keys: MintKeys, } +#[cfg(feature = "mint")] impl MintKeySet { - pub fn generate( - secret: &[u8], + pub fn generate( + secp: &Secp256k1, + xpriv: ExtendedPrivKey, unit: CurrencyUnit, - derivation_path: &str, max_order: u8, ) -> Self { - // Elliptic curve math context - - /* NUT-02 ยง 2.1 - for i in range(MAX_ORDER): - k_i = HASH_SHA256(s + D + i)[:32] - */ - let mut map = BTreeMap::new(); - - // SHA-256 midstate, for quicker hashing - let mut engine = Sha256::engine(); - engine.input(secret); - engine.input(derivation_path.as_bytes()); - for i in 0..max_order { let amount = Amount::from(2_u64.pow(i as u32)); - - // Reuse midstate - let mut e = engine.clone(); - e.input(i.to_string().as_bytes()); - let hash = Sha256::from_engine(e); - let secret_key = SecretKey::from_slice(&hash.to_byte_array()).unwrap(); // TODO: remove unwrap - let keypair = MintKeyPair::from_secret_key(secret_key); - map.insert(amount, keypair); + let secret_key = xpriv + .derive_priv( + secp, + &[ChildNumber::from_hardened_idx(i as u32).expect("order is valid index")], + ) + .expect("RNG busted") + .private_key; + let public_key = secret_key.public_key(secp); + map.insert( + amount, + MintKeyPair { + secret_key: secret_key.into(), + public_key: public_key.into(), + }, + ); } let keys = MintKeys::new(map); - Self { id: (&keys).into(), unit, keys, } } + + pub fn generate_from_seed( + secp: &Secp256k1, + seed: &[u8], + info: MintKeySetInfo, + ) -> Self { + let xpriv = + ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); + let max_order = info.max_order; + let unit = info.unit; + Self::generate(secp, xpriv, unit, max_order) + } + + pub fn generate_from_xpriv( + secp: &Secp256k1, + xpriv: ExtendedPrivKey, + info: MintKeySetInfo, + ) -> Self { + let max_order = info.max_order; + let unit = info.unit; + Self::generate(secp, xpriv, unit, max_order) + } } +#[cfg(feature = "mint")] impl From for Id { fn from(keyset: MintKeySet) -> Id { let keys: super::KeySet = keyset.into(); @@ -305,6 +336,7 @@ impl From for Id { } } +#[cfg(feature = "mint")] impl From<&MintKeys> for Id { fn from(map: &MintKeys) -> Self { let keys: super::Keys = map.clone().into();