diff --git a/Cargo.lock b/Cargo.lock index 81140bfb..2ed6f7b9 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 6c2471c2..c667c465 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 52e7068e..73ed6247 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 00000000..b094ebf4 --- /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 b014271f..438ea82d 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 91766efc..1658b11f 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 275219a1..ed4363e4 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 10cd5a16..337de7ca 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 00000000..e661eeaf --- /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 00000000..5208761d --- /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 00000000..e7a11a96 --- /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 35eff847..cb06e104 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 d40e2b12..e51c1c39 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 a669cea0..de745d13 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 abad5983..40876e58 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 0d7a1647..a4b736d3 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 00000000..aabe04ca --- /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) -> (), +}