From a11988b102bb406988bb6735f841a1903169d4c2 Mon Sep 17 00:00:00 2001 From: Cesar Rodas Date: Mon, 13 Jan 2025 20:46:45 -0300 Subject: [PATCH] Introduce a SignatoryManager service. The SignatoryManager manager provides an API to interact with keysets, private keys, and all key-related operations, offering segregation between the mint and the most sensible part of the mind: the private keys. Although the default signatory runs in memory, it is completely isolated from the rest of the system and can only be communicated through the interface offered by the signatory manager. Only messages can be sent from the mintd to the Signatory trait through the Signatory Manager. This pull request sets the foundation for eventually being able to run the Signatory and all the key-related operations in a separate service, possibly in a foreign service, to offload risks, as described in #476. The Signatory manager is concurrent and deferred any mechanism needed to handle concurrency to the Signatory trait. --- Cargo.lock | 13 + crates/cdk-common/src/error.rs | 8 + crates/cdk-common/src/lib.rs | 2 + crates/cdk-common/src/signatory.rs | 50 ++ .../cdk-integration-tests/src/init_regtest.rs | 17 +- crates/cdk-integration-tests/src/lib.rs | 21 +- .../tests/integration_tests_pure.rs | 16 +- crates/cdk-integration-tests/tests/mint.rs | 17 +- crates/cdk-signatory/Cargo.toml | 22 + crates/cdk-signatory/src/lib.rs | 519 ++++++++++++++++++ crates/cdk-signatory/src/main.rs | 3 + crates/cdk/Cargo.toml | 2 + crates/cdk/src/mint/builder.rs | 39 +- crates/cdk/src/mint/config.rs | 29 +- crates/cdk/src/mint/keysets.rs | 132 +---- crates/cdk/src/mint/mod.rs | 391 ++----------- crates/cdk/src/mint/signatory.rs | 121 ++++ 17 files changed, 886 insertions(+), 516 deletions(-) create mode 100644 crates/cdk-common/src/signatory.rs create mode 100644 crates/cdk-signatory/Cargo.toml create mode 100644 crates/cdk-signatory/src/lib.rs create mode 100644 crates/cdk-signatory/src/main.rs create mode 100644 crates/cdk/src/mint/signatory.rs diff --git a/Cargo.lock b/Cargo.lock index 81140bfb9..2ed6f7b9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -761,11 +761,13 @@ dependencies = [ "bitcoin 0.32.5", "cbor-diag", "cdk-common", + "cdk-signatory", "ciborium", "criterion", "futures", "getrandom", "lightning-invoice", + "paste", "rand", "regex", "reqwest", @@ -1030,6 +1032,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "cdk-signatory" +version = "0.6.0" +dependencies = [ + "async-trait", + "bitcoin 0.32.5", + "cdk-common", + "tokio", + "tracing", +] + [[package]] name = "cdk-sqlite" version = "0.6.0" diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index 6c2471c2e..c667c4653 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -52,6 +52,14 @@ pub enum Error { #[error("Amount Less Invoice is not allowed")] AmountLessNotAllowed, + /// Internal Error - Send error + #[error("Internal send error: {0}")] + SendError(String), + + /// Internal Error - Recv error + #[error("Internal receive error: {0}")] + RecvError(String), + // Mint Errors /// Minting is disabled #[error("Minting is disabled")] diff --git a/crates/cdk-common/src/lib.rs b/crates/cdk-common/src/lib.rs index 52e7068eb..73ed62474 100644 --- a/crates/cdk-common/src/lib.rs +++ b/crates/cdk-common/src/lib.rs @@ -13,6 +13,8 @@ pub mod error; pub mod lightning; pub mod pub_sub; #[cfg(feature = "mint")] +pub mod signatory; +#[cfg(feature = "mint")] pub mod subscription; pub mod ws; diff --git a/crates/cdk-common/src/signatory.rs b/crates/cdk-common/src/signatory.rs new file mode 100644 index 000000000..b094ebf4c --- /dev/null +++ b/crates/cdk-common/src/signatory.rs @@ -0,0 +1,50 @@ +//! Signatory mod +//! +//! This module abstract all the key related operations, defining an interface for the necessary +//! operations, to be implemented by the different signatory implementations. +//! +//! There is an in memory implementation, when the keys are stored in memory, in the same process, +//! but it is isolated from the rest of the application, and they communicate through a channel with +//! the defined API. +use std::collections::HashMap; + +use bitcoin::bip32::DerivationPath; +use cashu::{ + BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Proof, +}; + +use super::error::Error; + +#[async_trait::async_trait] +/// Signatory trait +pub trait Signatory { + /// Blind sign a message + async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result; + + /// Verify [`Proof`] meets conditions and is signed + async fn verify_proof(&self, proof: Proof) -> Result<(), Error>; + + /// Retrieve a keyset by id + async fn keyset(&self, keyset_id: Id) -> Result, Error>; + + /// Retrieve the public keys of a keyset + async fn keyset_pubkeys(&self, keyset_id: Id) -> Result; + + /// Retrieve the public keys of the active keyset for distribution to wallet + /// clients + async fn pubkeys(&self) -> Result; + + /// Return a list of all supported keysets + async fn keysets(&self) -> Result; + + /// Add current keyset to inactive keysets + /// Generate new keyset + async fn rotate_keyset( + &self, + unit: CurrencyUnit, + derivation_path_index: u32, + max_order: u8, + input_fee_ppk: u64, + custom_paths: HashMap, + ) -> Result<(), Error>; +} diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index b014271f7..438ea82de 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::env; use std::path::PathBuf; use std::sync::Arc; @@ -6,7 +7,7 @@ use anyhow::Result; use axum::Router; use bip39::Mnemonic; use cdk::cdk_database::{self, MintDatabase}; -use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; +use cdk::mint::{FeeReserve, MemorySignatory, MintBuilder, MintMeltLimits}; use cdk::nuts::{CurrencyUnit, PaymentMethod}; use cdk_cln::Cln as CdkCln; use ln_regtest_rs::bitcoin_client::BitcoinClient; @@ -152,7 +153,9 @@ where let mut mint_builder = MintBuilder::new(); - mint_builder = mint_builder.with_localstore(Arc::new(database)); + let localstore = Arc::new(database); + + mint_builder = mint_builder.with_localstore(localstore.clone()); mint_builder = mint_builder.add_ln_backend( CurrencyUnit::Sat, @@ -163,8 +166,18 @@ where let mnemonic = Mnemonic::generate(12)?; + let signatory_manager = MemorySignatory::new( + localstore, + &mnemonic.to_seed_normalized(""), + mint_builder.supported_units.clone(), + HashMap::new(), + ) + .await + .expect("valid signatory"); + mint_builder = mint_builder .with_name("regtest mint".to_string()) + .with_signatory(Arc::new(signatory_manager)) .with_mint_url(format!("http://{addr}:{port}")) .with_description("regtest mint".to_string()) .with_quote_ttl(10000, 10000) diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index 91766efc3..1658b11f7 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -9,7 +9,8 @@ use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk::cdk_lightning::MintLightning; use cdk::dhke::construct_proofs; -use cdk::mint::FeeReserve; +use cdk::mint::signatory::SignatoryManager; +use cdk::mint::{FeeReserve, MemorySignatory}; use cdk::mint_url::MintUrl; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::nut17::Params; @@ -76,15 +77,25 @@ pub async fn start_mint( let quote_ttl = QuoteTTL::new(10000, 10000); + let localstore = Arc::new(MintMemoryDatabase::default()); + let signatory_manager = Arc::new(SignatoryManager::new(Arc::new( + MemorySignatory::new( + localstore.clone(), + &mnemonic.to_seed_normalized(""), + supported_units, + HashMap::new(), + ) + .await + .expect("valid signatory"), + ))); + let mint = Mint::new( &get_mint_url(), - &mnemonic.to_seed_normalized(""), mint_info, quote_ttl, - Arc::new(MintMemoryDatabase::default()), + localstore, ln_backends.clone(), - supported_units, - HashMap::new(), + signatory_manager, ) .await?; diff --git a/crates/cdk-integration-tests/tests/integration_tests_pure.rs b/crates/cdk-integration-tests/tests/integration_tests_pure.rs index 275219a10..ed4363e40 100644 --- a/crates/cdk-integration-tests/tests/integration_tests_pure.rs +++ b/crates/cdk-integration-tests/tests/integration_tests_pure.rs @@ -10,6 +10,8 @@ mod integration_tests_pure { use cdk::amount::SplitTarget; use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk::cdk_database::WalletMemoryDatabase; + use cdk::mint::signatory::SignatoryManager; + use cdk::mint::MemorySignatory; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse, @@ -164,15 +166,21 @@ mod integration_tests_pure { let mint_url = "http://aaa"; let seed = random::<[u8; 32]>(); + + let localstore = Arc::new(MintMemoryDatabase::default()); + let signatory_manager = Arc::new(SignatoryManager::new(Arc::new( + MemorySignatory::new(localstore.clone(), &seed, supported_units, HashMap::new()) + .await + .expect("valid signatory"), + ))); + let mint: Mint = Mint::new( mint_url, - &seed, mint_info, quote_ttl, - Arc::new(MintMemoryDatabase::default()), + localstore, create_backends_fake_wallet(), - supported_units, - HashMap::new(), + signatory_manager, ) .await?; diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 10cd5a164..337de7cad 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -9,7 +9,8 @@ use bip39::Mnemonic; use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk::dhke::construct_proofs; -use cdk::mint::MintQuote; +use cdk::mint::signatory::SignatoryManager; +use cdk::mint::{MemorySignatory, MintQuote}; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PreMintSecrets, @@ -45,15 +46,21 @@ async fn new_mint(fee: u64) -> Mint { let quote_ttl = QuoteTTL::new(10000, 10000); + let localstore = Arc::new(MintMemoryDatabase::default()); + let seed = mnemonic.to_seed_normalized(""); + let signatory_manager = Arc::new(SignatoryManager::new(Arc::new( + MemorySignatory::new(localstore.clone(), &seed, supported_units, HashMap::new()) + .await + .expect("valid signatory"), + ))); + Mint::new( MINT_URL, - &mnemonic.to_seed_normalized(""), mint_info, quote_ttl, - Arc::new(MintMemoryDatabase::default()), - HashMap::new(), - supported_units, + localstore, HashMap::new(), + signatory_manager, ) .await .unwrap() diff --git a/crates/cdk-signatory/Cargo.toml b/crates/cdk-signatory/Cargo.toml new file mode 100644 index 000000000..e661eeaf9 --- /dev/null +++ b/crates/cdk-signatory/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cdk-signatory" +version = "0.6.0" +edition = "2021" +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "CDK signatory default implementation" + +[dependencies] +async-trait = "0.1.83" +bitcoin = { version = "0.32.2", features = [ + "base64", + "serde", + "rand", + "rand-std", +] } +cdk-common = { path = "../cdk-common", default-features = false, features = [ + "mint", +] } +tracing = "0.1.41" +tokio = { version = "1.21", features = ["rt", "macros", "sync", "time"] } diff --git a/crates/cdk-signatory/src/lib.rs b/crates/cdk-signatory/src/lib.rs new file mode 100644 index 000000000..5208761d2 --- /dev/null +++ b/crates/cdk-signatory/src/lib.rs @@ -0,0 +1,519 @@ +//! In memory signatory +//! +//! Implements the Signatory trait from cdk-common to manage the key in-process, to be included +//! inside the mint to be executed as a single process. +//! +//! Even if it is embedded in the same process, the keys are not accessible from the outside of this +//! module, all communication is done through the Signatory trait and the signatory manager. +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; +use bitcoin::secp256k1::{self, Secp256k1}; +use cdk_common::amount::Amount; +use cdk_common::database::{self, MintDatabase}; +use cdk_common::dhke::{sign_message, verify_message}; +use cdk_common::error::Error; +use cdk_common::mint::MintKeySetInfo; +use cdk_common::nuts::nut01::MintKeyPair; +use cdk_common::nuts::{ + self, BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeySetInfo, KeysResponse, + KeysetResponse, Kind, MintKeySet, Proof, +}; +use cdk_common::secret; +use cdk_common::signatory::Signatory; +use cdk_common::util::unix_time; +use tokio::sync::RwLock; + +/// Generate new [`MintKeySetInfo`] from path +#[tracing::instrument(skip_all)] +fn create_new_keyset( + secp: &secp256k1::Secp256k1, + xpriv: Xpriv, + derivation_path: DerivationPath, + derivation_path_index: Option, + unit: CurrencyUnit, + max_order: u8, + input_fee_ppk: u64, +) -> (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, + derivation_path_index, + max_order, + input_fee_ppk, + }; + (keyset, keyset_info) +} + +fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option { + let unit_index = unit.derivation_index()?; + + Some(DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(0).expect("0 is a valid index"), + ChildNumber::from_hardened_idx(unit_index).expect("0 is a valid index"), + ChildNumber::from_hardened_idx(index).expect("0 is a valid index"), + ])) +} + +/// In-memory Signatory +/// +/// This is the default signatory implementation for the mint. +/// +/// The private keys and the all key-related data is stored in memory, in the same process, but it +/// is not accessible from the outside. +pub struct MemorySignatory { + keysets: RwLock>, + localstore: Arc + Send + Sync>, + secp_ctx: Secp256k1, + xpriv: Xpriv, +} + +impl MemorySignatory { + /// Creates a new MemorySignatory instance + pub async fn new( + localstore: Arc + Send + Sync>, + seed: &[u8], + supported_units: HashMap, + custom_paths: HashMap, + ) -> Result { + let secp_ctx = Secp256k1::new(); + let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); + + let mut active_keysets = HashMap::new(); + let keysets_infos = localstore.get_keyset_infos().await?; + let mut active_keyset_units = vec![]; + + if !keysets_infos.is_empty() { + tracing::debug!("Setting all saved keysets to inactive"); + for keyset in keysets_infos.clone() { + // Set all to in active + let mut keyset = keyset; + keyset.active = false; + localstore.add_keyset_info(keyset).await?; + } + + let keysets_by_unit: HashMap> = + keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| { + acc.entry(ks.unit.clone()).or_default().push(ks.clone()); + acc + }); + + for (unit, keysets) in keysets_by_unit { + let mut keysets = keysets; + keysets.sort_by(|a, b| b.derivation_path_index.cmp(&a.derivation_path_index)); + + let highest_index_keyset = keysets + .first() + .cloned() + .expect("unit will not be added to hashmap if empty"); + + let keysets: Vec = keysets + .into_iter() + .filter(|ks| ks.derivation_path_index.is_some()) + .collect(); + + if let Some((input_fee_ppk, max_order)) = supported_units.get(&unit) { + let derivation_path_index = if keysets.is_empty() { + 1 + } else if &highest_index_keyset.input_fee_ppk == input_fee_ppk + && &highest_index_keyset.max_order == max_order + { + let id = highest_index_keyset.id; + let keyset = MintKeySet::generate_from_xpriv( + &secp_ctx, + xpriv, + highest_index_keyset.max_order, + highest_index_keyset.unit.clone(), + highest_index_keyset.derivation_path.clone(), + ); + active_keysets.insert(id, keyset); + let mut keyset_info = highest_index_keyset; + keyset_info.active = true; + localstore.add_keyset_info(keyset_info).await?; + localstore.set_active_keyset(unit, id).await?; + continue; + } else { + highest_index_keyset.derivation_path_index.unwrap_or(0) + 1 + }; + + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit.clone(), derivation_path_index) + .ok_or(Error::UnsupportedUnit)?, + }; + + let (keyset, keyset_info) = create_new_keyset( + &secp_ctx, + xpriv, + derivation_path, + Some(derivation_path_index), + unit.clone(), + *max_order, + *input_fee_ppk, + ); + + let id = keyset_info.id; + localstore.add_keyset_info(keyset_info).await?; + localstore.set_active_keyset(unit.clone(), id).await?; + active_keysets.insert(id, keyset); + active_keyset_units.push(unit.clone()); + } + } + } + + for (unit, (fee, max_order)) in supported_units { + if !active_keyset_units.contains(&unit) { + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => { + derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)? + } + }; + + let (keyset, keyset_info) = create_new_keyset( + &secp_ctx, + xpriv, + derivation_path, + Some(0), + unit.clone(), + max_order, + fee, + ); + + let id = keyset_info.id; + localstore.add_keyset_info(keyset_info).await?; + localstore.set_active_keyset(unit, id).await?; + active_keysets.insert(id, keyset); + } + } + + Ok(Self { + keysets: RwLock::new(HashMap::new()), + secp_ctx, + localstore, + xpriv, + }) + } +} + +impl MemorySignatory { + fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet { + MintKeySet::generate_from_xpriv( + &self.secp_ctx, + self.xpriv, + keyset_info.max_order, + keyset_info.unit, + keyset_info.derivation_path, + ) + } + + async fn load_and_get_keyset(&self, id: &Id) -> Result { + let keysets = self.keysets.read().await; + let keyset_info = self + .localstore + .get_keyset_info(id) + .await? + .ok_or(Error::UnknownKeySet)?; + + if keysets.contains_key(id) { + return Ok(keyset_info); + } + drop(keysets); + + let id = keyset_info.id; + let mut keysets = self.keysets.write().await; + keysets.insert(id, self.generate_keyset(keyset_info.clone())); + Ok(keyset_info) + } + + #[tracing::instrument(skip(self))] + async fn get_keypair_for_amount( + &self, + keyset_id: &Id, + amount: &Amount, + ) -> Result { + let keyset_info = self.load_and_get_keyset(keyset_id).await?; + let active = self + .localstore + .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_info.id != active { + return Err(Error::InactiveKeyset); + } + + let keysets = self.keysets.read().await; + let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?; + + match keyset.keys.get(amount) { + Some(key_pair) => Ok(key_pair.clone()), + None => Err(Error::AmountKey), + } + } +} + +#[async_trait::async_trait] +impl Signatory for MemorySignatory { + async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result { + let BlindedMessage { + amount, + blinded_secret, + keyset_id, + .. + } = blinded_message; + let key_pair = self.get_keypair_for_amount(&keyset_id, &amount).await?; + let c = sign_message(&key_pair.secret_key, &blinded_secret)?; + + let blinded_signature = BlindSignature::new( + amount, + c, + keyset_id, + &blinded_message.blinded_secret, + key_pair.secret_key, + )?; + + Ok(blinded_signature) + } + + async fn verify_proof(&self, proof: Proof) -> Result<(), Error> { + // Check if secret is a nut10 secret with conditions + if let Ok(secret) = + <&secret::Secret as TryInto>::try_into(&proof.secret) + { + // Checks and verifes known secret kinds. + // If it is an unknown secret kind it will be treated as a normal secret. + // Spending conditions will **not** be check. It is up to the wallet to ensure + // only supported secret kinds are used as there is no way for the mint to + // enforce only signing supported secrets as they are blinded at + // that point. + match secret.kind { + Kind::P2PK => { + proof.verify_p2pk()?; + } + Kind::HTLC => { + proof.verify_htlc()?; + } + } + } + + let key_pair = self + .get_keypair_for_amount(&proof.keyset_id, &proof.amount) + .await?; + + verify_message(&key_pair.secret_key, proof.c, proof.secret.as_bytes())?; + + Ok(()) + } + + async fn keyset(&self, keyset_id: Id) -> Result, Error> { + self.load_and_get_keyset(&keyset_id).await?; + Ok(self + .keysets + .read() + .await + .get(&keyset_id) + .map(|k| k.clone().into())) + } + + async fn keyset_pubkeys(&self, keyset_id: Id) -> Result { + self.load_and_get_keyset(&keyset_id).await?; + Ok(KeysResponse { + keysets: vec![self + .keysets + .read() + .await + .get(&keyset_id) + .ok_or(Error::UnknownKeySet)? + .clone() + .into()], + }) + } + + async fn pubkeys(&self) -> Result { + let active_keysets = self.localstore.get_active_keysets().await?; + let active_keysets: HashSet<&Id> = active_keysets.values().collect(); + for id in active_keysets.iter() { + let _ = self.load_and_get_keyset(id).await?; + } + let keysets = self.keysets.read().await; + Ok(KeysResponse { + keysets: keysets + .values() + .filter_map(|k| match active_keysets.contains(&k.id) { + true => Some(k.clone().into()), + false => None, + }) + .collect(), + }) + } + + async fn keysets(&self) -> Result { + let keysets = self.localstore.get_keyset_infos().await?; + let active_keysets: HashSet = self + .localstore + .get_active_keysets() + .await? + .values() + .cloned() + .collect(); + + Ok(KeysetResponse { + keysets: keysets + .into_iter() + .map(|k| KeySetInfo { + id: k.id, + unit: k.unit, + active: active_keysets.contains(&k.id), + input_fee_ppk: k.input_fee_ppk, + }) + .collect(), + }) + } + + /// Add current keyset to inactive keysets + /// Generate new keyset + #[tracing::instrument(skip(self))] + async fn rotate_keyset( + &self, + unit: CurrencyUnit, + derivation_path_index: u32, + max_order: u8, + input_fee_ppk: u64, + custom_paths: HashMap, + ) -> Result<(), Error> { + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit.clone(), derivation_path_index) + .ok_or(Error::UnsupportedUnit)?, + }; + + let (keyset, keyset_info) = create_new_keyset( + &self.secp_ctx, + self.xpriv, + derivation_path, + Some(derivation_path_index), + unit.clone(), + max_order, + input_fee_ppk, + ); + let id = keyset_info.id; + self.localstore.add_keyset_info(keyset_info).await?; + self.localstore.set_active_keyset(unit, id).await?; + + let mut keysets = self.keysets.write().await; + keysets.insert(id, keyset); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use bitcoin::key::Secp256k1; + use bitcoin::Network; + use cdk_common::MintKeySet; + use nuts::PublicKey; + + use super::*; + + #[test] + fn mint_mod_generate_keyset_from_seed() { + let seed = "test_seed".as_bytes(); + let keyset = MintKeySet::generate_from_seed( + &Secp256k1::new(), + seed, + 2, + CurrencyUnit::Sat, + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), + ); + + assert_eq!(keyset.unit, CurrencyUnit::Sat); + assert_eq!(keyset.keys.len(), 2); + + let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ + ( + Amount::from(1), + PublicKey::from_hex( + "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", + ) + .unwrap(), + ), + ( + Amount::from(2), + PublicKey::from_hex( + "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", + ) + .unwrap(), + ), + ] + .into_iter() + .collect(); + + let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset + .keys + .iter() + .map(|(amount, pair)| (*amount, pair.public_key)) + .collect(); + + assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); + } + + #[test] + fn mint_mod_generate_keyset_from_xpriv() { + let seed = "test_seed".as_bytes(); + let network = Network::Bitcoin; + let xpriv = Xpriv::new_master(network, seed).expect("Failed to create xpriv"); + let keyset = MintKeySet::generate_from_xpriv( + &Secp256k1::new(), + xpriv, + 2, + CurrencyUnit::Sat, + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), + ); + + assert_eq!(keyset.unit, CurrencyUnit::Sat); + assert_eq!(keyset.keys.len(), 2); + + let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ + ( + Amount::from(1), + PublicKey::from_hex( + "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", + ) + .unwrap(), + ), + ( + Amount::from(2), + PublicKey::from_hex( + "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", + ) + .unwrap(), + ), + ] + .into_iter() + .collect(); + + let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset + .keys + .iter() + .map(|(amount, pair)| (*amount, pair.public_key)) + .collect(); + + assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); + } +} diff --git a/crates/cdk-signatory/src/main.rs b/crates/cdk-signatory/src/main.rs new file mode 100644 index 000000000..e7a11a969 --- /dev/null +++ b/crates/cdk-signatory/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 35eff847c..cb06e104d 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -22,6 +22,7 @@ http_subscription = [] [dependencies] cdk-common = { path = "../cdk-common", version = "0.6.0" } +cdk-signatory = { path = "../cdk-signatory", version = "0.6.0" } cbor-diag = "0.1.12" arc-swap = "1.7.1" async-trait = "0.1" @@ -63,6 +64,7 @@ uuid = { version = "1", features = ["v4", "serde"] } # -Z minimal-versions sync_wrapper = "0.1.2" bech32 = "0.9.1" +paste = "1.0.15" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.21", features = [ diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index d40e2b122..e51c1c39b 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -5,9 +5,11 @@ use std::sync::Arc; use anyhow::anyhow; use cdk_common::database::{self, MintDatabase}; +use cdk_common::signatory::Signatory; use super::nut17::SupportedMethods; use super::nut19::{self, CachedEndpoint}; +use super::signatory::SignatoryManager; use super::Nuts; use crate::amount::Amount; use crate::cdk_lightning::{self, MintLightning}; @@ -31,7 +33,9 @@ pub struct MintBuilder { ln: Option + Send + Sync>>>, seed: Option>, quote_ttl: Option, - supported_units: HashMap, + /// expose supported units + pub supported_units: HashMap, + signatory: Option>, } impl MintBuilder { @@ -54,6 +58,12 @@ impl MintBuilder { builder } + /// Set signatory service + pub fn with_signatory(mut self, signatory: Arc) -> Self { + self.signatory = Some(signatory); + self + } + /// Set localstore pub fn with_localstore( mut self, @@ -225,18 +235,31 @@ impl MintBuilder { } /// Build mint - pub async fn build(&self) -> anyhow::Result { + pub async fn build(self) -> anyhow::Result { + let localstore = self.localstore.ok_or(anyhow!("Localstore not set"))?; + let signatory = if let Some(signatory) = self.signatory { + signatory + } else { + Arc::new( + cdk_signatory::MemorySignatory::new( + localstore.clone(), + self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?, + self.supported_units, + HashMap::new(), + ) + .await?, + ) + }; + + let signatory_manager = Arc::new(SignatoryManager::new(signatory)); + Ok(Mint::new( self.mint_url.as_ref().ok_or(anyhow!("Mint url not set"))?, - self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?, self.mint_info.clone(), self.quote_ttl.ok_or(anyhow!("Quote ttl not set"))?, - self.localstore - .clone() - .ok_or(anyhow!("Localstore not set"))?, + localstore, self.ln.clone().ok_or(anyhow!("Ln backends not set"))?, - self.supported_units.clone(), - HashMap::new(), + signatory_manager, ) .await?) } diff --git a/crates/cdk/src/mint/config.rs b/crates/cdk/src/mint/config.rs index a669cea09..de745d135 100644 --- a/crates/cdk/src/mint/config.rs +++ b/crates/cdk/src/mint/config.rs @@ -1,19 +1,16 @@ //! Active mint configuration //! //! This is the active configuration that can be updated at runtime. -use std::collections::HashMap; use std::sync::Arc; use arc_swap::ArcSwap; -use super::{Id, MintInfo, MintKeySet}; +use super::MintInfo; use crate::mint_url::MintUrl; use crate::types::QuoteTTL; /// Mint Inner configuration pub struct Config { - /// Active Mint Keysets - pub keysets: HashMap, /// Mint url pub mint_info: MintInfo, /// Mint config @@ -36,14 +33,8 @@ pub struct SwappableConfig { impl SwappableConfig { /// Creates a new configuration instance - pub fn new( - mint_url: MintUrl, - quote_ttl: QuoteTTL, - mint_info: MintInfo, - keysets: HashMap, - ) -> Self { + pub fn new(mint_url: MintUrl, quote_ttl: QuoteTTL, mint_info: MintInfo) -> Self { let inner = Config { - keysets, quote_ttl, mint_info, mint_url, @@ -71,7 +62,6 @@ impl SwappableConfig { mint_url, quote_ttl: current_inner.quote_ttl, mint_info: current_inner.mint_info.clone(), - keysets: current_inner.keysets.clone(), }; self.config.store(Arc::new(new_inner)); @@ -89,7 +79,6 @@ impl SwappableConfig { mint_info: current_inner.mint_info.clone(), mint_url: current_inner.mint_url.clone(), quote_ttl, - keysets: current_inner.keysets.clone(), }; self.config.store(Arc::new(new_inner)); @@ -107,20 +96,6 @@ impl SwappableConfig { mint_info, mint_url: current_inner.mint_url.clone(), quote_ttl: current_inner.quote_ttl, - keysets: current_inner.keysets.clone(), - }; - - self.config.store(Arc::new(new_inner)); - } - - /// Replaces the current keysets with a new one - pub fn set_keysets(&self, keysets: HashMap) { - let current_inner = self.load(); - let new_inner = Config { - mint_info: current_inner.mint_info.clone(), - quote_ttl: current_inner.quote_ttl, - mint_url: current_inner.mint_url.clone(), - keysets, }; self.config.store(Arc::new(new_inner)); diff --git a/crates/cdk/src/mint/keysets.rs b/crates/cdk/src/mint/keysets.rs index abad59839..40876e587 100644 --- a/crates/cdk/src/mint/keysets.rs +++ b/crates/cdk/src/mint/keysets.rs @@ -1,12 +1,9 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use bitcoin::bip32::DerivationPath; use tracing::instrument; -use super::{ - create_new_keyset, derivation_path_from_unit, CurrencyUnit, Id, KeySet, KeySetInfo, - KeysResponse, KeysetResponse, Mint, MintKeySet, MintKeySetInfo, -}; +use super::{CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Mint}; use crate::Error; impl Mint { @@ -14,78 +11,26 @@ impl Mint { /// clients #[instrument(skip(self))] pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result { - self.ensure_keyset_loaded(keyset_id).await?; - let keyset = self - .config - .load() - .keysets - .get(keyset_id) - .ok_or(Error::UnknownKeySet)? - .clone(); - Ok(KeysResponse { - keysets: vec![keyset.into()], - }) + self.signatory.keyset_pubkeys(keyset_id.to_owned()).await } /// Retrieve the public keys of the active keyset for distribution to wallet /// clients #[instrument(skip_all)] pub async fn pubkeys(&self) -> Result { - let active_keysets = self.localstore.get_active_keysets().await?; - - let active_keysets: HashSet<&Id> = active_keysets.values().collect(); - - for id in active_keysets.iter() { - self.ensure_keyset_loaded(id).await?; - } - - Ok(KeysResponse { - keysets: self - .config - .load() - .keysets - .values() - .filter_map(|k| match active_keysets.contains(&k.id) { - true => Some(k.clone().into()), - false => None, - }) - .collect(), - }) + self.signatory.pubkeys().await } /// Return a list of all supported keysets #[instrument(skip_all)] pub async fn keysets(&self) -> Result { - let keysets = self.localstore.get_keyset_infos().await?; - let active_keysets: HashSet = self - .localstore - .get_active_keysets() - .await? - .values() - .cloned() - .collect(); - - let keysets = keysets - .into_iter() - .map(|k| KeySetInfo { - id: k.id, - unit: k.unit, - active: active_keysets.contains(&k.id), - input_fee_ppk: k.input_fee_ppk, - }) - .collect(); - - Ok(KeysetResponse { keysets }) + self.signatory.keysets().await } /// Get keysets #[instrument(skip(self))] pub async fn keyset(&self, id: &Id) -> Result, Error> { - self.ensure_keyset_loaded(id).await?; - let config = self.config.load(); - let keysets = &config.keysets; - let keyset = keysets.get(id).map(|k| k.clone().into()); - Ok(keyset) + self.signatory.keyset(id.to_owned()).await } /// Add current keyset to inactive keysets @@ -99,61 +44,14 @@ impl Mint { input_fee_ppk: u64, custom_paths: HashMap, ) -> Result<(), Error> { - let derivation_path = match custom_paths.get(&unit) { - Some(path) => path.clone(), - None => derivation_path_from_unit(unit.clone(), derivation_path_index) - .ok_or(Error::UnsupportedUnit)?, - }; - - let (keyset, keyset_info) = create_new_keyset( - &self.secp_ctx, - self.xpriv, - derivation_path, - Some(derivation_path_index), - unit.clone(), - max_order, - input_fee_ppk, - ); - let id = keyset_info.id; - self.localstore.add_keyset_info(keyset_info).await?; - self.localstore.set_active_keyset(unit, id).await?; - - let mut keysets = self.config.load().keysets.clone(); - keysets.insert(id, keyset); - self.config.set_keysets(keysets); - - Ok(()) - } - - /// Ensure Keyset is loaded in mint - #[instrument(skip(self))] - pub async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> { - if self.config.load().keysets.contains_key(id) { - return Ok(()); - } - - let mut keysets = self.config.load().keysets.clone(); - 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)); - self.config.set_keysets(keysets); - - Ok(()) - } - - /// Generate [`MintKeySet`] from [`MintKeySetInfo`] - #[instrument(skip_all)] - pub fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet { - MintKeySet::generate_from_xpriv( - &self.secp_ctx, - self.xpriv, - keyset_info.max_order, - keyset_info.unit, - keyset_info.derivation_path, - ) + self.signatory + .rotate_keyset( + unit, + derivation_path_index, + max_order, + input_fee_ppk, + custom_paths, + ) + .await } } diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 0d7a16478..a4b736d3a 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -4,14 +4,12 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; -use bitcoin::secp256k1::{self, Secp256k1}; use cdk_common::common::{LnKey, QuoteTTL}; use cdk_common::database::{self, MintDatabase}; -use cdk_common::mint::MintKeySetInfo; use config::SwappableConfig; use futures::StreamExt; use serde::{Deserialize, Serialize}; +use signatory::SignatoryManager; use subscription::PubSubManager; use tokio::sync::Notify; use tokio::task::JoinSet; @@ -19,12 +17,10 @@ use tracing::instrument; use uuid::Uuid; use crate::cdk_lightning::{self, MintLightning}; -use crate::dhke::{sign_message, verify_message}; use crate::error::Error; use crate::fees::calculate_fee; use crate::mint_url::MintUrl; use crate::nuts::*; -use crate::util::unix_time; use crate::Amount; mod builder; @@ -34,12 +30,15 @@ mod info; mod keysets; mod melt; mod mint_nut04; +pub mod signatory; mod start_up_check; pub mod subscription; mod swap; +/// re-export types pub use builder::{MintBuilder, MintMeltLimits}; pub use cdk_common::mint::{MeltQuote, MintQuote}; +pub use cdk_signatory::MemorySignatory; /// Cashu Mint #[derive(Clone)] @@ -52,8 +51,8 @@ pub struct Mint { pub ln: HashMap + Send + Sync>>, /// Subscription manager pub pubsub_manager: Arc, - secp_ctx: Secp256k1, - xpriv: Xpriv, + /// Signatory + pub signatory: Arc, } impl Mint { @@ -61,139 +60,18 @@ impl Mint { #[allow(clippy::too_many_arguments)] pub async fn new( mint_url: &str, - seed: &[u8], mint_info: MintInfo, quote_ttl: QuoteTTL, localstore: Arc + Send + Sync>, ln: HashMap + Send + Sync>>, - // Hashmap where the key is the unit and value is (input fee ppk, max_order) - supported_units: HashMap, - custom_paths: HashMap, + signatory: Arc, ) -> Result { - let secp_ctx = Secp256k1::new(); - let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); - - let mut active_keysets = HashMap::new(); - let keysets_infos = localstore.get_keyset_infos().await?; - - let mut active_keyset_units = vec![]; - - if !keysets_infos.is_empty() { - tracing::debug!("Setting all saved keysets to inactive"); - for keyset in keysets_infos.clone() { - // Set all to in active - let mut keyset = keyset; - keyset.active = false; - localstore.add_keyset_info(keyset).await?; - } - - let keysets_by_unit: HashMap> = - keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| { - acc.entry(ks.unit.clone()).or_default().push(ks.clone()); - acc - }); - - for (unit, keysets) in keysets_by_unit { - let mut keysets = keysets; - keysets.sort_by(|a, b| b.derivation_path_index.cmp(&a.derivation_path_index)); - - let highest_index_keyset = keysets - .first() - .cloned() - .expect("unit will not be added to hashmap if empty"); - - let keysets: Vec = keysets - .into_iter() - .filter(|ks| ks.derivation_path_index.is_some()) - .collect(); - - if let Some((input_fee_ppk, max_order)) = supported_units.get(&unit) { - let derivation_path_index = if keysets.is_empty() { - 1 - } else if &highest_index_keyset.input_fee_ppk == input_fee_ppk - && &highest_index_keyset.max_order == max_order - { - let id = highest_index_keyset.id; - let keyset = MintKeySet::generate_from_xpriv( - &secp_ctx, - xpriv, - highest_index_keyset.max_order, - highest_index_keyset.unit.clone(), - highest_index_keyset.derivation_path.clone(), - ); - active_keysets.insert(id, keyset); - let mut keyset_info = highest_index_keyset; - keyset_info.active = true; - localstore.add_keyset_info(keyset_info).await?; - localstore.set_active_keyset(unit, id).await?; - continue; - } else { - highest_index_keyset.derivation_path_index.unwrap_or(0) + 1 - }; - - let derivation_path = match custom_paths.get(&unit) { - Some(path) => path.clone(), - None => derivation_path_from_unit(unit.clone(), derivation_path_index) - .ok_or(Error::UnsupportedUnit)?, - }; - - let (keyset, keyset_info) = create_new_keyset( - &secp_ctx, - xpriv, - derivation_path, - Some(derivation_path_index), - unit.clone(), - *max_order, - *input_fee_ppk, - ); - - let id = keyset_info.id; - localstore.add_keyset_info(keyset_info).await?; - localstore.set_active_keyset(unit.clone(), id).await?; - active_keysets.insert(id, keyset); - active_keyset_units.push(unit.clone()); - } - } - } - - for (unit, (fee, max_order)) in supported_units { - if !active_keyset_units.contains(&unit) { - let derivation_path = match custom_paths.get(&unit) { - Some(path) => path.clone(), - None => { - derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)? - } - }; - - let (keyset, keyset_info) = create_new_keyset( - &secp_ctx, - xpriv, - derivation_path, - Some(0), - unit.clone(), - max_order, - fee, - ); - - let id = keyset_info.id; - localstore.add_keyset_info(keyset_info).await?; - localstore.set_active_keyset(unit, id).await?; - active_keysets.insert(id, keyset); - } - } - Ok(Self { - config: SwappableConfig::new( - MintUrl::from_str(mint_url)?, - quote_ttl, - mint_info, - active_keysets, - ), + config: SwappableConfig::new(MintUrl::from_str(mint_url)?, quote_ttl, mint_info), pubsub_manager: Arc::new(localstore.clone().into()), - secp_ctx, - xpriv, localstore, ln, + signatory, }) } @@ -290,89 +168,13 @@ impl Mint { &self, blinded_message: &BlindedMessage, ) -> Result { - let BlindedMessage { - amount, - blinded_secret, - keyset_id, - .. - } = blinded_message; - self.ensure_keyset_loaded(keyset_id).await?; - - let keyset_info = self - .localstore - .get_keyset_info(keyset_id) - .await? - .ok_or(Error::UnknownKeySet)?; - - let active = self - .localstore - .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_info.id.ne(&active) { - return Err(Error::InactiveKeyset); - } - - let config = self.config.load(); - let keysets = &config.keysets; - let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?; - - let key_pair = match keyset.keys.get(amount) { - Some(key_pair) => key_pair, - None => return Err(Error::AmountKey), - }; - - let c = sign_message(&key_pair.secret_key, blinded_secret)?; - - let blinded_signature = BlindSignature::new( - *amount, - c, - keyset_info.id, - &blinded_message.blinded_secret, - key_pair.secret_key.clone(), - )?; - - Ok(blinded_signature) + self.signatory.blind_sign(blinded_message.to_owned()).await } /// Verify [`Proof`] meets conditions and is signed #[instrument(skip_all)] pub async fn verify_proof(&self, proof: &Proof) -> Result<(), Error> { - // Check if secret is a nut10 secret with conditions - if let Ok(secret) = - <&crate::secret::Secret as TryInto>::try_into(&proof.secret) - { - // Checks and verifes known secret kinds. - // If it is an unknown secret kind it will be treated as a normal secret. - // Spending conditions will **not** be check. It is up to the wallet to ensure - // only supported secret kinds are used as there is no way for the mint to - // enforce only signing supported secrets as they are blinded at - // that point. - match secret.kind { - Kind::P2PK => { - proof.verify_p2pk()?; - } - Kind::HTLC => { - proof.verify_htlc()?; - } - } - } - - self.ensure_keyset_loaded(&proof.keyset_id).await?; - let config = self.config.load(); - let keysets = &config.keysets; - let keyset = keysets.get(&proof.keyset_id).ok_or(Error::UnknownKeySet)?; - - let keypair = match keyset.keys.get(&proof.amount) { - Some(key_pair) => key_pair, - None => return Err(Error::AmountKey), - }; - - verify_message(&keypair.secret_key, proof.c, proof.secret.as_bytes())?; - - Ok(()) + self.signatory.verify_proof(proof.to_owned()).await } /// Verify melt request is valid @@ -518,146 +320,15 @@ pub struct FeeReserve { pub percent_fee_reserve: f32, } -/// Generate new [`MintKeySetInfo`] from path -#[instrument(skip_all)] -fn create_new_keyset( - secp: &secp256k1::Secp256k1, - xpriv: Xpriv, - derivation_path: DerivationPath, - derivation_path_index: Option, - unit: CurrencyUnit, - max_order: u8, - input_fee_ppk: u64, -) -> (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, - derivation_path_index, - max_order, - input_fee_ppk, - }; - (keyset, keyset_info) -} - -fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option { - let unit_index = unit.derivation_index()?; - - Some(DerivationPath::from(vec![ - ChildNumber::from_hardened_idx(0).expect("0 is a valid index"), - ChildNumber::from_hardened_idx(unit_index).expect("0 is a valid index"), - ChildNumber::from_hardened_idx(index).expect("0 is a valid index"), - ])) -} - #[cfg(test)] mod tests { - use std::collections::HashSet; - use bitcoin::Network; use cdk_common::common::{LnKey, QuoteTTL}; - use secp256k1::Secp256k1; + use cdk_common::mint::MintKeySetInfo; + use cdk_signatory::MemorySignatory; use uuid::Uuid; use super::*; - - #[test] - fn mint_mod_generate_keyset_from_seed() { - let seed = "test_seed".as_bytes(); - let keyset = MintKeySet::generate_from_seed( - &Secp256k1::new(), - seed, - 2, - CurrencyUnit::Sat, - derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), - ); - - assert_eq!(keyset.unit, CurrencyUnit::Sat); - assert_eq!(keyset.keys.len(), 2); - - let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ - ( - Amount::from(1), - PublicKey::from_hex( - "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", - ) - .unwrap(), - ), - ( - Amount::from(2), - PublicKey::from_hex( - "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", - ) - .unwrap(), - ), - ] - .into_iter() - .collect(); - - let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset - .keys - .iter() - .map(|(amount, pair)| (*amount, pair.public_key)) - .collect(); - - assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); - } - - #[test] - fn mint_mod_generate_keyset_from_xpriv() { - let seed = "test_seed".as_bytes(); - let network = Network::Bitcoin; - let xpriv = Xpriv::new_master(network, seed).expect("Failed to create xpriv"); - let keyset = MintKeySet::generate_from_xpriv( - &Secp256k1::new(), - xpriv, - 2, - CurrencyUnit::Sat, - derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), - ); - - assert_eq!(keyset.unit, CurrencyUnit::Sat); - assert_eq!(keyset.keys.len(), 2); - - let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ - ( - Amount::from(1), - PublicKey::from_hex( - "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", - ) - .unwrap(), - ), - ( - Amount::from(2), - PublicKey::from_hex( - "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", - ) - .unwrap(), - ), - ] - .into_iter() - .collect(); - - let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset - .keys - .iter() - .map(|(amount, pair)| (*amount, pair.public_key)) - .collect(); - - assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); - } - use crate::cdk_database::mint_memory::MintMemoryDatabase; #[derive(Default)] @@ -696,15 +367,24 @@ mod tests { .unwrap(), ); + let signatory_manager = Arc::new(SignatoryManager::new(Arc::new( + MemorySignatory::new( + localstore.clone(), + config.seed, + config.supported_units, + HashMap::new(), + ) + .await + .expect("valid signatory"), + ))); + Mint::new( config.mint_url, - config.seed, config.mint_info, config.quote_ttl, - localstore, - HashMap::new(), - config.supported_units, + localstore.clone(), HashMap::new(), + signatory_manager, ) .await } @@ -805,12 +485,27 @@ mod tests { mint.rotate_keyset(CurrencyUnit::default(), 0, 32, 1, HashMap::new()) .await?; - let keys = mint.config.load().keysets.clone(); + let keys = mint + .signatory + .keyset_pubkeys("005f6e8c540c9e61".parse().expect("valid key")) + .await + .expect("keys"); - let expected_keys = r#"{"005f6e8c540c9e61":{"id":"005f6e8c540c9e61","unit":"sat","keys":{"1":{"public_key":"03e8aded7525acee36e3394e28f2dcbc012533ef2a2b085a55fc291d311afee3ef","secret_key":"32ee9fc0723772aed4c7b8ac0a02ffe390e54a4e0b037ec6035c2afa10ebd873"},"2":{"public_key":"02628c0919e5cb8ce9aed1f81ce313f40e1ab0b33439d5be2abc69d9bb574902e0","secret_key":"48384bf901bbe8f937d601001d067e73b28b435819c009589350c664f9ba872c"},"4":{"public_key":"039e7c7f274e1e8a90c61669e961c944944e6154c0794fccf8084af90252d2848f","secret_key":"1f039c1e54e9e65faae8ecf69492f810b4bb2292beb3734059f2bb4d564786d0"},"8":{"public_key":"02ca0e563ae941700aefcb16a7fb820afbb3258ae924ab520210cb730227a76ca3","secret_key":"ea3c2641d847c9b15c5f32c150b5c9c04d0666af0549e54f51f941cf584442be"},"16":{"public_key":"031dbab0e4f7fb4fb0030f0e1a1dc80668eadd0b1046df3337bb13a7b9c982d392","secret_key":"5b244f8552077e68b30b534e85bd0e8e29ae0108ff47f5cd92522aa524d3288f"},"32":{"public_key":"037241f7ad421374eb764a48e7769b5e2473582316844fda000d6eef28eea8ffb8","secret_key":"95608f61dd690aef34e6a2d4cbef3ad8fddb4537a14480a17512778058e4f5bd"},"64":{"public_key":"02bc9767b4abf88becdac47a59e67ee9a9a80b9864ef57d16084575273ac63c0e7","secret_key":"2e9cd067fafa342f3118bc1e62fbb8e53acdb0f96d51ce8a1e1037e43fad0dce"},"128":{"public_key":"0351e33a076f415c2cadc945bc9bcb75bf4a774b28df8a0605dea1557e5897fed8","secret_key":"7014f27be5e2b77e4951a81c18ae3585d0b037899d8a37b774970427b13d8f65"},"256":{"public_key":"0314b9f4300367c7e64fa85770da90839d2fc2f57d63660f08bb3ebbf90ed76840","secret_key":"1a545bd9c40fc6cf2ab281710e279967e9f4b86cd07761c741da94bc8042c8fb"},"512":{"public_key":"030d95abc7e881d173f4207a3349f4ee442b9e51cc461602d3eb9665b9237e8db3","secret_key":"622984ef16d1cb28e9adc7a7cfea1808d85b4bdabd015977f0320c9f573858b4"},"1024":{"public_key":"0351a68a667c5fc21d66c187baecefa1d65529d06b7ae13112d432b6bca16b0e8c","secret_key":"6a8badfa26129499b60edb96cda4cbcf08f8007589eb558a9d0307bdc56e0ff6"},"2048":{"public_key":"0376166d8dcf97d8b0e9f11867ff0dafd439c90255b36a25be01e37e14741b9c6a","secret_key":"48fe41181636716ce202b3a3303c2475e6d511991930868d907441e1bcbf8566"},"4096":{"public_key":"03d40f47b4e5c4d72f2a977fab5c66b54d945b2836eb888049b1dd9334d1d70304","secret_key":"66a25bf144a3b40c015dd1f630aa4ba81d2242f5aee845e4f378246777b21676"},"8192":{"public_key":"03be18afaf35a29d7bcd5dfd1936d82c1c14691a63f8aa6ece258e16b0c043049b","secret_key":"4ddac662e82f6028888c11bdefd07229d7c1b56987395f106cc9ea5b301695f6"},"16384":{"public_key":"028e9c6ce70f34cd29aad48656bf8345bb5ba2cb4f31fdd978686c37c93f0ab411","secret_key":"83676bd7d047655476baecad2864519f0ffd8e60f779956d2faebcc727caa7bd"},"32768":{"public_key":"0253e34bab4eec93e235c33994e01bf851d5caca4559f07d37b5a5c266de7cf840","secret_key":"d5be522906223f5d92975e2a77f7e166aa121bf93d5fe442d6d132bf67166b04"},"65536":{"public_key":"02684ede207f9ace309b796b5259fc81ef0d4492b4fb5d66cf866b0b4a6f27bec9","secret_key":"20d859b7052d768e007bf285ee11dc0b98a4abfe272a551852b0cce9fb6d5ad4"},"131072":{"public_key":"027cdf7be8b20a49ac7f2f065f7c53764c8926799877858c6b00b888a8aa6741a5","secret_key":"f6eef28183344b32fc0a1fba00cd6cf967614e51d1c990f0bfce8f67c6d9746a"},"262144":{"public_key":"026939b8f766c3ebaf26408e7e54fc833805563e2ef14c8ee4d0435808b005ec4c","secret_key":"690f23e4eaa250c652afeac24d4efb583095a66abf6b87a7f3d17b1f42c5f896"},"524288":{"public_key":"03772542057493a46eed6513b40386e766eedada16560ffde2f776b65794e9f004","secret_key":"fe36e61bea74665f8796b4b62f9501ae6e0d5b16733d2c05c146cd39f89475a0"},"1048576":{"public_key":"02b016346e5a322d371c6e6164b28b31b4d93a51572351ca2f26cdc12e916d9ac3","secret_key":"b9269779e057ce715964caa6d6b5b65672f255e86746e994b6b8c4780cb9d728"},"2097152":{"public_key":"028f25283e36a11df7713934a5287267381f8304aca3c1eb1b89fddce973ef1436","secret_key":"41aec998b9624ddcff97eb7341daa6385b2a8714ed3f12969ef39649f4d641ab"},"4194304":{"public_key":"03e5841d310819a49ec42dfb24839c61f68bbfc93ac68f6dad37fd5b2d204cc535","secret_key":"e5aef2509c56236f004e2df4343beab6406816fb187c3532d4340a9674857c64"},"8388608":{"public_key":"0307ebfeb87b7bca9baa03fad00499e5cc999fa5179ef0b7ad4f555568bcb946f5","secret_key":"369e8dcabcc69a2eabb7363beb66178cafc29e53b02c46cd15374028c3110541"},"16777216":{"public_key":"02f2508e7df981c32f7b0008a273e2a1f19c23bb60a1561dba6b2a95ed1251eb90","secret_key":"f93965b96ed5428bcacd684eff2f43a9777d03adfde867fa0c6efb39c46a7550"},"33554432":{"public_key":"0381883a1517f8c9979a84fcd5f18437b1a2b0020376ecdd2e515dc8d5a157a318","secret_key":"7f5e77c7ed04dff952a7c15564ab551c769243eb65423adfebf46bf54360cd64"},"67108864":{"public_key":"02aa648d39c9a725ef5927db15af6895f0d43c17f0a31faff4406314fc80180086","secret_key":"d34eda86679bf872dfb6faa6449285741bba6c6d582cd9fe5a9152d5752596cc"},"134217728":{"public_key":"0380658e5163fcf274e1ace6c696d1feef4c6068e0d03083d676dc5ef21804f22d","secret_key":"3ad22e92d497309c5b08b2dc01cb5180de3e00d3d703229914906bc847183987"},"268435456":{"public_key":"031526f03de945c638acccb879de837ac3fabff8590057cfb8552ebcf51215f3aa","secret_key":"3a740771e29119b171ab8e79e97499771439e0ab6a082ec96e43baf06a546372"},"536870912":{"public_key":"035eb3e7262e126c5503e1b402db05f87de6556773ae709cb7aa1c3b0986b87566","secret_key":"9b77ee8cd879128c0ea6952dd188e63617fbaa9e66a3bca0244bcceb9b1f7f48"},"1073741824":{"public_key":"03f12e6a0903ed0db87485a296b1dca9d953a8a6919ff88732238fbc672d6bd125","secret_key":"f3947bca4df0f024eade569c81c5c53e167476e074eb81fa6b289e5e10dd4e42"},"2147483648":{"public_key":"02cece3fb38a54581e0646db4b29242b6d78e49313dda46764094f9d128c1059c1","secret_key":"582d54a894cd41441157849e0d16750e5349bd9310776306e7313b255866950b"}}}}"#; + let expected_keys = r#"{"keysets":[{"id":"005f6e8c540c9e61","unit":"sat","keys":{"1":"03e8aded7525acee36e3394e28f2dcbc012533ef2a2b085a55fc291d311afee3ef","2":"02628c0919e5cb8ce9aed1f81ce313f40e1ab0b33439d5be2abc69d9bb574902e0","4":"039e7c7f274e1e8a90c61669e961c944944e6154c0794fccf8084af90252d2848f","8":"02ca0e563ae941700aefcb16a7fb820afbb3258ae924ab520210cb730227a76ca3","16":"031dbab0e4f7fb4fb0030f0e1a1dc80668eadd0b1046df3337bb13a7b9c982d392","32":"037241f7ad421374eb764a48e7769b5e2473582316844fda000d6eef28eea8ffb8","64":"02bc9767b4abf88becdac47a59e67ee9a9a80b9864ef57d16084575273ac63c0e7","128":"0351e33a076f415c2cadc945bc9bcb75bf4a774b28df8a0605dea1557e5897fed8","256":"0314b9f4300367c7e64fa85770da90839d2fc2f57d63660f08bb3ebbf90ed76840","512":"030d95abc7e881d173f4207a3349f4ee442b9e51cc461602d3eb9665b9237e8db3","1024":"0351a68a667c5fc21d66c187baecefa1d65529d06b7ae13112d432b6bca16b0e8c","2048":"0376166d8dcf97d8b0e9f11867ff0dafd439c90255b36a25be01e37e14741b9c6a","4096":"03d40f47b4e5c4d72f2a977fab5c66b54d945b2836eb888049b1dd9334d1d70304","8192":"03be18afaf35a29d7bcd5dfd1936d82c1c14691a63f8aa6ece258e16b0c043049b","16384":"028e9c6ce70f34cd29aad48656bf8345bb5ba2cb4f31fdd978686c37c93f0ab411","32768":"0253e34bab4eec93e235c33994e01bf851d5caca4559f07d37b5a5c266de7cf840","65536":"02684ede207f9ace309b796b5259fc81ef0d4492b4fb5d66cf866b0b4a6f27bec9","131072":"027cdf7be8b20a49ac7f2f065f7c53764c8926799877858c6b00b888a8aa6741a5","262144":"026939b8f766c3ebaf26408e7e54fc833805563e2ef14c8ee4d0435808b005ec4c","524288":"03772542057493a46eed6513b40386e766eedada16560ffde2f776b65794e9f004","1048576":"02b016346e5a322d371c6e6164b28b31b4d93a51572351ca2f26cdc12e916d9ac3","2097152":"028f25283e36a11df7713934a5287267381f8304aca3c1eb1b89fddce973ef1436","4194304":"03e5841d310819a49ec42dfb24839c61f68bbfc93ac68f6dad37fd5b2d204cc535","8388608":"0307ebfeb87b7bca9baa03fad00499e5cc999fa5179ef0b7ad4f555568bcb946f5","16777216":"02f2508e7df981c32f7b0008a273e2a1f19c23bb60a1561dba6b2a95ed1251eb90","33554432":"0381883a1517f8c9979a84fcd5f18437b1a2b0020376ecdd2e515dc8d5a157a318","67108864":"02aa648d39c9a725ef5927db15af6895f0d43c17f0a31faff4406314fc80180086","134217728":"0380658e5163fcf274e1ace6c696d1feef4c6068e0d03083d676dc5ef21804f22d","268435456":"031526f03de945c638acccb879de837ac3fabff8590057cfb8552ebcf51215f3aa","536870912":"035eb3e7262e126c5503e1b402db05f87de6556773ae709cb7aa1c3b0986b87566","1073741824":"03f12e6a0903ed0db87485a296b1dca9d953a8a6919ff88732238fbc672d6bd125","2147483648":"02cece3fb38a54581e0646db4b29242b6d78e49313dda46764094f9d128c1059c1"}}]}"#; assert_eq!(expected_keys, serde_json::to_string(&keys.clone()).unwrap()); + mint.rotate_keyset(CurrencyUnit::default(), 1, 32, 2, HashMap::new()) + .await?; + + let keys = mint + .signatory + .keyset_pubkeys("00c919b6c4fa90c6".parse().expect("valid key")) + .await + .expect("keys"); + + assert_ne!(expected_keys, serde_json::to_string(&keys.clone()).unwrap()); + Ok(()) } } diff --git a/crates/cdk/src/mint/signatory.rs b/crates/cdk/src/mint/signatory.rs new file mode 100644 index 000000000..aabe04ca3 --- /dev/null +++ b/crates/cdk/src/mint/signatory.rs @@ -0,0 +1,121 @@ +//! Signatory manager for handling signatory requests. +use std::collections::HashMap; +use std::sync::Arc; + +use bitcoin::bip32::DerivationPath; +use cdk_common::error::Error; +use cdk_common::signatory::Signatory; +use cdk_common::{ + BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Proof, +}; +use tokio::sync::{mpsc, oneshot}; +use tokio::task::JoinHandle; + +macro_rules! signatory_manager { + ( + $( + $variant:ident($($input:ty),*) -> $output:ty, + )* $(,)? + ) => { + paste::paste! { + #[allow(unused_parens)] + enum Request { + $( + /// Asynchronous method to handle the `[<$variant:camel>]` request. + [<$variant:camel>]((($($input),*), oneshot::Sender>)), + )* + } + + /// Manager for handling signatory requests. + pub struct SignatoryManager { + pipeline: mpsc::Sender, + runner: JoinHandle<()>, + } + + #[allow(unused_parens)] + impl SignatoryManager { + /// Creates a new SignatoryManager with the given signatory. + /// + /// # Arguments + /// * `signatory` - An `Arc` of a signatory object implementing the required trait. + pub fn new(signatory: Arc) -> Self { + let (sender, receiver) = mpsc::channel(10_000); + let runner = tokio::spawn(async move { + let mut receiver = receiver; + loop { + let request = if let Some(request) = receiver.recv().await { + request + } else { + continue; + }; + let signatory = signatory.clone(); + match request { + $( + Request::[<$variant:camel>]((( $([<$input:snake>]),* ), response)) => { + tokio::spawn(async move { + let output = signatory.[<$variant:lower>]($([<$input:snake>]),*).await; + if let Err(err) = response.send(output) { + tracing::error!("Error sending response: {:?}", err); + } + }); + } + )* + } + } + }); + + Self { + pipeline: sender, + runner, + } + } + + $( + /// Asynchronous method to handle the `$variant` request. + /// + /// # Arguments + /// * $($input: $input),* - The inputs required for the `$variant` request. + /// + /// # Returns + /// * `Result<$output, Error>` - The result of processing the request. + pub async fn [<$variant:lower>](&self, $([<$input:snake>]: $input),*) -> Result<$output, Error> { + let (sender, receiver) = oneshot::channel(); + + self.pipeline + .try_send(Request::[<$variant:camel>]((($([<$input:snake>]),*), sender))) + .map_err(|e| Error::SendError(e.to_string()))?; + + receiver + .await + .map_err(|e| Error::RecvError(e.to_string()))? + } + )* + } + + impl Drop for SignatoryManager { + fn drop(&mut self) { + self.runner.abort(); + } + } + + impl From for SignatoryManager { + fn from(signatory: T) -> Self { + Self::new(Arc::new(signatory)) + } + } + + } + }; +} + +type Map = HashMap; + +signatory_manager! { + blind_sign(BlindedMessage) -> BlindSignature, + verify_proof(Proof) -> (), + keyset(Id) -> Option, + keysets() -> KeysetResponse, + keyset_pubkeys(Id) -> KeysResponse, + pubkeys() -> KeysResponse, + rotate_keyset(CurrencyUnit, u32, u8, u64, Map) -> (), +}