diff --git a/crates/cashu/src/nuts/nut00/mod.rs b/crates/cashu/src/nuts/nut00/mod.rs index 4d8bbb81b..2d36ef749 100644 --- a/crates/cashu/src/nuts/nut00/mod.rs +++ b/crates/cashu/src/nuts/nut00/mod.rs @@ -200,6 +200,18 @@ pub enum Witness { HTLCWitness(HTLCWitness), } +impl From for Witness { + fn from(witness: P2PKWitness) -> Self { + Self::P2PKWitness(witness) + } +} + +impl From for Witness { + fn from(witness: HTLCWitness) -> Self { + Self::HTLCWitness(witness) + } +} + impl Witness { /// Add signatures to [`Witness`] pub fn add_signatures(&mut self, signatues: Vec) { diff --git a/crates/cashu/src/nuts/nut01/mod.rs b/crates/cashu/src/nuts/nut01/mod.rs index bda245fd4..5a04c1501 100644 --- a/crates/cashu/src/nuts/nut01/mod.rs +++ b/crates/cashu/src/nuts/nut01/mod.rs @@ -46,6 +46,14 @@ pub enum Error { #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct Keys(BTreeMap); +impl Deref for Keys { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl From for Keys { fn from(keys: MintKeys) -> Self { Self( diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index 27b666768..1da1655ee 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -59,6 +59,14 @@ pub enum Error { #[error("Multi-Part payment is not supported for unit `{0}` and method `{1}`")] MppUnitMethodNotSupported(CurrencyUnit, PaymentMethod), + /// 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 ca9bfdd9c..33c298434 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::{Path, PathBuf}; use std::sync::Arc; @@ -6,7 +7,7 @@ use anyhow::Result; use bip39::Mnemonic; use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning::{self, MintLightning}; -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 cdk_lnd::Lnd as CdkLnd; @@ -156,7 +157,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, @@ -167,8 +170,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_description("regtest mint".to_string()) .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index b06223df6..fe9dd4c15 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -10,7 +10,8 @@ use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk::cdk_database::MintDatabase; 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, @@ -49,15 +50,17 @@ async fn new_mint(fee: u64) -> Mint { .expect("Could not set mint info"); let mnemonic = Mnemonic::generate(12).unwrap(); - Mint::new( - &mnemonic.to_seed_normalized(""), - Arc::new(localstore), - HashMap::new(), - supported_units, - HashMap::new(), - ) - .await - .unwrap() + 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(localstore, HashMap::new(), signatory_manager) + .await + .unwrap() } async fn initialize() -> &'static Mint { diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index fcf92447f..ee7588292 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -26,6 +26,9 @@ cdk-lnbits = { path = "../cdk-lnbits", version = "0.6.0", default-features = fal cdk-phoenixd = { path = "../cdk-phoenixd", version = "0.6.0", default-features = false } cdk-lnd = { path = "../cdk-lnd", version = "0.6.0", default-features = false } cdk-fake-wallet = { path = "../cdk-fake-wallet", version = "0.6.0", default-features = false } +cdk-signatory = { path = "../cdk-signatory", default-features = false, features = [ + "grpc", +] } cdk-strike = { path = "../cdk-strike", version = "0.6.0" } cdk-axum = { path = "../cdk-axum", version = "0.6.0", default-features = false } config = { version = "0.13.3", features = ["toml"] } diff --git a/crates/cdk-mintd/src/bin/signatory.rs b/crates/cdk-mintd/src/bin/signatory.rs new file mode 100644 index 000000000..d43768a90 --- /dev/null +++ b/crates/cdk-mintd/src/bin/signatory.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; +use std::env; +use std::str::FromStr; + +use bip39::Mnemonic; +use cdk::nuts::CurrencyUnit; +use cdk_mintd::cli::CLIArgs; +use cdk_mintd::env_vars::ENV_WORK_DIR; +use cdk_mintd::{config, work_dir}; +use cdk_signatory::proto::server::grpc_server; +use cdk_signatory::MemorySignatory; +use clap::Parser; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = CLIArgs::parse(); + let work_dir = if let Some(work_dir) = args.work_dir { + tracing::info!("Using work dir from cmd arg"); + work_dir + } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) { + tracing::info!("Using work dir from env var"); + env_work_dir.into() + } else { + work_dir()? + }; + + let config_file_arg = match args.config { + Some(c) => c, + None => work_dir.join("config.toml"), + }; + + let settings = if config_file_arg.exists() { + config::Settings::new(Some(config_file_arg)) + } else { + tracing::info!("Config file does not exist. Attempting to read env vars"); + config::Settings::default() + }; + + // This check for any settings defined in ENV VARs + // ENV VARS will take **priority** over those in the config + let mut settings = settings.from_env()?; + let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?; + + let signatory = MemorySignatory::new( + settings.database.engine.clone().mint(&work_dir).await?, + &mnemonic.to_seed_normalized(""), + settings + .supported_units + .take() + .unwrap_or(vec![CurrencyUnit::default()]) + .into_iter() + .map(|u| (u, (0, 32))) + .collect::>(), + HashMap::new(), + ) + .await?; + + grpc_server(signatory, "[::1]:50051".parse().unwrap()).await?; + + Ok(()) +} diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index b14cfc79b..df9882e7c 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -1,8 +1,11 @@ use std::path::PathBuf; +use std::sync::Arc; use cdk::nuts::{CurrencyUnit, PublicKey}; -use cdk::Amount; +use cdk::{cdk_database, Amount}; use cdk_axum::cache; +use cdk_redb::MintRedbDatabase; +use cdk_sqlite::MintSqliteDatabase; use config::{Config, ConfigError, File}; use serde::{Deserialize, Serialize}; @@ -169,6 +172,30 @@ impl std::str::FromStr for DatabaseEngine { } } +impl DatabaseEngine { + /// Convert the database instance into a mint database + pub async fn mint>( + self, + work_dir: P, + ) -> Result< + Arc + Sync + Send + 'static>, + cdk_database::Error, + > { + match self { + DatabaseEngine::Sqlite => { + let sql_db_path = work_dir.into().join("cdk-mintd.sqlite"); + let db = MintSqliteDatabase::new(&sql_db_path).await?; + db.migrate().await; + Ok(Arc::new(db)) + } + DatabaseEngine::Redb => { + let redb_path = work_dir.into().join("cdk-mintd.redb"); + Ok(Arc::new(MintRedbDatabase::new(&redb_path)?)) + } + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Database { pub engine: DatabaseEngine, @@ -187,6 +214,8 @@ pub struct Settings { pub lnd: Option, pub fake_wallet: Option, pub database: Database, + pub supported_units: Option>, + pub remote_signatory: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/crates/cdk-mintd/src/env_vars.rs b/crates/cdk-mintd/src/env_vars.rs index 27b815b01..29c5287ab 100644 --- a/crates/cdk-mintd/src/env_vars.rs +++ b/crates/cdk-mintd/src/env_vars.rs @@ -72,15 +72,15 @@ pub const ENV_FAKE_WALLET_MIN_DELAY: &str = "CDK_MINTD_FAKE_WALLET_MIN_DELAY"; pub const ENV_FAKE_WALLET_MAX_DELAY: &str = "CDK_MINTD_FAKE_WALLET_MAX_DELAY"; impl Settings { - pub fn from_env(&mut self) -> Result { + pub fn from_env(mut self) -> Result { if let Ok(database) = env::var(DATABASE_ENV_VAR) { let engine = DatabaseEngine::from_str(&database).map_err(|err| anyhow!(err))?; self.database = Database { engine }; } - self.info = self.info.clone().from_env(); - self.mint_info = self.mint_info.clone().from_env(); - self.ln = self.ln.clone().from_env(); + self.info = self.info.from_env(); + self.mint_info = self.mint_info.from_env(); + self.ln = self.ln.from_env(); match self.ln.ln_backend { LnBackend::Cln => { @@ -104,7 +104,7 @@ impl Settings { LnBackend::None => bail!("Ln backend must be set"), } - Ok(self.clone()) + Ok(self) } } diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 1919dcf22..2464982ae 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; +use anyhow::anyhow; + pub mod cli; pub mod config; pub mod env_vars; @@ -22,6 +24,16 @@ fn expand_path(path: &str) -> Option { } } +/// Work dir +pub fn work_dir() -> anyhow::Result { + let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; + let dir = home_dir.join(".cdk-mintd"); + + std::fs::create_dir_all(&dir)?; + + Ok(dir) +} + #[cfg(test)] mod test { use std::env::current_dir; diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index f6d728e67..a0241a106 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -5,17 +5,15 @@ use std::collections::HashMap; use std::env; -use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; -use anyhow::{anyhow, bail, Result}; +use anyhow::bail; use axum::http::Request; use axum::middleware::Next; use axum::response::Response; use axum::{middleware, Router}; use bip39::Mnemonic; -use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning; use cdk::cdk_lightning::MintLightning; use cdk::mint::{MintBuilder, MintMeltLimits}; @@ -25,11 +23,10 @@ use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod}; use cdk::types::LnKey; use cdk_axum::cache::HttpCache; use cdk_mintd::cli::CLIArgs; -use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; +use cdk_mintd::config::{self, LnBackend}; use cdk_mintd::env_vars::ENV_WORK_DIR; use cdk_mintd::setup::LnBackendSetup; -use cdk_redb::MintRedbDatabase; -use cdk_sqlite::MintSqliteDatabase; +use cdk_mintd::work_dir; use clap::Parser; use tokio::sync::Notify; use tower_http::compression::CompressionLayer; @@ -76,7 +73,7 @@ async fn main() -> anyhow::Result<()> { let mut mint_builder = MintBuilder::new(); - let mut settings = if config_file_arg.exists() { + let settings = if config_file_arg.exists() { config::Settings::new(Some(config_file_arg)) } else { tracing::info!("Config file does not exist. Attempting to read env vars"); @@ -86,22 +83,7 @@ async fn main() -> anyhow::Result<()> { // This check for any settings defined in ENV VARs // ENV VARS will take **priority** over those in the config let settings = settings.from_env()?; - - let localstore: Arc + Send + Sync> = - match settings.database.engine { - DatabaseEngine::Sqlite => { - let sql_db_path = work_dir.join("cdk-mintd.sqlite"); - let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?; - - sqlite_db.migrate().await; - - Arc::new(sqlite_db) - } - DatabaseEngine::Redb => { - let redb_path = work_dir.join("cdk-mintd.redb"); - Arc::new(MintRedbDatabase::new(&redb_path)?) - } - }; + let localstore = settings.database.engine.clone().mint(&work_dir).await?; mint_builder = mint_builder.with_localstore(localstore); @@ -306,6 +288,12 @@ async fn main() -> anyhow::Result<()> { .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); + mint_builder = if let Some(remote_signatory) = settings.remote_signatory.clone() { + mint_builder.with_remote_signatory(remote_signatory) + } else { + mint_builder + }; + let cached_endpoints = vec![ CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11), CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11), @@ -411,12 +399,3 @@ async fn logging_middleware(req: Request, next: Next) -> Response { response } - -fn work_dir() -> Result { - let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; - let dir = home_dir.join(".cdk-mintd"); - - std::fs::create_dir_all(&dir)?; - - Ok(dir) -} diff --git a/crates/cdk-signatory/Cargo.toml b/crates/cdk-signatory/Cargo.toml new file mode 100644 index 000000000..972cf76f5 --- /dev/null +++ b/crates/cdk-signatory/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "cdk-signatory" +version = "0.6.0" +edition = "2021" +description = "CDK signatory default implementation" + +[features] +default = [] +grpc = ["dep:tonic", "tokio/full", "dep:prost", "dep:tonic-build"] + +[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", default-features = false, features = ["sync"] } +tonic = { version = "0.11.0", optional = true } +prost = { version = "0.13.1", optional = true } + +[build-dependencies] +tonic-build = { version = "0.11.0", features = ["prost"], optional = true } diff --git a/crates/cdk-signatory/build.rs b/crates/cdk-signatory/build.rs new file mode 100644 index 000000000..746b4345b --- /dev/null +++ b/crates/cdk-signatory/build.rs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(feature = "grpc")] + tonic_build::compile_protos("src/proto/signatory.proto").unwrap(); +} diff --git a/crates/cdk-signatory/src/lib.rs b/crates/cdk-signatory/src/lib.rs new file mode 100644 index 000000000..ddd09a79c --- /dev/null +++ b/crates/cdk-signatory/src/lib.rs @@ -0,0 +1,525 @@ +//! 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; + +#[cfg(feature = "grpc")] +pub mod proto; + +#[cfg(feature = "grpc")] +pub use proto::client::RemoteSigner; + +/// 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) = + <&crate::secret::Secret as TryInto>::try_into(&proof.secret) + { + // Checks and verifies 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/proto/client.rs b/crates/cdk-signatory/src/proto/client.rs new file mode 100644 index 000000000..04bb07755 --- /dev/null +++ b/crates/cdk-signatory/src/proto/client.rs @@ -0,0 +1,67 @@ +use std::collections::HashMap; + +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 crate::proto::signatory_client::SignatoryClient; + +/// A client for the Signatory service. +pub struct RemoteSigner { + client: SignatoryClient, +} + +impl RemoteSigner { + /// Create a new RemoteSigner from a tonic transport channel. + pub async fn new(url: String) -> Result { + Ok(Self { + client: SignatoryClient::connect(url).await?, + }) + } +} + +#[async_trait::async_trait] +impl Signatory for RemoteSigner { + async fn blind_sign(&self, request: BlindedMessage) -> Result { + let req: super::BlindedMessage = request.into(); + self.client + .clone() + .blind_sign(req) + .await + .map(|response| response.into_inner().try_into()) + .map_err(|e| Error::Custom(e.to_string()))? + } + + async fn verify_proof(&self, _proof: Proof) -> Result<(), Error> { + todo!() + } + async fn keyset(&self, _keyset_id: Id) -> Result, Error> { + todo!() + } + + async fn keyset_pubkeys(&self, _keyset_id: Id) -> Result { + todo!() + } + + async fn pubkeys(&self) -> Result { + todo!() + } + + async fn keysets(&self) -> Result { + todo!() + } + + async fn rotate_keyset( + &self, + _unit: CurrencyUnit, + _derivation_path_index: u32, + _max_order: u8, + _input_fee_ppk: u64, + _custom_paths: HashMap, + ) -> Result<(), Error> { + todo!() + } +} diff --git a/crates/cdk-signatory/src/proto/mod.rs b/crates/cdk-signatory/src/proto/mod.rs new file mode 100644 index 000000000..10ffd44f7 --- /dev/null +++ b/crates/cdk-signatory/src/proto/mod.rs @@ -0,0 +1,119 @@ +use cdk_common::{HTLCWitness, P2PKWitness}; +use tonic::Status; + +tonic::include_proto!("cdk_signatory"); + +pub mod client; +pub mod server; + +impl From for BlindedMessage { + fn from(value: cdk_common::BlindedMessage) -> Self { + BlindedMessage { + amount: value.amount.into(), + keyset_id: value.keyset_id.to_string(), + blinded_secret: value.blinded_secret.to_bytes().to_vec(), + witness: value.witness.map(|x| x.into()), + } + } +} + +impl TryInto for BlindedMessage { + type Error = Status; + fn try_into(self) -> Result { + Ok(cdk_common::BlindedMessage { + amount: self.amount.into(), + keyset_id: self + .keyset_id + .parse() + .map_err(|e| Status::from_error(Box::new(e)))?, + blinded_secret: cdk_common::PublicKey::from_slice(&self.blinded_secret) + .map_err(|e| Status::from_error(Box::new(e)))?, + witness: self.witness.map(|x| x.try_into()).transpose()?, + }) + } +} + +impl From for BlindSignatureDleq { + fn from(value: cdk_common::BlindSignatureDleq) -> Self { + BlindSignatureDleq { + e: value.e.as_secret_bytes().to_vec(), + s: value.s.as_secret_bytes().to_vec(), + } + } +} + +impl TryInto for BlindSignatureDleq { + type Error = cdk_common::error::Error; + fn try_into(self) -> Result { + Ok(cdk_common::BlindSignatureDleq { + e: cdk_common::SecretKey::from_slice(&self.e)?, + s: cdk_common::SecretKey::from_slice(&self.s)?, + }) + } +} + +impl From for BlindSignature { + fn from(value: cdk_common::BlindSignature) -> Self { + BlindSignature { + amount: value.amount.into(), + blinded_secret: value.c.to_bytes().to_vec(), + keyset_id: value.keyset_id.to_string(), + dleq: value.dleq.map(|x| x.into()), + } + } +} + +impl TryInto for BlindSignature { + type Error = cdk_common::error::Error; + + fn try_into(self) -> Result { + Ok(cdk_common::BlindSignature { + amount: self.amount.into(), + c: cdk_common::PublicKey::from_slice(&self.blinded_secret)?, + keyset_id: self.keyset_id.parse().expect("Invalid keyset id"), + dleq: self.dleq.map(|dleq| dleq.try_into()).transpose()?, + }) + } +} + +impl From for Witness { + fn from(value: cdk_common::Witness) -> Self { + match value { + cdk_common::Witness::P2PKWitness(P2PKWitness { signatures }) => Witness { + witness_type: Some(witness::WitnessType::P2pkWitness(P2pkWitness { + signatures, + })), + }, + cdk_common::Witness::HTLCWitness(HTLCWitness { + preimage, + signatures, + }) => Witness { + witness_type: Some(witness::WitnessType::HtlcWitness(HtlcWitness { + preimage, + signatures: signatures.unwrap_or_default(), + })), + }, + } + } +} + +impl TryInto for Witness { + type Error = Status; + fn try_into(self) -> Result { + match self.witness_type { + Some(witness::WitnessType::P2pkWitness(P2pkWitness { signatures })) => { + Ok(P2PKWitness { signatures }.into()) + } + Some(witness::WitnessType::HtlcWitness(hltc_witness)) => Ok(HTLCWitness { + preimage: hltc_witness.preimage, + signatures: if hltc_witness.signatures.is_empty() { + None + } else { + Some(hltc_witness.signatures) + }, + } + .into()), + None => Err(Status::invalid_argument("Witness type not set")), + } + } +} diff --git a/crates/cdk-signatory/src/proto/server.rs b/crates/cdk-signatory/src/proto/server.rs new file mode 100644 index 000000000..a594f4fe8 --- /dev/null +++ b/crates/cdk-signatory/src/proto/server.rs @@ -0,0 +1,38 @@ +use std::net::SocketAddr; + +use cdk_common::signatory::Signatory as _; +use tonic::transport::{Error, Server}; +use tonic::{Request, Response, Status}; + +use crate::proto::{self, signatory_server}; +use crate::MemorySignatory; + +struct CdkSignatory(MemorySignatory); + +#[tonic::async_trait] +impl signatory_server::Signatory for CdkSignatory { + async fn blind_sign( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request: {:?}", request); + let blind_signature = self + .0 + .blind_sign(request.into_inner().try_into()?) + .await + .map_err(|e| Status::from_error(Box::new(e)))?; + Ok(Response::new(blind_signature.into())) + } +} + +/// Runs the signatory server +pub async fn grpc_server(signatory: MemorySignatory, addr: SocketAddr) -> Result<(), Error> { + tracing::info!("grpc_server listening on {}", addr); + Server::builder() + .add_service(signatory_server::SignatoryServer::new(CdkSignatory( + signatory, + ))) + .serve(addr) + .await?; + Ok(()) +} diff --git a/crates/cdk-signatory/src/proto/signatory.proto b/crates/cdk-signatory/src/proto/signatory.proto new file mode 100644 index 000000000..6c6105b2d --- /dev/null +++ b/crates/cdk-signatory/src/proto/signatory.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package cdk_signatory; + +service Signatory { + rpc BlindSign (BlindedMessage) returns (BlindSignature); +} + + +message BlindSignature { + uint64 amount = 1; + string keyset_id = 2; + bytes blinded_secret = 3; + optional BlindSignatureDLEQ dleq = 4; +} + +message BlindSignatureDLEQ { + bytes e = 1; + bytes s = 2; +} + + +// Represents a blinded message +message BlindedMessage { + uint64 amount = 1; + string keyset_id = 2; + bytes blinded_secret = 3; + optional Witness witness = 4; // This field is optional by default in proto3 +} + +// Witness type +message Witness { + oneof witness_type { + P2PKWitness p2pk_witness = 1; + HTLCWitness htlc_witness = 2; + } +} + +// P2PKWitness type +message P2PKWitness { + // List of signatures + repeated string signatures = 1; +} + +// HTLCWitness type +message HTLCWitness { + // Preimage + string preimage = 1; + + // List of signatures + repeated string signatures = 2; +} diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 5c28749cf..c0c839787 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -11,17 +11,20 @@ license = "MIT" [features] -default = ["mint", "wallet"] -mint = ["dep:futures", "cdk-common/mint"] +mint = ["dep:futures", "cdk-common/mint", "cdk-signatory"] # We do not commit to a MSRV with swagger enabled swagger = ["mint", "dep:utoipa", "cdk-common/swagger"] +# We do not commit to a MSRV with grpc enabled +grpc = ["mint", "cdk-signatory/grpc"] +# We do not commit to a MSRV with grpc enabled wallet = ["dep:reqwest", "cdk-common/wallet"] bench = [] http_subscription = [] [dependencies] -cdk-common = { path = "../cdk-common", version = "0.6.0" } +cdk-common = { path = "../cdk-common" } +cdk-signatory = { path = "../cdk-signatory", default-features = false, optional = true } cbor-diag = "0.1.12" async-trait = "0.1" anyhow = { version = "1.0.43", features = ["backtrace"] } @@ -62,6 +65,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 = [ @@ -105,3 +109,4 @@ criterion = "0.5.1" [[bench]] name = "dhke_benchmarks" harness = false +default = ["mint", "wallet"] diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index 5ab52befa..206979065 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}; @@ -18,6 +20,12 @@ use crate::nuts::{ }; use crate::types::{LnKey, QuoteTTL}; +#[derive(Debug)] +pub enum SignatoryInfo { + Seed(Vec), + Remote(String), +} + /// Cashu Mint #[derive(Default)] pub struct MintBuilder { @@ -27,9 +35,11 @@ pub struct MintBuilder { localstore: Option + Send + Sync>>, /// Ln backends for mint ln: Option + Send + Sync>>>, - seed: Option>, + signatory_info: Option, quote_ttl: Option, - supported_units: HashMap, + /// expose supported units + pub supported_units: HashMap, + signatory: Option>, } impl MintBuilder { @@ -52,6 +62,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, @@ -61,9 +77,15 @@ impl MintBuilder { self } - /// Set seed + /// Set seed to create a local signatory pub fn with_seed(mut self, seed: Vec) -> Self { - self.seed = Some(seed); + self.signatory_info = Some(SignatoryInfo::Seed(seed)); + self + } + + /// connect to a remote signatary instead of a creating a local one + pub fn with_remote_signatory(mut self, url: String) -> Self { + self.signatory_info = Some(SignatoryInfo::Remote(url)); self } @@ -217,23 +239,51 @@ impl MintBuilder { } /// Build mint - pub async fn build(&self) -> anyhow::Result { - let localstore = self - .localstore - .clone() - .ok_or(anyhow!("Localstore not set"))?; + pub async fn build(self) -> anyhow::Result { + let localstore = self.localstore.ok_or(anyhow!("Localstore not set"))?; localstore.set_mint_info(self.mint_info.clone()).await?; localstore .set_quote_ttl(self.quote_ttl.ok_or(anyhow!("Quote ttl not set"))?) .await?; + let signatory = if let Some(signatory) = self.signatory { + signatory + } else { + match self.signatory_info { + Some(SignatoryInfo::Seed(seed)) => Arc::new( + cdk_signatory::MemorySignatory::new( + localstore.clone(), + &seed, + self.supported_units, + HashMap::new(), + ) + .await?, + ) + as Arc, + #[cfg(feature = "grpc")] + Some(SignatoryInfo::Remote(url)) => Arc::new( + cdk_signatory::RemoteSigner::new(url) + .await + .map_err(|e| anyhow!("Remote signatory error: {}", e.to_string()))?, + ) + as Arc, + #[cfg(not(feature = "grpc"))] + Some(SignatoryInfo::Remote(url)) => panic!( + "CDK not compiled with grpc feature, therefore the remote signatory is disabled (url={})", url + ), + None => { + return Err(anyhow!("Signatory not set")); + } + } + }; + + let signatory_manager = Arc::new(SignatoryManager::new(signatory)); + Ok(Mint::new( - self.seed.as_ref().ok_or(anyhow!("Mint seed 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 new file mode 100644 index 000000000..de745d135 --- /dev/null +++ b/crates/cdk/src/mint/config.rs @@ -0,0 +1,103 @@ +//! Active mint configuration +//! +//! This is the active configuration that can be updated at runtime. +use std::sync::Arc; + +use arc_swap::ArcSwap; + +use super::MintInfo; +use crate::mint_url::MintUrl; +use crate::types::QuoteTTL; + +/// Mint Inner configuration +pub struct Config { + /// Mint url + pub mint_info: MintInfo, + /// Mint config + pub mint_url: MintUrl, + /// Quotes ttl + pub quote_ttl: QuoteTTL, +} + +/// Mint configuration +/// +/// This struct is used to configure the mint, and it is wrapped inside a ArcSwap, so it can be +/// updated at runtime without locking the shared config nor without requiriming a mutable reference +/// to the config +/// +/// ArcSwap is used instead of a RwLock since the updates should be less frequent than the reads +#[derive(Clone)] +pub struct SwappableConfig { + config: Arc>, +} + +impl SwappableConfig { + /// Creates a new configuration instance + pub fn new(mint_url: MintUrl, quote_ttl: QuoteTTL, mint_info: MintInfo) -> Self { + let inner = Config { + quote_ttl, + mint_info, + mint_url, + }; + + Self { + config: Arc::new(ArcSwap::from_pointee(inner)), + } + } + + /// Gets an Arc of the current configuration + pub fn load(&self) -> Arc { + self.config.load().clone() + } + + /// Gets a copy of the mint url + pub fn mint_url(&self) -> MintUrl { + self.load().mint_url.clone() + } + + /// Replace the current mint url with a new one + pub fn set_mint_url(&self, mint_url: MintUrl) { + let current_inner = self.load(); + let new_inner = Config { + mint_url, + quote_ttl: current_inner.quote_ttl, + mint_info: current_inner.mint_info.clone(), + }; + + self.config.store(Arc::new(new_inner)); + } + + /// Gets a copy of the quote ttl + pub fn quote_ttl(&self) -> QuoteTTL { + self.load().quote_ttl + } + + /// Replaces the current quote ttl with a new one + pub fn set_quote_ttl(&self, quote_ttl: QuoteTTL) { + let current_inner = self.load(); + let new_inner = Config { + mint_info: current_inner.mint_info.clone(), + mint_url: current_inner.mint_url.clone(), + quote_ttl, + }; + + self.config.store(Arc::new(new_inner)); + } + + /// Gets a copy of the mint info + pub fn mint_info(&self) -> MintInfo { + self.load().mint_info.clone() + } + + /// Replaces the current mint info with a new one + pub fn set_mint_info(&self, mint_info: MintInfo) { + let current_inner = self.load(); + let new_inner = Config { + mint_info, + mint_url: current_inner.mint_url.clone(), + quote_ttl: current_inner.quote_ttl, + }; + + self.config.store(Arc::new(new_inner)); + } +} diff --git a/crates/cdk/src/mint/keysets.rs b/crates/cdk/src/mint/keysets.rs index c099381cf..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,77 +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 - .keysets - .read() - .await - .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 - .keysets - .read() - .await - .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 keysets = self.keysets.read().await; - 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 @@ -98,62 +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.keysets.write().await; - keysets.insert(id, keyset); - - Ok(()) - } - - /// Ensure Keyset is loaded in mint - #[instrument(skip(self))] - pub async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> { - { - let keysets = self.keysets.read().await; - if keysets.contains_key(id) { - return Ok(()); - } - } - - let mut keysets = self.keysets.write().await; - let keyset_info = self - .localstore - .get_keyset_info(id) - .await? - .ok_or(Error::UnknownKeySet)?; - let id = keyset_info.id; - keysets.insert(id, self.generate_keyset(keyset_info)); - - Ok(()) - } - - /// 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 910c4da2c..6b91f9e3a 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -3,25 +3,21 @@ use std::collections::HashMap; use std::sync::Arc; -use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; -use bitcoin::secp256k1::{self, Secp256k1}; use cdk_common::common::LnKey; use cdk_common::database::{self, MintDatabase}; -use cdk_common::mint::MintKeySetInfo; use futures::StreamExt; use serde::{Deserialize, Serialize}; +use signatory::SignatoryManager; use subscription::PubSubManager; -use tokio::sync::{Notify, RwLock}; +use tokio::sync::Notify; use tokio::task::JoinSet; 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::nuts::*; -use crate::util::unix_time; use crate::Amount; mod builder; @@ -29,12 +25,17 @@ mod check_spendable; 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}; +#[cfg(feature = "grpc")] +pub use cdk_signatory::proto::client::RemoteSigner; +pub use cdk_signatory::MemorySignatory; /// Cashu Mint #[derive(Clone)] @@ -45,143 +46,23 @@ pub struct Mint { pub ln: HashMap + Send + Sync>>, /// Subscription manager pub pubsub_manager: Arc, - secp_ctx: Secp256k1, - xpriv: Xpriv, - keysets: Arc>>, + /// Signatory + pub signatory: Arc, } impl Mint { /// Create new [`Mint`] #[allow(clippy::too_many_arguments)] pub async fn new( - seed: &[u8], 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); - } - } - - let keysets = Arc::new(RwLock::new(active_keysets)); - Ok(Self { pubsub_manager: Arc::new(localstore.clone().into()), - secp_ctx, - xpriv, localstore, ln, - keysets, + signatory, }) } @@ -283,87 +164,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 keysets = self.keysets.read().await; - 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 verifies 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 keysets = self.keysets.read().await; - 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 @@ -509,148 +316,18 @@ 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 std::str::FromStr; - 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::*; use crate::cdk_database::mint_memory::MintMemoryDatabase; - #[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); - } - #[derive(Default)] struct MintConfig<'a> { active_keysets: HashMap, @@ -688,14 +365,18 @@ mod tests { .unwrap(), ); - Mint::new( - config.seed, - localstore, - HashMap::new(), - config.supported_units, - HashMap::new(), - ) - .await + 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(localstore, HashMap::new(), signatory_manager).await } #[tokio::test] @@ -787,12 +468,27 @@ mod tests { mint.rotate_keyset(CurrencyUnit::default(), 0, 32, 1, HashMap::new()) .await?; - let keys = mint.keysets.read().await.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) -> (), +} diff --git a/misc/itests.sh b/misc/itests.sh index cb1276062..c1fdbab43 100755 --- a/misc/itests.sh +++ b/misc/itests.sh @@ -45,8 +45,8 @@ fi echo "Temp directory created: $cdk_itests" export MINT_DATABASE="$1"; -cargo build -p cdk-integration-tests -cargo build --bin regtest_mint +cargo build -p cdk-integration-tests +cargo build --bin regtest_mint # cargo run --bin regtest_mint > "$cdk_itests/mint.log" 2>&1 & cargo run --bin regtest_mint & @@ -60,7 +60,7 @@ START_TIME=$(date +%s) while true; do # Get the current time CURRENT_TIME=$(date +%s) - + # Calculate the elapsed time ELAPSED_TIME=$((CURRENT_TIME - START_TIME)) @@ -83,6 +83,7 @@ while true; do fi done +export RUST_BACKTRACE=1 # Run cargo test cargo test -p cdk-integration-tests --test regtest