Browse Source

Introduce a SignatoryManager service.

The SignatoryManager manager provides an API to interact with keysets, private
keys, and all key-related operations, offering segregation between the mint and
the most sensible part of the mind: the private keys.

Although the default signatory runs in memory, it is completely isolated from
the rest of the system and can only be communicated through the interface
offered by the signatory manager. Only messages can be sent from the mintd to
the Signatory trait through the Signatory Manager.

This pull request sets the foundation for eventually being able to run the
Signatory and all the key-related operations in a separate service, possibly in
a foreign service, to offload risks, as described in #476.

The Signatory manager is concurrent and deferred any mechanism needed to handle
concurrency to the Signatory trait.
Cesar Rodas 2 months ago
parent
commit
792b7a3180

+ 12 - 0
crates/cashu/src/nuts/nut00/mod.rs

@@ -202,6 +202,18 @@ pub enum Witness {
     HTLCWitness(HTLCWitness),
 }
 
+impl From<P2PKWitness> for Witness {
+    fn from(witness: P2PKWitness) -> Self {
+        Self::P2PKWitness(witness)
+    }
+}
+
+impl From<HTLCWitness> for Witness {
+    fn from(witness: HTLCWitness) -> Self {
+        Self::HTLCWitness(witness)
+    }
+}
+
 impl Witness {
     /// Add signatures to [`Witness`]
     pub fn add_signatures(&mut self, signatues: Vec<String>) {

+ 8 - 0
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<AmountStr, PublicKey>);
 
+impl Deref for Keys {
+    type Target = BTreeMap<AmountStr, PublicKey>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
 impl From<MintKeys> for Keys {
     fn from(keys: MintKeys) -> Self {
         Self(

+ 2 - 2
crates/cdk-axum/Cargo.toml

@@ -28,8 +28,8 @@ futures = { version = "0.3.28", default-features = false }
 moka = { version = "0.11.1", features = ["future"] }
 serde_json = "1"
 paste = "1.0.15"
-serde = { version = "1", features = ["derive"] }
-uuid = { version = "1", features = ["v4", "serde"] }
+serde = { version = "1.0.210", features = ["derive"] }
+uuid = { version = "=1.12.1", features = ["v4", "serde"] }
 sha2 = "0.10.8"
 redis = { version = "0.23.3", features = [
     "tokio-rustls-comp",

+ 9 - 4
crates/cdk-cln/Cargo.toml

@@ -6,17 +6,22 @@ authors = ["CDK Developers"]
 license = "MIT"
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0" # MSRV
+rust-version = "1.63.0"                            # MSRV
 description = "CDK ln backend for cln"
 
 [dependencies]
 async-trait = "0.1"
 bitcoin = { version = "0.32.2", default-features = false }
-cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = ["mint"] }
+cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = [
+    "mint",
+] }
 cln-rpc = "0.3.0"
 futures = { version = "0.3.28", default-features = false }
 tokio = { version = "1", default-features = false }
 tokio-util = { version = "0.7.11", default-features = false }
-tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
+tracing = { version = "0.1", default-features = false, features = [
+    "attributes",
+    "log",
+] }
 thiserror = "1"
-uuid = { version = "1", features = ["v4"] }
+uuid = { version = "=1.12.1", features = ["v4"] }

+ 8 - 0
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")]

+ 2 - 0
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;
 

+ 74 - 0
crates/cdk-common/src/signatory.rs

@@ -0,0 +1,74 @@
+//! 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::mint::MintKeySetInfo;
+use cashu::{
+    BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Proof,
+};
+
+use super::error::Error;
+
+/// Type alias to make the keyset info API more useful, queryable by unit and Id
+pub enum KeysetIdentifier {
+    /// Mint Keyset by unit
+    Unit(CurrencyUnit),
+    /// Mint Keyset by Id
+    Id(Id),
+}
+
+impl From<Id> for KeysetIdentifier {
+    fn from(id: Id) -> Self {
+        Self::Id(id)
+    }
+}
+
+impl From<CurrencyUnit> for KeysetIdentifier {
+    fn from(unit: CurrencyUnit) -> Self {
+        Self::Unit(unit)
+    }
+}
+
+#[async_trait::async_trait]
+/// Signatory trait
+pub trait Signatory {
+    /// Blind sign a message
+    async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result<BlindSignature, Error>;
+
+    /// 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<Option<KeySet>, Error>;
+
+    /// Retrieve the public keys of a keyset
+    async fn keyset_pubkeys(&self, keyset_id: Id) -> Result<KeysResponse, Error>;
+
+    /// Retrieve the public keys of the active keyset for distribution to wallet
+    /// clients
+    async fn pubkeys(&self) -> Result<KeysResponse, Error>;
+
+    /// Return a list of all supported keysets
+    async fn keysets(&self) -> Result<KeysetResponse, Error>;
+
+    /// 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<CurrencyUnit, DerivationPath>,
+    ) -> Result<MintKeySetInfo, Error>;
+
+    /// Get Mint Keyset Info by Unit or Id
+    async fn get_keyset_info(&self, keyset_id: KeysetIdentifier) -> Result<MintKeySetInfo, Error>;
+}

+ 1 - 1
crates/cdk-integration-tests/Cargo.toml

@@ -32,7 +32,7 @@ futures = { version = "0.3.28", default-features = false, features = [
     "executor",
 ] }
 once_cell = "1.19.0"
-uuid = { version = "1", features = ["v4"] }
+uuid = { version = "=1.12.1", features = ["v4"] }
 serde = "1"
 serde_json = "1"
 # ln-regtest-rs = { path = "../../../../ln-regtest-rs" }

+ 14 - 1
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::types::QuoteTTL;
 use cdk_cln::Cln as CdkCln;
@@ -156,7 +157,9 @@ where
     L: MintLightning<Err = cdk_lightning::Error> + Send + Sync + 'static,
 {
     let mut mint_builder = MintBuilder::new();
+
     let localstore = Arc::new(database);
+
     mint_builder = mint_builder.with_localstore(localstore.clone());
 
     mint_builder = mint_builder.add_ln_backend(
@@ -168,8 +171,18 @@ where
 
     let mnemonic = Mnemonic::generate(12)?;
 
+    let signatory_manager = MemorySignatory::new(
+        localstore.clone(),
+        &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_seed(mnemonic.to_seed_normalized("").to_vec());
 

+ 28 - 13
crates/cdk-integration-tests/tests/mint.rs

@@ -1,6 +1,7 @@
 //! Mint tests
 
 use std::collections::{HashMap, HashSet};
+use std::ops::Deref;
 use std::sync::Arc;
 use std::time::Duration;
 
@@ -10,7 +11,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::{FeeReserve, MintBuilder, MintMeltLimits, MintQuote};
+use cdk::mint::signatory::SignatoryManager;
+use cdk::mint::{FeeReserve, MemorySignatory, MintBuilder, MintMeltLimits, MintQuote};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
     CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod,
@@ -26,7 +28,7 @@ use tokio::time::sleep;
 
 pub const MINT_URL: &str = "http://127.0.0.1:8088";
 
-static INSTANCE: OnceCell<Mint> = OnceCell::const_new();
+static INSTANCE: OnceCell<Arc<Mint>> = OnceCell::const_new();
 
 async fn new_mint(fee: u64) -> Mint {
     let mut supported_units = HashMap::new();
@@ -51,19 +53,29 @@ async fn new_mint(fee: u64) -> Mint {
         .expect("Could not set mint info");
     let mnemonic = Mnemonic::generate(12).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(
-        &mnemonic.to_seed_normalized(""),
-        Arc::new(localstore),
+        localstore,
         HashMap::new(),
-        supported_units,
+        signatory_manager,
         HashMap::new(),
     )
     .await
     .unwrap()
 }
 
-async fn initialize() -> &'static Mint {
-    INSTANCE.get_or_init(|| new_mint(0)).await
+async fn initialize() -> Arc<Mint> {
+    INSTANCE
+        .get_or_init(|| async { Arc::new(new_mint(0).await) })
+        .await
+        .clone()
 }
 
 async fn mint_proofs(
@@ -115,7 +127,7 @@ async fn test_mint_double_spend() -> Result<()> {
     let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
     let keyset_id = Id::from(&keys);
 
-    let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?;
+    let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?;
 
     let preswap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?;
 
@@ -149,7 +161,7 @@ async fn test_attempt_to_swap_by_overflowing() -> Result<()> {
     let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
     let keyset_id = Id::from(&keys);
 
-    let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?;
+    let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?;
 
     let amount = 2_u64.pow(63);
 
@@ -188,7 +200,7 @@ pub async fn test_p2pk_swap() -> Result<()> {
     let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
     let keyset_id = Id::from(&keys);
 
-    let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?;
+    let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?;
 
     let secret = SecretKey::generate();
 
@@ -306,7 +318,7 @@ async fn test_swap_unbalanced() -> Result<()> {
     let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
     let keyset_id = Id::from(&keys);
 
-    let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?;
+    let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?;
 
     let preswap = PreMintSecrets::random(keyset_id, 95.into(), &SplitTarget::default())?;
 
@@ -470,7 +482,7 @@ async fn test_correct_keyset() -> Result<()> {
         .with_description("regtest mint".to_string())
         .with_seed(mnemonic.to_seed_normalized("").to_vec());
 
-    let mint = mint_builder.build().await?;
+    let mint = mint_builder.clone().build().await?;
 
     localstore
         .set_mint_info(mint_builder.mint_info.clone())
@@ -495,7 +507,10 @@ async fn test_correct_keyset() -> Result<()> {
 
     assert!(keyset_info.derivation_path_index == Some(2));
 
-    let mint = mint_builder.build().await?;
+    let mint = mint_builder
+        .with_signatory(mint.signatory.deref().deref().to_owned())
+        .build()
+        .await?;
 
     let active = mint.localstore.get_active_keysets().await?;
 

+ 11 - 8
crates/cdk-mintd/Cargo.toml

@@ -26,14 +26,17 @@ cdk-redb = { path = "../cdk-redb", version = "0.7.1", default-features = false,
 cdk-sqlite = { path = "../cdk-sqlite", version = "0.7.1", default-features = false, features = [
     "mint",
 ] }
-cdk-cln = { path = "../cdk-cln", version = "0.7.1", default-features = false }
-cdk-lnbits = { path = "../cdk-lnbits", version = "0.7.1", default-features = false }
-cdk-phoenixd = { path = "../cdk-phoenixd", version = "0.7.1", default-features = false }
-cdk-lnd = { path = "../cdk-lnd", version = "0.7.1", default-features = false }
-cdk-fake-wallet = { path = "../cdk-fake-wallet", version = "0.7.1", default-features = false }
-cdk-strike = { path = "../cdk-strike", version = "0.7.1" }
-cdk-axum = { path = "../cdk-axum", version = "0.7.1", default-features = false }
-cdk-mint-rpc = { path = "../cdk-mint-rpc", version = "0.7.1", default-features = false, optional = true }
+cdk-cln = { path = "../cdk-cln", version = "0.7.0", default-features = false }
+cdk-lnbits = { path = "../cdk-lnbits", version = "0.7.0", default-features = false }
+cdk-phoenixd = { path = "../cdk-phoenixd", version = "0.7.0", default-features = false }
+cdk-lnd = { path = "../cdk-lnd", version = "0.7.0", default-features = false }
+cdk-fake-wallet = { path = "../cdk-fake-wallet", version = "0.7.0", default-features = false }
+cdk-strike = { path = "../cdk-strike", version = "0.7.0" }
+cdk-axum = { path = "../cdk-axum", version = "0.7.0", default-features = false }
+cdk-mint-rpc = { path = "../cdk-mint-rpc", version = "0.7.0", default-features = false, optional = true }
+cdk-signatory = { path = "../cdk-signatory", default-features = false, features = [
+    "grpc",
+] }
 config = { version = "0.13.3", features = ["toml"] }
 clap = { version = "~4.0.32", features = ["derive"] }
 bitcoin = { version = "0.32.2", features = [

+ 61 - 0
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<dyn std::error::Error>> {
+    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<_, _>>(),
+        HashMap::new(),
+    )
+    .await?;
+
+    grpc_server(signatory, "[::1]:50051".parse().unwrap()).await?;
+
+    Ok(())
+}

+ 30 - 1
crates/cdk-mintd/src/config.rs

@@ -1,9 +1,12 @@
 use std::path::PathBuf;
+use std::sync::Arc;
 
 use bitcoin::hashes::{sha256, Hash};
 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};
 
@@ -187,6 +190,30 @@ impl std::str::FromStr for DatabaseEngine {
     }
 }
 
+impl DatabaseEngine {
+    /// Convert the database instance into a mint database
+    pub async fn mint<P: Into<PathBuf>>(
+        self,
+        work_dir: P,
+    ) -> Result<
+        Arc<dyn cdk_database::MintDatabase<Err = cdk_database::Error> + 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,
@@ -207,6 +234,8 @@ pub struct Settings {
     pub database: Database,
     #[cfg(feature = "management-rpc")]
     pub mint_management_rpc: Option<MintManagementRpc>,
+    pub supported_units: Option<Vec<CurrencyUnit>>,
+    pub remote_signatory: Option<String>,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]

+ 5 - 5
crates/cdk-mintd/src/env_vars.rs

@@ -83,15 +83,15 @@ pub const ENV_MINT_MANAGEMENT_PORT: &str = "CDK_MINTD_MANAGEMENT_PORT";
 pub const ENV_MINT_MANAGEMENT_TLS_DIR_PATH: &str = "CDK_MINTD_MANAGEMENT_TLS_DIR_PATH";
 
 impl Settings {
-    pub fn from_env(&mut self) -> Result<Self> {
+    pub fn from_env(mut self) -> Result<Self> {
         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();
 
         #[cfg(feature = "management-rpc")]
         {
@@ -125,7 +125,7 @@ impl Settings {
             LnBackend::None => bail!("Ln backend must be set"),
         }
 
-        Ok(self.clone())
+        Ok(self)
     }
 }
 

+ 12 - 0
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<PathBuf> {
     }
 }
 
+/// Work dir
+pub fn work_dir() -> anyhow::Result<PathBuf> {
+    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;

+ 11 - 32
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};
@@ -27,11 +25,10 @@ use cdk_axum::cache::HttpCache;
 #[cfg(feature = "management-rpc")]
 use cdk_mint_rpc::MintRPCServer;
 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;
@@ -79,7 +76,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");
@@ -89,22 +86,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<dyn MintDatabase<Err = cdk_database::Error> + 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);
 
@@ -308,6 +290,12 @@ async fn main() -> anyhow::Result<()> {
         .with_description(settings.mint_info.description)
         .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),
@@ -460,12 +448,3 @@ async fn logging_middleware<B>(req: Request<B>, next: Next<B>) -> Response {
 
     response
 }
-
-fn work_dir() -> Result<PathBuf> {
-    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)
-}

+ 9 - 4
crates/cdk-phoenixd/Cargo.toml

@@ -6,7 +6,7 @@ authors = ["CDK Developers"]
 license = "MIT"
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0" # MSRV
+rust-version = "1.63.0"                            # MSRV
 description = "CDK ln backend for phoenixd"
 
 [dependencies]
@@ -14,11 +14,16 @@ async-trait = "0.1"
 anyhow = "1"
 axum = "0.6.20"
 bitcoin = { version = "0.32.2", default-features = false }
-cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = ["mint"] }
+cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = [
+    "mint",
+] }
 futures = { version = "0.3.28", default-features = false }
 tokio = { version = "1", default-features = false }
 tokio-util = { version = "0.7.11", default-features = false }
-tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
+tracing = { version = "0.1", default-features = false, features = [
+    "attributes",
+    "log",
+] }
 thiserror = "1"
 phoenixd-rs = "0.4.0"
-uuid = { version = "1", features = ["v4"] }
+uuid = { version = "=1.12.1", features = ["v4"] }

+ 1 - 1
crates/cdk-redb/Cargo.toml

@@ -27,4 +27,4 @@ tracing = { version = "0.1", default-features = false, features = [
 serde = { version = "1", default-features = false, features = ["derive"] }
 serde_json = "1"
 lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
-uuid = { version = "1", features = ["v4", "serde"] }
+uuid = { version = "=1.12.1", features = ["v4", "serde"] }

+ 28 - 0
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.12.6", optional = true }
+
+[build-dependencies]
+tonic-build = { version = "0.11.0", features = ["prost"], optional = true }

+ 4 - 0
crates/cdk-signatory/build.rs

@@ -0,0 +1,4 @@
+fn main() {
+    #[cfg(feature = "grpc")]
+    tonic_build::compile_protos("src/proto/signatory.proto").unwrap();
+}

+ 541 - 0
crates/cdk-signatory/src/lib.rs

@@ -0,0 +1,541 @@
+//! 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::{KeysetIdentifier, 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<C: secp256k1::Signing>(
+    secp: &secp256k1::Secp256k1<C>,
+    xpriv: Xpriv,
+    derivation_path: DerivationPath,
+    derivation_path_index: Option<u32>,
+    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<DerivationPath> {
+    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<HashMap<Id, MintKeySet>>,
+    localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
+    secp_ctx: Secp256k1<secp256k1::All>,
+    xpriv: Xpriv,
+}
+
+impl MemorySignatory {
+    /// Creates a new MemorySignatory instance
+    pub async fn new(
+        localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
+        seed: &[u8],
+        supported_units: HashMap<CurrencyUnit, (u64, u8)>,
+        custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    ) -> Result<Self, Error> {
+        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<CurrencyUnit, Vec<MintKeySetInfo>> =
+                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<MintKeySetInfo> = 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<MintKeySetInfo, Error> {
+        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<MintKeyPair, Error> {
+        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<BlindSignature, Error> {
+        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<crate::nuts::nut10::Secret>>::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<Option<KeySet>, 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<KeysResponse, Error> {
+        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<KeysResponse, Error> {
+        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<KeysetResponse, Error> {
+        let keysets = self.localstore.get_keyset_infos().await?;
+        let active_keysets: HashSet<Id> = 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<CurrencyUnit, DerivationPath>,
+    ) -> Result<MintKeySetInfo, 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.clone()).await?;
+        self.localstore.set_active_keyset(unit, id).await?;
+
+        let mut keysets = self.keysets.write().await;
+        keysets.insert(id, keyset);
+
+        Ok(keyset_info)
+    }
+
+    async fn get_keyset_info(&self, keyset_id: KeysetIdentifier) -> Result<MintKeySetInfo, Error> {
+        let keyset_id = match keyset_id {
+            KeysetIdentifier::Id(id) => id,
+            KeysetIdentifier::Unit(unit) => self
+                .localstore
+                .get_active_keyset_id(&unit)
+                .await?
+                .ok_or(Error::UnsupportedUnit)?,
+        };
+
+        self.localstore
+            .get_keyset_info(&keyset_id)
+            .await?
+            .ok_or(Error::UnknownKeySet)
+    }
+}
+
+#[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);
+    }
+}

+ 72 - 0
crates/cdk-signatory/src/proto/client.rs

@@ -0,0 +1,72 @@
+use std::collections::HashMap;
+
+use bitcoin::bip32::DerivationPath;
+use cdk_common::error::Error;
+use cdk_common::mint::MintKeySetInfo;
+use cdk_common::signatory::{KeysetIdentifier, 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<tonic::transport::Channel>,
+}
+
+impl RemoteSigner {
+    /// Create a new RemoteSigner from a tonic transport channel.
+    pub async fn new(url: String) -> Result<Self, tonic::transport::Error> {
+        Ok(Self {
+            client: SignatoryClient::connect(url).await?,
+        })
+    }
+}
+
+#[async_trait::async_trait]
+impl Signatory for RemoteSigner {
+    async fn blind_sign(&self, request: BlindedMessage) -> Result<BlindSignature, Error> {
+        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<Option<KeySet>, Error> {
+        todo!()
+    }
+
+    async fn keyset_pubkeys(&self, _keyset_id: Id) -> Result<KeysResponse, Error> {
+        todo!()
+    }
+
+    async fn pubkeys(&self) -> Result<KeysResponse, Error> {
+        todo!()
+    }
+
+    async fn keysets(&self) -> Result<KeysetResponse, Error> {
+        todo!()
+    }
+
+    async fn get_keyset_info(&self, _keyset_id: KeysetIdentifier) -> Result<MintKeySetInfo, Error> {
+        todo!()
+    }
+
+    async fn rotate_keyset(
+        &self,
+        _unit: CurrencyUnit,
+        _derivation_path_index: u32,
+        _max_order: u8,
+        _input_fee_ppk: u64,
+        _custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    ) -> Result<MintKeySetInfo, Error> {
+        todo!()
+    }
+}

+ 119 - 0
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<cdk_common::BlindedMessage> 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<cdk_common::BlindedMessage> for BlindedMessage {
+    type Error = Status;
+    fn try_into(self) -> Result<cdk_common::BlindedMessage, Self::Error> {
+        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<cdk_common::BlindSignatureDleq> 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<cdk_common::BlindSignatureDleq> for BlindSignatureDleq {
+    type Error = cdk_common::error::Error;
+    fn try_into(self) -> Result<cdk_common::BlindSignatureDleq, Self::Error> {
+        Ok(cdk_common::BlindSignatureDleq {
+            e: cdk_common::SecretKey::from_slice(&self.e)?,
+            s: cdk_common::SecretKey::from_slice(&self.s)?,
+        })
+    }
+}
+
+impl From<cdk_common::BlindSignature> 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<cdk_common::BlindSignature> for BlindSignature {
+    type Error = cdk_common::error::Error;
+
+    fn try_into(self) -> Result<cdk_common::BlindSignature, Self::Error> {
+        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<cdk_common::Witness> 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<cdk_common::Witness> for Witness {
+    type Error = Status;
+    fn try_into(self) -> Result<cdk_common::Witness, Self::Error> {
+        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")),
+        }
+    }
+}

+ 38 - 0
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<proto::BlindedMessage>,
+    ) -> Result<Response<proto::BlindSignature>, 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(())
+}

+ 52 - 0
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;
+}

+ 1 - 1
crates/cdk-sqlite/Cargo.toml

@@ -34,4 +34,4 @@ tracing = { version = "0.1", default-features = false, features = [
 ] }
 serde_json = "1"
 lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
-uuid = { version = "1", features = ["v4", "serde"] }
+uuid = { version = "=1.12.1", features = ["v4", "serde"] }

+ 9 - 4
crates/cdk-strike/Cargo.toml

@@ -6,7 +6,7 @@ authors = ["CDK Developers"]
 license = "MIT"
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0" # MSRV
+rust-version = "1.63.0"                            # MSRV
 description = "CDK ln backend for Strike api"
 
 [dependencies]
@@ -14,13 +14,18 @@ async-trait = "0.1"
 anyhow = "1"
 axum = "0.6.20"
 bitcoin = { version = "0.32.2", default-features = false }
-cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = ["mint"] }
+cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = [
+    "mint",
+] }
 futures = { version = "0.3.28", default-features = false }
 tokio = { version = "1", default-features = false }
 tokio-util = { version = "0.7.11", default-features = false }
-tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
+tracing = { version = "0.1", default-features = false, features = [
+    "attributes",
+    "log",
+] }
 thiserror = "1"
-uuid = { version = "1", features = ["v4"] }
+uuid = { version = "=1.12.1", features = ["v4"] }
 strike-rs = "0.4.0"
 # strike-rs = { path = "../../../../strike-rs" }
 # strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "577ad9591" }

+ 7 - 2
crates/cdk/Cargo.toml

@@ -11,10 +11,12 @@ 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 = []
@@ -22,6 +24,7 @@ http_subscription = []
 
 [dependencies]
 cdk-common = { path = "../cdk-common", version = "0.7.1" }
+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.12.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"]

+ 61 - 7
crates/cdk/src/mint/builder.rs

@@ -6,9 +6,11 @@ use std::sync::Arc;
 use anyhow::anyhow;
 use bitcoin::bip32::DerivationPath;
 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};
@@ -19,8 +21,14 @@ use crate::nuts::{
 };
 use crate::types::LnKey;
 
+#[derive(Clone, Debug)]
+pub enum SignatoryInfo {
+    Seed(Vec<u8>),
+    Remote(String),
+}
+
 /// Cashu Mint
-#[derive(Default)]
+#[derive(Default, Clone)]
 pub struct MintBuilder {
     /// Mint Info
     pub mint_info: MintInfo,
@@ -28,8 +36,10 @@ pub struct MintBuilder {
     localstore: Option<Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>>,
     /// Ln backends for mint
     ln: Option<HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>>,
-    seed: Option<Vec<u8>>,
-    supported_units: HashMap<CurrencyUnit, (u64, u8)>,
+    signatory_info: Option<SignatoryInfo>,
+    /// expose supported units
+    pub supported_units: HashMap<CurrencyUnit, (u64, u8)>,
+    signatory: Option<Arc<dyn Signatory + Sync + Send + 'static>>,
     custom_paths: HashMap<CurrencyUnit, DerivationPath>,
 }
 
@@ -53,6 +63,12 @@ impl MintBuilder {
         builder
     }
 
+    /// Set signatory service
+    pub fn with_signatory(mut self, signatory: Arc<dyn Signatory + Sync + Send + 'static>) -> Self {
+        self.signatory = Some(signatory);
+        self
+    }
+
     /// Set localstore
     pub fn with_localstore(
         mut self,
@@ -62,9 +78,15 @@ impl MintBuilder {
         self
     }
 
-    /// Set seed
+    /// Set seed to create a local signatory
     pub fn with_seed(mut self, seed: Vec<u8>) -> 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
     }
 
@@ -224,11 +246,43 @@ impl MintBuilder {
             .clone()
             .ok_or(anyhow!("Localstore not set"))?;
 
+        let signatory = if let Some(signatory) = self.signatory.as_ref() {
+            signatory.clone()
+        } else {
+            match self.signatory_info.as_ref() {
+                Some(SignatoryInfo::Seed(seed)) => Arc::new(
+                    cdk_signatory::MemorySignatory::new(
+                        localstore.clone(),
+                        seed,
+                        self.supported_units.clone(),
+                        HashMap::new(),
+                    )
+                    .await?,
+                )
+                    as Arc<dyn Signatory + Sync + Send + 'static>,
+                #[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<dyn Signatory + Sync + Send + 'static>,
+                #[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(),
+            signatory_manager,
             self.custom_paths.clone(),
         )
         .await?)

+ 103 - 0
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<ArcSwap<Config>>,
+}
+
+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<Config> {
+        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));
+    }
+}

+ 20 - 135
crates/cdk/src/mint/keysets.rs

@@ -1,12 +1,10 @@
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
 
 use bitcoin::bip32::DerivationPath;
+use cdk_common::mint::MintKeySetInfo;
 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 +12,26 @@ impl Mint {
     /// clients
     #[instrument(skip(self))]
     pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<KeysResponse, Error> {
-        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<KeysResponse, Error> {
-        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<KeysetResponse, Error> {
-        let keysets = self.localstore.get_keyset_infos().await?;
-        let active_keysets: HashSet<Id> = 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<Option<KeySet>, 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,31 +45,15 @@ impl Mint {
         input_fee_ppk: u64,
         custom_paths: &HashMap<CurrencyUnit, DerivationPath>,
     ) -> Result<MintKeySetInfo, 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.clone()).await?;
-        self.localstore.set_active_keyset(unit.clone(), id).await?;
-
-        let mut keysets = self.keysets.write().await;
-        keysets.insert(id, keyset);
-
-        tracing::info!("Rotated to new keyset {} for {}", id, unit);
-
-        Ok(keyset_info)
+        self.signatory
+            .rotate_keyset(
+                unit,
+                derivation_path_index,
+                max_order,
+                input_fee_ppk,
+                custom_paths.to_owned(),
+            )
+            .await
     }
 
     /// Rotate to next keyset for unit
@@ -133,17 +64,7 @@ impl Mint {
         max_order: u8,
         input_fee_ppk: u64,
     ) -> Result<MintKeySetInfo, Error> {
-        let current_keyset_id = self
-            .localstore
-            .get_active_keyset_id(&unit)
-            .await?
-            .ok_or(Error::UnsupportedUnit)?;
-
-        let keyset_info = self
-            .localstore
-            .get_keyset_info(&current_keyset_id)
-            .await?
-            .ok_or(Error::UnknownKeySet)?;
+        let keyset_info = self.signatory.get_keyset_info(unit.clone().into()).await?;
 
         tracing::debug!(
             "Current active keyset {} path index {:?}",
@@ -151,50 +72,14 @@ impl Mint {
             keyset_info.derivation_path_index
         );
 
-        let keyset_info = self
+        self.signatory
             .rotate_keyset(
                 unit,
                 keyset_info.derivation_path_index.unwrap_or(1) + 1,
                 max_order,
                 input_fee_ppk,
-                &self.custom_paths,
+                self.custom_paths.to_owned(),
             )
-            .await?;
-
-        Ok(keyset_info)
-    }
-
-    /// 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,
-        )
+            .await
     }
 }

File diff suppressed because it is too large
+ 34 - 342
crates/cdk/src/mint/mod.rs


+ 134 - 0
crates/cdk/src/mint/signatory.rs

@@ -0,0 +1,134 @@
+//! 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::mint::MintKeySetInfo;
+use cdk_common::signatory::{KeysetIdentifier, 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<Result<$output, Error>>)),
+            )*
+        }
+
+        /// Manager for handling signatory requests.
+        pub struct SignatoryManager {
+            inner: Arc<dyn Signatory + Send + Sync + 'static>,
+            pipeline: mpsc::Sender<Request>,
+            runner: JoinHandle<()>,
+        }
+
+        impl ::std::ops::Deref for SignatoryManager {
+            type Target = Arc<dyn Signatory + Send + Sync + 'static>;
+
+            fn deref(&self) -> &Self::Target {
+                return &self.inner;
+            }
+        }
+
+        #[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<dyn Signatory + Send + Sync + 'static>) -> Self {
+                let (sender, receiver) = mpsc::channel(10_000);
+                let signatory_for_inner = signatory.clone();
+                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();
+                        tokio::spawn(async move {
+                            match request {
+                                $(
+                                    Request::[<$variant:camel>]((( $([<$input:snake>]),* ), response)) => {
+                                        let output = signatory.[<$variant:lower>]($([<$input:snake>]),*).await;
+                                        if let Err(err) = response.send(output) {
+                                            tracing::error!("Error sending response: {:?}", err);
+                                        }
+                                    }
+                                )*
+                            }
+                        });
+                    }
+                });
+
+                Self {
+                    pipeline: sender,
+                    inner: signatory_for_inner,
+                    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<T: Signatory + Send + Sync + 'static> From<T> for SignatoryManager {
+            fn from(signatory: T) -> Self {
+                Self::new(Arc::new(signatory))
+            }
+        }
+
+        }
+    };
+}
+
+type Map = HashMap<CurrencyUnit, DerivationPath>;
+
+signatory_manager! {
+    blind_sign(BlindedMessage) -> BlindSignature,
+    verify_proof(Proof) -> (),
+    keyset(Id) -> Option<KeySet>,
+    keysets() -> KeysetResponse,
+    keyset_pubkeys(Id) -> KeysResponse,
+    pubkeys() -> KeysResponse,
+    rotate_keyset(CurrencyUnit, u32, u8, u64, Map) -> MintKeySetInfo,
+    get_keyset_info(KeysetIdentifier) -> MintKeySetInfo,
+}

+ 4 - 3
misc/itests.sh

@@ -46,8 +46,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 &
 
@@ -61,7 +61,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))
 
@@ -84,6 +84,7 @@ while true; do
     fi
 done
 
+export RUST_BACKTRACE=1
 
 # Run cargo test
 cargo test -p cdk-integration-tests --test regtest

Some files were not shown because too many files changed in this diff