Pārlūkot izejas kodu

Introduce a SignatoryManager service. (#509)

* WIP: 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.

* Fixed missing default feature for signatory

* Do not read keys from the DB

* Removed KeysDatabase Trait from MintDatabase

All Keys operations should be done through the signatory

* Make sure signatory has all the keys in memory

Drop also foreign constraints on sqlite

* Fix race condition

* Adding debug info to failing test

* Add `sleep` in test

* Fixed issue with active auth keyset

* Fixed dependency

* Move all keys and keysets to an ArcSwap.

Since the keys and keysets exist in RAM, most wrapping functions are infallible
and synchronous, improving performance and adding breaking API changes.

The signatory will provide this information on the boot and update when the
`rotate_keyset` is executed.

Todo: Implement a subscription key to reload the keys when the GRPC server
changes the keys. For the embedded mode, that makes no sense since there is a
single way to rotate keys, and that bit is already covered.

* Implementing https://github.com/cashubtc/nuts/pull/250

* Add CLI for cdk-signatory to spawn an external signatory

Add to the pipeline the external signatory

* Update tests

* Apply suggestions from code review

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
Co-authored-by: thesimplekid <tsk@thesimplekid.com>

* Minor change

* Update proto buf to use the newest format

* Rename binary

* Add instrumentations

* Add more comments

* Use a single database for the signatory

Store all keys, even auth keys, in a single database. Leave the MintAuthDatabse
trait implementation for the CDK but not the signagtory

This commit also moves the cli mod to its own file

* Update dep

* Add `test_mint_keyset_gen` test

---------

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
Co-authored-by: thesimplekid <tsk@thesimplekid.com>
C 4 nedēļas atpakaļ
vecāks
revīzija
ade48cd8a9
47 mainītis faili ar 2742 papildinājumiem un 1128 dzēšanām
  1. 1 0
      Cargo.toml
  2. 12 0
      crates/cashu/src/nuts/nut00/mod.rs
  3. 8 0
      crates/cashu/src/nuts/nut01/mod.rs
  4. 2 7
      crates/cdk-axum/src/auth.rs
  5. 3 13
      crates/cdk-axum/src/router_handlers.rs
  6. 0 1
      crates/cdk-cli/Cargo.toml
  7. 2 4
      crates/cdk-common/src/database/mint/mod.rs
  8. 2 2
      crates/cdk-common/src/database/mint/test.rs
  9. 8 0
      crates/cdk-common/src/error.rs
  10. 7 3
      crates/cdk-integration-tests/src/init_auth_mint.rs
  11. 42 37
      crates/cdk-integration-tests/src/init_pure_tests.rs
  12. 22 60
      crates/cdk-integration-tests/tests/integration_tests_pure.rs
  13. 18 23
      crates/cdk-integration-tests/tests/mint.rs
  14. 1 1
      crates/cdk-mint-rpc/src/proto/server.rs
  15. 7 1
      crates/cdk-mintd/Cargo.toml
  16. 12 6
      crates/cdk-mintd/src/config.rs
  17. 2 0
      crates/cdk-mintd/src/env_vars/common.rs
  18. 9 1
      crates/cdk-mintd/src/env_vars/info.rs
  19. 45 26
      crates/cdk-mintd/src/main.rs
  20. 40 0
      crates/cdk-signatory/Cargo.toml
  21. 6 0
      crates/cdk-signatory/build.rs
  22. 51 0
      crates/cdk-signatory/generate_certs.sh
  23. 173 0
      crates/cdk-signatory/proto/signatory.proto
  24. 165 0
      crates/cdk-signatory/src/bin/cli/mod.rs
  25. 15 0
      crates/cdk-signatory/src/bin/signatory.rs
  26. 156 0
      crates/cdk-signatory/src/common.rs
  27. 345 0
      crates/cdk-signatory/src/db_signatory.rs
  28. 147 0
      crates/cdk-signatory/src/embedded.rs
  29. 21 0
      crates/cdk-signatory/src/lib.rs
  30. 158 0
      crates/cdk-signatory/src/proto/client.rs
  31. 455 0
      crates/cdk-signatory/src/proto/convert.rs
  32. 6 0
      crates/cdk-signatory/src/proto/mod.rs
  33. 222 0
      crates/cdk-signatory/src/proto/server.rs
  34. 152 0
      crates/cdk-signatory/src/signatory.rs
  35. 31 0
      crates/cdk-sqlite/src/mint/migrations/20250415093121_drop_keystore_foreign.sql
  36. 7 0
      crates/cdk/Cargo.toml
  37. 79 269
      crates/cdk/src/mint/auth/mod.rs
  38. 47 21
      crates/cdk/src/mint/builder.rs
  39. 1 1
      crates/cdk/src/mint/issue/issue_nut04.rs
  40. 31 46
      crates/cdk/src/mint/keysets/auth.rs
  41. 48 268
      crates/cdk/src/mint/keysets/mod.rs
  42. 1 1
      crates/cdk/src/mint/melt.rs
  43. 149 324
      crates/cdk/src/mint/mod.rs
  44. 13 3
      crates/cdk/src/mint/swap.rs
  45. 3 5
      crates/cdk/src/mint/verification.rs
  46. 3 1
      justfile
  47. 14 4
      misc/fake_itests.sh

+ 1 - 0
Cargo.toml

@@ -55,6 +55,7 @@ cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-featu
 cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.9.2" }
 cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.9.2" }
 cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.9.2" }
+cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.9.2", default-features = false }
 clap = { version = "4.5.31", features = ["derive"] }
 ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
 cbor-diag = "0.1.12"

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

@@ -283,6 +283,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

@@ -91,6 +91,14 @@ impl<'de> Deserialize<'de> for Keys {
     }
 }
 
+impl Deref for Keys {
+    type Target = BTreeMap<Amount, PublicKey>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
 impl From<MintKeys> for Keys {
     fn from(keys: MintKeys) -> Self {
         Self(

+ 2 - 7
crates/cdk-axum/src/auth.rs

@@ -103,12 +103,7 @@ where
 pub async fn get_auth_keysets(
     State(state): State<MintState>,
 ) -> Result<Json<KeysetResponse>, Response> {
-    let keysets = state.mint.auth_keysets().await.map_err(|err| {
-        tracing::error!("Could not get keysets: {}", err);
-        into_response(err)
-    })?;
-
-    Ok(Json(keysets))
+    Ok(Json(state.mint.auth_keysets()))
 }
 
 #[cfg_attr(feature = "swagger", utoipa::path(
@@ -125,7 +120,7 @@ pub async fn get_auth_keysets(
 pub async fn get_blind_auth_keys(
     State(state): State<MintState>,
 ) -> Result<Json<KeysResponse>, Response> {
-    let pubkeys = state.mint.auth_pubkeys().await.map_err(|err| {
+    let pubkeys = state.mint.auth_pubkeys().map_err(|err| {
         tracing::error!("Could not get keys: {}", err);
         into_response(err)
     })?;

+ 3 - 13
crates/cdk-axum/src/router_handlers.rs

@@ -82,12 +82,7 @@ post_cache_wrapper!(
 pub(crate) async fn get_keys(
     State(state): State<MintState>,
 ) -> Result<Json<KeysResponse>, Response> {
-    let pubkeys = state.mint.pubkeys().await.map_err(|err| {
-        tracing::error!("Could not get keys: {}", err);
-        into_response(err)
-    })?;
-
-    Ok(Json(pubkeys))
+    Ok(Json(state.mint.pubkeys()))
 }
 
 #[cfg_attr(feature = "swagger", utoipa::path(
@@ -110,7 +105,7 @@ pub(crate) async fn get_keyset_pubkeys(
     State(state): State<MintState>,
     Path(keyset_id): Path<Id>,
 ) -> Result<Json<KeysResponse>, Response> {
-    let pubkeys = state.mint.keyset_pubkeys(&keyset_id).await.map_err(|err| {
+    let pubkeys = state.mint.keyset_pubkeys(&keyset_id).map_err(|err| {
         tracing::error!("Could not get keyset pubkeys: {}", err);
         into_response(err)
     })?;
@@ -134,12 +129,7 @@ pub(crate) async fn get_keyset_pubkeys(
 pub(crate) async fn get_keysets(
     State(state): State<MintState>,
 ) -> Result<Json<KeysetResponse>, Response> {
-    let keysets = state.mint.keysets().await.map_err(|err| {
-        tracing::error!("Could not get keysets: {}", err);
-        into_response(err)
-    })?;
-
-    Ok(Json(keysets))
+    Ok(Json(state.mint.keysets()))
 }
 
 #[cfg_attr(feature = "swagger", utoipa::path(

+ 0 - 1
crates/cdk-cli/Cargo.toml

@@ -37,4 +37,3 @@ nostr-sdk = { version = "0.41.0", default-features = false, features = [
 reqwest.workspace = true
 url.workspace = true
 serde_with.workspace = true
-

+ 2 - 4
crates/cdk-common/src/database/mint/mod.rs

@@ -42,6 +42,7 @@ pub trait KeysDatabase {
     /// Get [`MintKeySetInfo`]s
     async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err>;
 }
+
 /// Mint Quote Database trait
 #[async_trait]
 pub trait QuotesDatabase {
@@ -172,10 +173,7 @@ pub trait SignaturesDatabase {
 /// Mint Database trait
 #[async_trait]
 pub trait Database<Error>:
-    KeysDatabase<Err = Error>
-    + QuotesDatabase<Err = Error>
-    + ProofsDatabase<Err = Error>
-    + SignaturesDatabase<Err = Error>
+    QuotesDatabase<Err = Error> + ProofsDatabase<Err = Error> + SignaturesDatabase<Err = Error>
 {
     /// Set [`MintInfo`]
     async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error>;

+ 2 - 2
crates/cdk-common/src/database/mint/test.rs

@@ -12,7 +12,7 @@ use super::*;
 use crate::mint::MintKeySetInfo;
 
 #[inline]
-async fn setup_keyset<E: Debug, DB: Database<E>>(db: &DB) -> Id {
+async fn setup_keyset<E: Debug, DB: Database<E> + KeysDatabase<Err = E>>(db: &DB) -> Id {
     let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
     let keyset_info = MintKeySetInfo {
         id: keyset_id,
@@ -30,7 +30,7 @@ async fn setup_keyset<E: Debug, DB: Database<E>>(db: &DB) -> Id {
 }
 
 /// State transition test
-pub async fn state_transition<E: Debug, DB: Database<E>>(db: DB) {
+pub async fn state_transition<E: Debug, DB: Database<E> + KeysDatabase<Err = E>>(db: DB) {
     let keyset_id = setup_keyset(&db).await;
 
     let proofs = vec![

+ 8 - 0
crates/cdk-common/src/error.rs

@@ -92,6 +92,14 @@ pub enum Error {
     #[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
     AmountlessInvoiceNotSupported(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")]

+ 7 - 3
crates/cdk-integration-tests/src/init_auth_mint.rs

@@ -4,23 +4,25 @@ use std::sync::Arc;
 use anyhow::Result;
 use bip39::Mnemonic;
 use cashu::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
-use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase};
+use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase, MintKeysDatabase};
 use cdk::mint::{MintBuilder, MintMeltLimits};
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
 use cdk::types::FeeReserve;
 use cdk::wallet::AuthWallet;
 use cdk_fake_wallet::FakeWallet;
 
-pub async fn start_fake_mint_with_auth<D, A>(
+pub async fn start_fake_mint_with_auth<D, A, K>(
     _addr: &str,
     _port: u16,
     openid_discovery: String,
     database: D,
     auth_database: A,
+    key_store: K,
 ) -> Result<()>
 where
     D: MintDatabase<cdk_database::Error> + Send + Sync + 'static,
     A: MintAuthDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
+    K: MintKeysDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
 {
     let fee_reserve = FeeReserve {
         min_fee_reserve: 1.into(),
@@ -31,7 +33,9 @@ where
 
     let mut mint_builder = MintBuilder::new();
 
-    mint_builder = mint_builder.with_localstore(Arc::new(database));
+    mint_builder = mint_builder
+        .with_localstore(Arc::new(database))
+        .with_keystore(Arc::new(key_store));
 
     mint_builder = mint_builder
         .add_ln_backend(

+ 42 - 37
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -9,7 +9,7 @@ use anyhow::{anyhow, bail, Result};
 use async_trait::async_trait;
 use bip39::Mnemonic;
 use cdk::amount::SplitTarget;
-use cdk::cdk_database::{self, MintDatabase, WalletDatabase};
+use cdk::cdk_database::{self, WalletDatabase};
 use cdk::mint::{MintBuilder, MintMeltLimits};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
@@ -56,18 +56,15 @@ impl Debug for DirectMintConnection {
 #[async_trait]
 impl MintConnector for DirectMintConnection {
     async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
-        self.mint.pubkeys().await.map(|pks| pks.keysets)
+        Ok(self.mint.pubkeys().keysets)
     }
 
     async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error> {
-        self.mint
-            .keyset(&keyset_id)
-            .await
-            .and_then(|res| res.ok_or(Error::UnknownKeySet))
+        self.mint.keyset(&keyset_id).ok_or(Error::UnknownKeySet)
     }
 
     async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
-        self.mint.keysets().await
+        Ok(self.mint.keysets())
     }
 
     async fn post_mint_quote(
@@ -173,40 +170,42 @@ pub fn setup_tracing() {
 }
 
 pub async fn create_and_start_test_mint() -> Result<Mint> {
-    let mut mint_builder = MintBuilder::new();
-
     // Read environment variable to determine database type
     let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set");
 
-    let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
-        match db_type.to_lowercase().as_str() {
-            "sqlite" => {
-                // Create a temporary directory for SQLite database
-                let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?;
-                let path = temp_dir.join("mint.db").to_str().unwrap().to_string();
-                let database = cdk_sqlite::MintSqliteDatabase::new(&path)
+    let mut mint_builder = match db_type.to_lowercase().as_str() {
+        "sqlite" => {
+            // Create a temporary directory for SQLite database
+            let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?;
+            let path = temp_dir.join("mint.db").to_str().unwrap().to_string();
+            let database = Arc::new(
+                cdk_sqlite::MintSqliteDatabase::new(&path)
                     .await
-                    .expect("Could not create sqlite db");
-                Arc::new(database)
-            }
-            "redb" => {
-                // Create a temporary directory for ReDB database
-                let temp_dir = create_temp_dir("cdk-test-redb-mint")?;
-                let path = temp_dir.join("mint.redb");
-                let database = cdk_redb::MintRedbDatabase::new(&path)
-                    .expect("Could not create redb mint database");
-                Arc::new(database)
-            }
-            "memory" => {
-                let database = cdk_sqlite::mint::memory::empty().await?;
-                Arc::new(database)
-            }
-            _ => {
-                bail!("Db type not set")
-            }
-        };
-
-    mint_builder = mint_builder.with_localstore(localstore.clone());
+                    .expect("Could not create sqlite db"),
+            );
+            MintBuilder::new()
+                .with_localstore(database.clone())
+                .with_keystore(database)
+        }
+        "redb" => {
+            // Create a temporary directory for ReDB database
+            let temp_dir = create_temp_dir("cdk-test-redb-mint")?;
+            let path = temp_dir.join("mint.redb");
+            let database = Arc::new(
+                cdk_redb::MintRedbDatabase::new(&path)
+                    .expect("Could not create redb mint database"),
+            );
+            MintBuilder::new()
+                .with_localstore(database.clone())
+                .with_keystore(database)
+        }
+        "memory" => MintBuilder::new()
+            .with_localstore(Arc::new(cdk_sqlite::mint::memory::empty().await?))
+            .with_keystore(Arc::new(cdk_sqlite::mint::memory::empty().await?)),
+        _ => {
+            bail!("Db type not set")
+        }
+    };
 
     let fee_reserve = FeeReserve {
         min_fee_reserve: 1.into(),
@@ -237,6 +236,12 @@ pub async fn create_and_start_test_mint() -> Result<Mint> {
         .with_urls(vec!["https://aaa".to_string()])
         .with_seed(mnemonic.to_seed_normalized("").to_vec());
 
+    let localstore = mint_builder
+        .localstore
+        .as_ref()
+        .map(|x| x.clone())
+        .expect("localstore");
+
     localstore
         .set_mint_info(mint_builder.mint_info.clone())
         .await?;

+ 22 - 60
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -8,6 +8,7 @@ use std::assert_eq;
 use std::collections::{HashMap, HashSet};
 use std::hash::RandomState;
 use std::str::FromStr;
+use std::time::Duration;
 
 use cashu::amount::SplitTarget;
 use cashu::dhke::construct_proofs;
@@ -24,6 +25,7 @@ use cdk::wallet::{ReceiveOptions, SendMemo, SendOptions};
 use cdk::Amount;
 use cdk_fake_wallet::create_fake_invoice;
 use cdk_integration_tests::init_pure_tests::*;
+use tokio::time::sleep;
 
 /// Tests the token swap and send functionality:
 /// 1. Alice gets funded with 64 sats
@@ -235,15 +237,7 @@ async fn test_mint_double_spend() {
         .await
         .expect("Could not get proofs");
 
-    let keys = mint_bob
-        .pubkeys()
-        .await
-        .unwrap()
-        .keysets
-        .first()
-        .unwrap()
-        .clone()
-        .keys;
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
     let keyset_id = Id::from(&keys);
 
     let preswap = PreMintSecrets::random(
@@ -300,15 +294,7 @@ async fn test_attempt_to_swap_by_overflowing() {
 
     let amount = 2_u64.pow(63);
 
-    let keys = mint_bob
-        .pubkeys()
-        .await
-        .unwrap()
-        .keysets
-        .first()
-        .unwrap()
-        .clone()
-        .keys;
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
     let keyset_id = Id::from(&keys);
 
     let pre_mint_amount =
@@ -429,15 +415,7 @@ pub async fn test_p2pk_swap() {
 
     let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
 
-    let keys = mint_bob
-        .pubkeys()
-        .await
-        .unwrap()
-        .keysets
-        .first()
-        .cloned()
-        .unwrap()
-        .keys;
+    let keys = mint_bob.pubkeys().keysets.first().cloned().unwrap().keys;
 
     let post_swap = mint_bob.process_swap_request(swap_request).await.unwrap();
 
@@ -496,6 +474,8 @@ pub async fn test_p2pk_swap() {
 
     assert!(attempt_swap.is_ok());
 
+    sleep(Duration::from_secs(1)).await;
+
     let mut msgs = HashMap::new();
     while let Ok((sub_id, msg)) = listener.try_recv() {
         assert_eq!(sub_id, "test".into());
@@ -509,10 +489,16 @@ pub async fn test_p2pk_swap() {
         }
     }
 
-    for keys in public_keys_to_listen {
-        let statuses = msgs.remove(&keys).expect("some events");
+    for (i, key) in public_keys_to_listen.into_iter().enumerate() {
+        let statuses = msgs.remove(&key).expect("some events");
         // Every input pk receives two state updates, as there are only two state transitions
-        assert_eq!(statuses, vec![State::Pending, State::Spent]);
+        assert_eq!(
+            statuses,
+            vec![State::Pending, State::Spent],
+            "failed to test key {:?} (pos {})",
+            key,
+            i,
+        );
     }
 
     assert!(listener.try_recv().is_err(), "no other event is happening");
@@ -527,7 +513,7 @@ async fn test_swap_overpay_underpay_fee() {
         .expect("Failed to create test mint");
 
     mint_bob
-        .rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new())
+        .rotate_keyset(CurrencyUnit::Sat, 32, 1)
         .await
         .unwrap();
 
@@ -545,15 +531,7 @@ async fn test_swap_overpay_underpay_fee() {
         .await
         .expect("Could not get proofs");
 
-    let keys = mint_bob
-        .pubkeys()
-        .await
-        .unwrap()
-        .keysets
-        .first()
-        .unwrap()
-        .clone()
-        .keys;
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
     let keyset_id = Id::from(&keys);
 
     let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap();
@@ -597,7 +575,7 @@ async fn test_mint_enforce_fee() {
         .expect("Failed to create test mint");
 
     mint_bob
-        .rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new())
+        .rotate_keyset(CurrencyUnit::Sat, 32, 1)
         .await
         .unwrap();
 
@@ -619,15 +597,7 @@ async fn test_mint_enforce_fee() {
         .await
         .expect("Could not get proofs");
 
-    let keys = mint_bob
-        .pubkeys()
-        .await
-        .unwrap()
-        .keysets
-        .first()
-        .unwrap()
-        .clone()
-        .keys;
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
     let keyset_id = Id::from(&keys);
 
     let five_proofs: Vec<_> = proofs.drain(..5).collect();
@@ -689,7 +659,7 @@ async fn test_mint_change_with_fee_melt() {
         .expect("Failed to create test mint");
 
     mint_bob
-        .rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new())
+        .rotate_keyset(CurrencyUnit::Sat, 32, 1)
         .await
         .unwrap();
 
@@ -914,14 +884,6 @@ async fn test_concurrent_double_spend_melt() {
 }
 
 async fn get_keyset_id(mint: &Mint) -> Id {
-    let keys = mint
-        .pubkeys()
-        .await
-        .unwrap()
-        .keysets
-        .first()
-        .unwrap()
-        .clone()
-        .keys;
+    let keys = mint.pubkeys().keysets.first().unwrap().clone().keys;
     Id::from(&keys)
 }

+ 18 - 23
crates/cdk-integration-tests/tests/mint.rs

@@ -31,7 +31,9 @@ async fn test_correct_keyset() {
 
     let mut mint_builder = MintBuilder::new();
     let localstore = Arc::new(database);
-    mint_builder = mint_builder.with_localstore(localstore.clone());
+    mint_builder = mint_builder
+        .with_localstore(localstore.clone())
+        .with_keystore(localstore.clone());
 
     mint_builder = mint_builder
         .add_ln_backend(
@@ -57,42 +59,35 @@ async fn test_correct_keyset() {
     let quote_ttl = QuoteTTL::new(10000, 10000);
     localstore.set_quote_ttl(quote_ttl).await.unwrap();
 
-    mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0)
-        .await
-        .unwrap();
-    mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0)
-        .await
-        .unwrap();
+    let active = mint.get_active_keysets();
+
+    let active = active
+        .get(&CurrencyUnit::Sat)
+        .expect("There is a keyset for unit");
+    let old_keyset_info = mint.get_keyset_info(active).expect("There is keyset");
 
-    let active = mint.localstore.get_active_keysets().await.unwrap();
+    mint.rotate_keyset(CurrencyUnit::Sat, 32, 0).await.unwrap();
+
+    let active = mint.get_active_keysets();
 
     let active = active
         .get(&CurrencyUnit::Sat)
         .expect("There is a keyset for unit");
 
-    let keyset_info = mint
-        .localstore
-        .get_keyset_info(active)
-        .await
-        .unwrap()
-        .expect("There is keyset");
+    let keyset_info = mint.get_keyset_info(active).expect("There is keyset");
 
-    assert!(keyset_info.derivation_path_index == Some(2));
+    assert_ne!(keyset_info.id, old_keyset_info.id);
 
+    mint.rotate_keyset(CurrencyUnit::Sat, 32, 0).await.unwrap();
     let mint = mint_builder.build().await.unwrap();
 
-    let active = mint.localstore.get_active_keysets().await.unwrap();
+    let active = mint.get_active_keysets();
 
     let active = active
         .get(&CurrencyUnit::Sat)
         .expect("There is a keyset for unit");
 
-    let keyset_info = mint
-        .localstore
-        .get_keyset_info(active)
-        .await
-        .unwrap()
-        .expect("There is keyset");
+    let new_keyset_info = mint.get_keyset_info(active).expect("There is keyset");
 
-    assert!(keyset_info.derivation_path_index == Some(2));
+    assert_ne!(new_keyset_info.id, keyset_info.id);
 }

+ 1 - 1
crates/cdk-mint-rpc/src/proto/server.rs

@@ -688,7 +688,7 @@ impl CdkMint for MintRPCServer {
 
         let keyset_info = self
             .mint
-            .rotate_next_keyset(
+            .rotate_keyset(
                 unit,
                 request.max_order.map(|a| a as u8).unwrap_or(32),
                 request.input_fee_ppk.unwrap_or(0),

+ 7 - 1
crates/cdk-mintd/Cargo.toml

@@ -18,7 +18,7 @@ cln = ["dep:cdk-cln"]
 lnd = ["dep:cdk-lnd"]
 lnbits = ["dep:cdk-lnbits"]
 fakewallet = ["dep:cdk-fake-wallet"]
-grpc-processor = ["dep:cdk-payment-processor"]
+grpc-processor = ["dep:cdk-payment-processor", "cdk-signatory/grpc"]
 sqlcipher = ["cdk-sqlite/sqlcipher"]
 # MSRV is not committed to with redb enabled
 redb = ["dep:cdk-redb"]
@@ -45,6 +45,7 @@ cdk-lnbits = { workspace = true, optional = true }
 cdk-lnd = { workspace = true, optional = true }
 cdk-fake-wallet = { workspace = true, optional = true }
 cdk-axum.workspace = true
+cdk-signatory.workspace = true
 cdk-mint-rpc = { workspace = true, optional = true }
 cdk-payment-processor = { workspace = true, optional = true }
 config = { version = "0.15.11", features = ["toml"] }
@@ -63,3 +64,8 @@ home.workspace = true
 url.workspace = true
 utoipa = { workspace = true, optional = true }
 utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true }
+
+[build-dependencies]
+# Dep of utopia 2.5.0 breaks so keeping here for now
+zip = "=2.4.2"
+time = "=0.3.39"

+ 12 - 6
crates/cdk-mintd/src/config.rs

@@ -12,7 +12,9 @@ pub struct Info {
     pub url: String,
     pub listen_host: String,
     pub listen_port: u16,
-    pub mnemonic: String,
+    pub mnemonic: Option<String>,
+    pub signatory_url: Option<String>,
+    pub signatory_certs: Option<String>,
     pub input_fee_ppk: Option<u64>,
 
     pub http_cache: cache::Config,
@@ -28,8 +30,12 @@ impl std::fmt::Debug for Info {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         // Use a fallback approach that won't panic
         let mnemonic_display = {
-            let hash = sha256::Hash::hash(self.mnemonic.clone().into_bytes().as_ref());
-            format!("<hashed: {hash}>")
+            if let Some(mnemonic) = self.mnemonic.as_ref() {
+                let hash = sha256::Hash::hash(mnemonic.as_bytes());
+                format!("<hashed: {hash}>")
+            } else {
+                format!("<url: {}>", self.signatory_url.clone().unwrap_or_default())
+            }
         };
 
         f.debug_struct("Info")
@@ -377,7 +383,7 @@ mod tests {
             url: "http://example.com".to_string(),
             listen_host: "127.0.0.1".to_string(),
             listen_port: 8080,
-            mnemonic: "test secret mnemonic phrase".to_string(),
+            mnemonic: Some("test secret mnemonic phrase".to_string()),
             input_fee_ppk: Some(100),
             ..Default::default()
         };
@@ -404,7 +410,7 @@ mod tests {
             url: "http://example.com".to_string(),
             listen_host: "127.0.0.1".to_string(),
             listen_port: 8080,
-            mnemonic: "".to_string(), // Empty mnemonic
+            mnemonic: Some("".to_string()), // Empty mnemonic
             enable_swagger_ui: Some(false),
             ..Default::default()
         };
@@ -423,7 +429,7 @@ mod tests {
             url: "http://example.com".to_string(),
             listen_host: "127.0.0.1".to_string(),
             listen_port: 8080,
-            mnemonic: "特殊字符 !@#$%^&*()".to_string(), // Special characters
+            mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), // Special characters
             ..Default::default()
         };
 

+ 2 - 0
crates/cdk-mintd/src/env_vars/common.rs

@@ -6,6 +6,8 @@ pub const ENV_URL: &str = "CDK_MINTD_URL";
 pub const ENV_LISTEN_HOST: &str = "CDK_MINTD_LISTEN_HOST";
 pub const ENV_LISTEN_PORT: &str = "CDK_MINTD_LISTEN_PORT";
 pub const ENV_MNEMONIC: &str = "CDK_MINTD_MNEMONIC";
+pub const ENV_SIGNATORY_URL: &str = "CDK_MINTD_SIGNATORY_URL";
+pub const ENV_SIGNATORY_CERTS: &str = "CDK_MINTD_SIGNATORY_CERTS";
 pub const ENV_SECONDS_QUOTE_VALID: &str = "CDK_MINTD_SECONDS_QUOTE_VALID";
 pub const ENV_CACHE_SECONDS: &str = "CDK_MINTD_CACHE_SECONDS";
 pub const ENV_EXTEND_CACHE_SECONDS: &str = "CDK_MINTD_EXTEND_CACHE_SECONDS";

+ 9 - 1
crates/cdk-mintd/src/env_vars/info.rs

@@ -22,8 +22,16 @@ impl Info {
             }
         }
 
+        if let Ok(signatory_url) = env::var(ENV_SIGNATORY_URL) {
+            self.signatory_url = Some(signatory_url);
+        }
+
+        if let Ok(signatory_certs) = env::var(ENV_SIGNATORY_CERTS) {
+            self.signatory_certs = Some(signatory_certs);
+        }
+
         if let Ok(mnemonic) = env::var(ENV_MNEMONIC) {
-            self.mnemonic = mnemonic;
+            self.mnemonic = Some(mnemonic);
         }
 
         if let Ok(cache_seconds_str) = env::var(ENV_CACHE_SECONDS) {

+ 45 - 26
crates/cdk-mintd/src/main.rs

@@ -12,7 +12,7 @@ use std::sync::Arc;
 use anyhow::{anyhow, bail, Result};
 use axum::Router;
 use bip39::Mnemonic;
-use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase};
+use cdk::cdk_database::{self, MintAuthDatabase};
 use cdk::mint::{MintBuilder, MintMeltLimits};
 // Feature-gated imports
 #[cfg(any(
@@ -107,8 +107,6 @@ async fn main() -> anyhow::Result<()> {
         None => work_dir.join("config.toml"),
     };
 
-    let mut mint_builder = MintBuilder::new();
-
     let mut settings = if config_file_arg.exists() {
         config::Settings::new(Some(config_file_arg))
     } else {
@@ -120,25 +118,28 @@ async fn main() -> anyhow::Result<()> {
     // ENV VARS will take **priority** over those in the config
     let settings = settings.from_env()?;
 
-    let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
-        match settings.database.engine {
-            DatabaseEngine::Sqlite => {
-                let sql_db_path = work_dir.join("cdk-mintd.sqlite");
-                #[cfg(not(feature = "sqlcipher"))]
-                let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?;
-                #[cfg(feature = "sqlcipher")]
-                let sqlite_db = MintSqliteDatabase::new(&sql_db_path, args.password).await?;
-
-                Arc::new(sqlite_db)
-            }
-            #[cfg(feature = "redb")]
-            DatabaseEngine::Redb => {
-                let redb_path = work_dir.join("cdk-mintd.redb");
-                Arc::new(MintRedbDatabase::new(&redb_path)?)
-            }
-        };
-
-    mint_builder = mint_builder.with_localstore(localstore);
+    let mut mint_builder = match settings.database.engine {
+        DatabaseEngine::Sqlite => {
+            let sql_db_path = work_dir.join("cdk-mintd.sqlite");
+            #[cfg(not(feature = "sqlcipher"))]
+            let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?;
+            #[cfg(feature = "sqlcipher")]
+            let sqlite_db = MintSqliteDatabase::new(&sql_db_path, args.password).await?;
+
+            let db = Arc::new(sqlite_db);
+            MintBuilder::new()
+                .with_localstore(db.clone())
+                .with_keystore(db)
+        }
+        #[cfg(feature = "redb")]
+        DatabaseEngine::Redb => {
+            let redb_path = work_dir.join("cdk-mintd.redb");
+            let db = Arc::new(MintRedbDatabase::new(&redb_path)?);
+            MintBuilder::new()
+                .with_localstore(db.clone())
+                .with_keystore(db)
+        }
+    };
 
     let mut contact_info: Option<Vec<ContactInfo>> = None;
 
@@ -361,13 +362,31 @@ async fn main() -> anyhow::Result<()> {
         mint_builder = mint_builder.with_tos_url(tos_url.to_string());
     }
 
-    let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
-
     mint_builder = mint_builder
         .with_name(settings.mint_info.name)
         .with_version(mint_version)
-        .with_description(settings.mint_info.description)
-        .with_seed(mnemonic.to_seed_normalized("").to_vec());
+        .with_description(settings.mint_info.description);
+
+    mint_builder = if let Some(signatory_url) = settings.info.signatory_url {
+        tracing::info!(
+            "Connecting to remote signatory to {} with certs {:?}",
+            signatory_url,
+            settings.info.signatory_certs
+        );
+        mint_builder.with_signatory(Arc::new(
+            cdk_signatory::SignatoryRpcClient::new(signatory_url, settings.info.signatory_certs)
+                .await?,
+        ))
+    } else if let Some(mnemonic) = settings
+        .info
+        .mnemonic
+        .map(|s| Mnemonic::from_str(&s))
+        .transpose()?
+    {
+        mint_builder.with_seed(mnemonic.to_seed_normalized("").to_vec())
+    } else {
+        bail!("No seed nor remote signatory set");
+    };
 
     let cached_endpoints = vec![
         CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11),

+ 40 - 0
crates/cdk-signatory/Cargo.toml

@@ -0,0 +1,40 @@
+[package]
+name = "cdk-signatory"
+version = "0.9.2"
+edition = "2021"
+description = "CDK signatory default implementation"
+
+[features]
+default = ["grpc"]
+sqlcipher = ["cdk-sqlite/sqlcipher"]
+redb = ["dep:cdk-redb"]
+grpc = ["dep:tonic", "tokio/full", "dep:prost", "dep:tonic-build"]
+
+[dependencies]
+async-trait.workspace = true
+bitcoin.workspace = true
+cdk-common = { workspace = true, default-features=false, features = [
+    "mint", "auth",
+] }
+tonic = { workspace = true, optional = true }
+prost = { workspace = true, optional = true }
+tracing.workspace = true
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+# main.rs dependencies
+anyhow.workspace = true
+cdk-sqlite = { workspace = true, features = ["mint", "auth"] }
+cdk-redb = { workspace = true, features = ["mint", "auth"], optional = true }
+clap = { workspace = true }
+bip39.workspace = true
+home.workspace = true
+thiserror.workspace = true
+tracing-subscriber.workspace = true
+tokio = { workspace = true, features = ["full"] }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
+getrandom = { version = "0.2", features = ["js"] }
+
+[build-dependencies]
+tonic-build = { workspace = true, features = ["prost"], optional = true }

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

@@ -0,0 +1,6 @@
+fn main() {
+    println!("cargo:rerun-if-changed=src/proto/signatory.proto");
+
+    #[cfg(feature = "grpc")]
+    tonic_build::compile_protos("proto/signatory.proto").unwrap();
+}

+ 51 - 0
crates/cdk-signatory/generate_certs.sh

@@ -0,0 +1,51 @@
+if [ $# -eq 1 ]; then
+  cd "$1" || { echo "Failed to cd into '$1'"; exit 1; }
+fi
+
+# Generate private key for Certificate Authority (CA)
+openssl genrsa -out ca.key 4096
+
+# Generate CA certificate
+openssl req -new -x509 -days 365 -key ca.key -out ca.pem -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=MyCA"
+
+# Generate private key for Server
+openssl genrsa -out server.key 4096
+
+# Generate Certificate Signing Request (CSR) for Server
+openssl req -new -key server.key -out server.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=localhost"
+
+# Generate Server certificate
+openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -extfile <(printf "subjectAltName=DNS:localhost,DNS:my-server,IP:127.0.0.1")
+
+# Generate private key for Client
+openssl genrsa -out client.key 4096
+
+# Generate CSR for Client
+openssl req -new -key client.key -out client.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=client"
+
+# Generate Client certificate
+openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem
+
+# Verify the certificates
+echo "Verifying Server Certificate:"
+openssl verify -CAfile ca.pem server.pem
+
+echo "Verifying Client Certificate:"
+openssl verify -CAfile ca.pem client.pem
+
+# Clean up CSR files (optional)
+rm server.csr client.csr
+
+# Display certificate information
+echo "Server Certificate Info:"
+openssl x509 -in server.pem -text -noout | grep "Subject:\|Issuer:\|DNS:\|IP Address:"
+
+echo "Client Certificate Info:"
+openssl x509 -in client.pem -text -noout | grep "Subject:\|Issuer:"
+
+# Final files you'll need:
+# - ca.pem (Certificate Authority certificate)
+# - server.key (Server private key)
+# - server.pem (Server certificate)
+# - client.key (Client private key)
+# - client.pem (Client certificate)

+ 173 - 0
crates/cdk-signatory/proto/signatory.proto

@@ -0,0 +1,173 @@
+syntax = "proto3";
+
+package signatory;
+
+service Signatory {
+  rpc BlindSign(BlindedMessages) returns (BlindSignResponse);
+  rpc VerifyProofs(Proofs) returns (BooleanResponse);
+  // returns all the keysets for the mint
+  rpc Keysets(EmptyRequest) returns (KeysResponse);
+  // rotates the keysets
+  rpc RotateKeyset(RotationRequest) returns (KeyRotationResponse);
+}
+
+enum Operation {
+  OPERATION_UNSPECIFIED = 0;
+  OPERATION_MINT = 1;
+  OPERATION_MELT = 2;
+  OPERATION_SWAP = 3;
+}
+
+message BlindSignResponse {
+  Error error = 1;
+  BlindSignatures sigs = 2;
+}
+
+message BlindedMessages {
+  repeated BlindedMessage blinded_messages = 1;
+  Operation operation = 2;
+  string correlation_id = 3;
+}
+
+// Represents a blinded message
+message BlindedMessage {
+  uint64 amount = 1;
+  string keyset_id = 2;
+  bytes blinded_secret = 3;
+}
+
+message BooleanResponse {
+  Error error = 1;
+  bool success = 2;
+}
+
+message KeyRotationResponse {
+  Error error = 1;
+  KeySet keyset = 2;
+}
+
+message KeysResponse {
+  Error error = 1;
+  SignatoryKeysets keysets = 2;
+}
+
+message SignatoryKeysets {
+  bytes pubkey = 1;
+  repeated KeySet keysets = 2;
+}
+
+message KeySet {
+  string id = 1;
+  CurrencyUnit unit = 2;
+  bool active = 3;
+  uint64 input_fee_ppk = 4;
+  Keys keys = 5;
+}
+
+message Keys {
+  map<uint64, bytes> keys = 1;
+}
+
+message RotationRequest {
+  CurrencyUnit unit = 1;
+  uint64 input_fee_ppk = 2;
+  uint32 max_order = 3;
+}
+
+enum CurrencyUnitType {
+  CURRENCY_UNIT_TYPE_UNSPECIFIED = 0;
+  CURRENCY_UNIT_TYPE_SAT = 1;
+  CURRENCY_UNIT_TYPE_MSAT = 2;
+  CURRENCY_UNIT_TYPE_USD = 3;
+  CURRENCY_UNIT_TYPE_EUR = 4;
+  CURRENCY_UNIT_TYPE_AUTH = 5;
+}
+
+message CurrencyUnit {
+  oneof currency_unit {
+    CurrencyUnitType unit = 1;
+    string custom_unit = 2;
+  }
+}
+
+message Proofs {
+  repeated Proof proof = 1;
+  Operation operation = 3;
+  string correlation_id = 4;
+}
+
+message Proof {
+  uint64 amount = 1;
+  string keyset_id = 2;
+  bytes secret = 3;
+  bytes c = 4;
+}
+
+message ProofDLEQ {
+  bytes e = 1;
+  bytes s = 2;
+  bytes r = 3;
+}
+
+message SigningResponse {
+  Error error = 1;
+  BlindSignatures blind_signatures = 2;
+}
+message BlindSignatures {
+  repeated BlindSignature blind_signatures = 1;
+}
+
+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;
+}
+
+// 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;
+}
+
+enum ErrorCode {
+  ERROR_CODE_UNSPECIFIED = 0;
+  ERROR_CODE_AMOUNT_OUTSIDE_LIMIT = 1;
+  ERROR_CODE_DUPLICATE_INPUTS_PROVIDED = 2;
+  ERROR_CODE_DUPLICATE_OUTPUTS_PROVIDED = 3;
+  ERROR_CODE_KEYSET_NOT_KNOWN = 4;
+  ERROR_CODE_KEYSET_INACTIVE = 5;
+  ERROR_CODE_MINTING_DISABLED = 6;
+  ERROR_CODE_COULD_NOT_ROTATE_KEYSET = 7;
+  ERROR_CODE_INVALID_PROOF = 8;
+  ERROR_CODE_INVALID_BLIND_MESSAGE = 9;
+  ERROR_CODE_UNIT_NOT_SUPPORTED = 10;
+}
+
+message Error {
+  ErrorCode code = 1;
+  string detail = 2;
+}
+
+message EmptyRequest {}

+ 165 - 0
crates/cdk-signatory/src/bin/cli/mod.rs

@@ -0,0 +1,165 @@
+//! Signatory CLI main logic
+//!
+//! This logic is in this file to be excluded for wasm
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::sync::Arc;
+use std::{env, fs};
+
+use anyhow::{bail, Result};
+use bip39::rand::{thread_rng, Rng};
+use bip39::Mnemonic;
+use cdk_common::database::MintKeysDatabase;
+use cdk_common::CurrencyUnit;
+#[cfg(feature = "redb")]
+use cdk_redb::MintRedbDatabase;
+use cdk_signatory::{db_signatory, grpc_server};
+use cdk_sqlite::MintSqliteDatabase;
+use clap::Parser;
+use tracing::Level;
+use tracing_subscriber::EnvFilter;
+
+const DEFAULT_WORK_DIR: &str = ".cdk-signatory";
+const ENV_MNEMONIC: &str = "CDK_MINTD_MNEMONIC";
+
+/// Simple CLI application to interact with cashu
+#[derive(Parser)]
+#[command(name = "cashu-signatory")]
+#[command(author = "thesimplekid <tsk@thesimplekid.com>")]
+#[command(version = "0.1.0")]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+    /// Database engine to use (sqlite/redb)
+    #[arg(short, long, default_value = "sqlite")]
+    engine: String,
+    /// Database password for sqlcipher
+    #[arg(long)]
+    password: Option<String>,
+    /// Path to working dir
+    #[arg(short, long)]
+    work_dir: Option<PathBuf>,
+    /// Logging level
+    #[arg(short, long, default_value = "error")]
+    log_level: Level,
+    #[arg(long, default_value = "127.0.0.1")]
+    listen_addr: String,
+    #[arg(long, default_value = "15060")]
+    listen_port: u32,
+    #[arg(long, short)]
+    certs: Option<String>,
+    /// Supported units with the format of name,fee and max_order
+    #[arg(long, short, default_value = "sat,0,32")]
+    units: Vec<String>,
+}
+
+/// Main function for the signatory standalone binary
+pub async fn cli_main() -> Result<()> {
+    let args: Cli = Cli::parse();
+    let default_filter = args.log_level;
+    let supported_units = args
+        .units
+        .into_iter()
+        .map(|unit| {
+            let mut parts = unit.split(",").collect::<Vec<_>>();
+            parts.reverse();
+            let unit: CurrencyUnit = parts.pop().unwrap_or_default().parse()?;
+            let fee = parts
+                .pop()
+                .map(|x| x.parse())
+                .transpose()?
+                .unwrap_or_default();
+            let max_order = parts.pop().map(|x| x.parse()).transpose()?.unwrap_or(32);
+            Ok::<(_, (_, _)), anyhow::Error>((unit, (fee, max_order)))
+        })
+        .collect::<Result<HashMap<_, _>, _>>()?;
+
+    let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn";
+
+    let env_filter = EnvFilter::new(format!("{default_filter},{sqlx_filter}"));
+
+    // Parse input
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
+
+    let work_dir = match &args.work_dir {
+        Some(work_dir) => work_dir.clone(),
+        None => {
+            let home_dir = home::home_dir().unwrap();
+            home_dir.join(DEFAULT_WORK_DIR)
+        }
+    };
+
+    let certs = Some(
+        args.certs
+            .map(|x| x.into())
+            .unwrap_or_else(|| work_dir.clone()),
+    );
+
+    fs::create_dir_all(&work_dir)?;
+
+    let localstore: Arc<dyn MintKeysDatabase<Err = cdk_common::database::Error> + Send + Sync> =
+        match args.engine.as_str() {
+            "sqlite" => {
+                let sql_path = work_dir.join("cdk-cli.sqlite");
+                #[cfg(not(feature = "sqlcipher"))]
+                let db = MintSqliteDatabase::new(&sql_path).await?;
+                #[cfg(feature = "sqlcipher")]
+                let db = {
+                    match args.password {
+                        Some(pass) => MintSqliteDatabase::new(&sql_path, pass).await?,
+                        None => bail!("Missing database password"),
+                    }
+                };
+
+                Arc::new(db)
+            }
+            "redb" => {
+                #[cfg(feature = "redb")]
+                {
+                    let redb_path = work_dir.join("cdk-cli.redb");
+                    Arc::new(MintRedbDatabase::new(&redb_path)?)
+                }
+                #[cfg(not(feature = "redb"))]
+                {
+                    bail!("redb feature not enabled");
+                }
+            }
+            _ => bail!("Unknown DB engine"),
+        };
+
+    let seed_path = work_dir.join("seed");
+
+    let mnemonic = if let Ok(mnemonic) = env::var(ENV_MNEMONIC) {
+        Mnemonic::from_str(&mnemonic)?
+    } else {
+        match fs::metadata(seed_path.clone()) {
+            Ok(_) => {
+                let contents = fs::read_to_string(seed_path.clone())?;
+                Mnemonic::from_str(&contents)?
+            }
+            Err(_e) => {
+                let mut rng = thread_rng();
+                let random_bytes: [u8; 32] = rng.gen();
+
+                let mnemonic = Mnemonic::from_entropy(&random_bytes)?;
+                tracing::info!("Creating new seed");
+
+                fs::write(seed_path, mnemonic.to_string())?;
+
+                mnemonic
+            }
+        }
+    };
+    let seed = mnemonic.to_seed_normalized("");
+
+    let signatory =
+        db_signatory::DbSignatory::new(localstore, &seed, supported_units, Default::default())
+            .await?;
+
+    let socket_addr = SocketAddr::from_str(&format!("{}:{}", args.listen_addr, args.listen_port))?;
+
+    grpc_server(signatory, socket_addr, certs).await?;
+
+    Ok(())
+}

+ 15 - 0
crates/cdk-signatory/src/bin/signatory.rs

@@ -0,0 +1,15 @@
+#[cfg(not(target_arch = "wasm32"))]
+mod cli;
+
+fn main() {
+    #[cfg(target_arch = "wasm32")]
+    println!("Not supported in wasm32");
+    #[cfg(not(target_arch = "wasm32"))]
+    {
+        use tokio::runtime::Runtime;
+        let rt = Runtime::new().unwrap();
+        rt.block_on(async {
+            cli::cli_main().await.unwrap();
+        });
+    }
+}

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

@@ -0,0 +1,156 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
+use bitcoin::secp256k1::{self, All, Secp256k1};
+use cdk_common::database;
+use cdk_common::error::Error;
+use cdk_common::mint::MintKeySetInfo;
+use cdk_common::nuts::{CurrencyUnit, Id, MintKeySet};
+use cdk_common::util::unix_time;
+
+/// Initialize keysets and returns a [`Result`] with a tuple of the following:
+/// * a [`HashMap`] mapping each active keyset `Id` to `MintKeySet`
+/// * a [`Vec`] of `CurrencyUnit` containing active keysets units
+pub async fn init_keysets(
+    xpriv: Xpriv,
+    secp_ctx: &Secp256k1<All>,
+    localstore: &Arc<dyn database::MintKeysDatabase<Err = database::Error> + Send + Sync>,
+    supported_units: &HashMap<CurrencyUnit, (u64, u8)>,
+    custom_paths: &HashMap<CurrencyUnit, DerivationPath>,
+) -> Result<(HashMap<Id, MintKeySet>, Vec<CurrencyUnit>), Error> {
+    let mut active_keysets: HashMap<Id, MintKeySet> = HashMap::new();
+    let mut active_keyset_units: Vec<CurrencyUnit> = vec![];
+
+    // Get keysets info from DB
+    let keysets_infos = localstore.get_keyset_infos().await?;
+
+    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));
+
+            // Get the keyset with the highest counter
+            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) {
+                if !keysets.is_empty()
+                    && &highest_index_keyset.input_fee_ppk == input_fee_ppk
+                    && &highest_index_keyset.max_order == max_order
+                {
+                    tracing::debug!("Current highest index keyset matches expect fee and max order. Setting active");
+                    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?;
+                    active_keyset_units.push(unit.clone());
+                    localstore.set_active_keyset(unit, id).await?;
+                } else {
+                    // Check to see if there are not keysets by this unit
+                    let derivation_path_index = if keysets.is_empty() {
+                        1
+                    } 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());
+                };
+            }
+        }
+    }
+
+    Ok((active_keysets, active_keyset_units))
+}
+
+/// Generate new [`MintKeySetInfo`] from path
+#[tracing::instrument(skip_all)]
+pub 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)
+}
+
+pub 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"),
+    ]))
+}

+ 345 - 0
crates/cdk-signatory/src/db_signatory.rs

@@ -0,0 +1,345 @@
+//! Main Signatory implementation
+//!
+//! It is named db_signatory because it uses a database to maintain state.
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use bitcoin::bip32::{DerivationPath, Xpriv};
+use bitcoin::secp256k1::{self, Secp256k1};
+use cdk_common::dhke::{sign_message, verify_message};
+use cdk_common::mint::MintKeySetInfo;
+use cdk_common::nuts::{BlindSignature, BlindedMessage, CurrencyUnit, Id, MintKeySet, Proof};
+use cdk_common::{database, Error, PublicKey};
+use tokio::sync::RwLock;
+use tracing::instrument;
+
+use crate::common::{create_new_keyset, derivation_path_from_unit, init_keysets};
+use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet, SignatoryKeysets};
+
+/// 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 DbSignatory {
+    keysets: RwLock<HashMap<Id, (MintKeySetInfo, MintKeySet)>>,
+    active_keysets: RwLock<HashMap<CurrencyUnit, Id>>,
+    localstore: Arc<dyn database::MintKeysDatabase<Err = database::Error> + Send + Sync>,
+    secp_ctx: Secp256k1<secp256k1::All>,
+    custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    xpriv: Xpriv,
+    xpub: PublicKey,
+}
+
+impl DbSignatory {
+    /// Creates a new MemorySignatory instance
+    pub async fn new(
+        localstore: Arc<dyn database::MintKeysDatabase<Err = database::Error> + Send + Sync>,
+        seed: &[u8],
+        mut 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, active_keyset_units) = init_keysets(
+            xpriv,
+            &secp_ctx,
+            &localstore,
+            &supported_units,
+            &custom_paths,
+        )
+        .await?;
+
+        supported_units.entry(CurrencyUnit::Auth).or_insert((0, 1));
+
+        // Create new keysets for supported units that aren't covered by the current keysets
+        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 keys = Self {
+            keysets: Default::default(),
+            active_keysets: Default::default(),
+            localstore,
+            custom_paths,
+            xpub: xpriv.to_keypair(&secp_ctx).public_key().into(),
+            secp_ctx,
+            xpriv,
+        };
+        keys.reload_keys_from_db().await?;
+
+        Ok(keys)
+    }
+
+    /// Load all the keysets from the database, even if they are not active.
+    ///
+    /// Since the database is owned by this process, we can load all the keysets in memory, and use
+    /// it as the primary source, and the database as the persistence layer.
+    ///
+    /// Any operation performed with keysets, are done through this trait and never to the database
+    /// directly.
+    async fn reload_keys_from_db(&self) -> Result<(), Error> {
+        let mut keysets = self.keysets.write().await;
+        let mut active_keysets = self.active_keysets.write().await;
+        keysets.clear();
+        active_keysets.clear();
+
+        let db_active_keysets = self.localstore.get_active_keysets().await?;
+
+        for mut info in self.localstore.get_keyset_infos().await? {
+            let id = info.id;
+            let keyset = self.generate_keyset(&info);
+            info.active = db_active_keysets.get(&info.unit) == Some(&info.id);
+            if info.active {
+                active_keysets.insert(info.unit.clone(), id);
+            }
+            keysets.insert(id, (info, keyset));
+        }
+
+        Ok(())
+    }
+
+    fn generate_keyset(&self, keyset_info: &MintKeySetInfo) -> MintKeySet {
+        MintKeySet::generate_from_xpriv(
+            &self.secp_ctx,
+            self.xpriv,
+            keyset_info.max_order,
+            keyset_info.unit.clone(),
+            keyset_info.derivation_path.clone(),
+        )
+    }
+}
+
+#[async_trait::async_trait]
+impl Signatory for DbSignatory {
+    fn name(&self) -> String {
+        format!("Signatory {}", env!("CARGO_PKG_VERSION"))
+    }
+
+    #[instrument(skip_all)]
+    async fn blind_sign(
+        &self,
+        blinded_messages: Vec<BlindedMessage>,
+    ) -> Result<Vec<BlindSignature>, Error> {
+        let keysets = self.keysets.read().await;
+
+        blinded_messages
+            .into_iter()
+            .map(|blinded_message| {
+                let BlindedMessage {
+                    amount,
+                    blinded_secret,
+                    keyset_id,
+                    ..
+                } = blinded_message;
+
+                let (info, key) = keysets.get(&keyset_id).ok_or(Error::UnknownKeySet)?;
+                if !info.active {
+                    return Err(Error::InactiveKeyset);
+                }
+
+                let key_pair = key.keys.get(&amount).ok_or(Error::UnknownKeySet)?;
+                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.clone(),
+                )?;
+
+                Ok(blinded_signature)
+            })
+            .collect::<Result<Vec<_>, _>>()
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn verify_proofs(&self, proofs: Vec<Proof>) -> Result<(), Error> {
+        let keysets = self.keysets.read().await;
+
+        proofs.into_iter().try_for_each(|proof| {
+            let (_, key) = keysets.get(&proof.keyset_id).ok_or(Error::UnknownKeySet)?;
+            let key_pair = key.keys.get(&proof.amount).ok_or(Error::UnknownKeySet)?;
+            verify_message(&key_pair.secret_key, proof.c, proof.secret.as_bytes())?;
+            Ok(())
+        })
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn keysets(&self) -> Result<SignatoryKeysets, Error> {
+        Ok(SignatoryKeysets {
+            pubkey: self.xpub,
+            keysets: self
+                .keysets
+                .read()
+                .await
+                .values()
+                .map(|k| k.into())
+                .collect::<Vec<_>>(),
+        })
+    }
+
+    /// Add current keyset to inactive keysets
+    /// Generate new keyset
+    #[tracing::instrument(skip(self))]
+    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<SignatoryKeySet, Error> {
+        let path_index = if let Some(current_keyset_id) =
+            self.localstore.get_active_keyset_id(&args.unit).await?
+        {
+            let keyset_info = self
+                .localstore
+                .get_keyset_info(&current_keyset_id)
+                .await?
+                .ok_or(Error::UnknownKeySet)?;
+
+            keyset_info.derivation_path_index.unwrap_or(1) + 1
+        } else {
+            1
+        };
+
+        let derivation_path = match self.custom_paths.get(&args.unit) {
+            Some(path) => path.clone(),
+            None => derivation_path_from_unit(args.unit.clone(), path_index)
+                .ok_or(Error::UnsupportedUnit)?,
+        };
+
+        let (keyset, info) = create_new_keyset(
+            &self.secp_ctx,
+            self.xpriv,
+            derivation_path,
+            Some(path_index),
+            args.unit.clone(),
+            args.max_order,
+            args.input_fee_ppk,
+        );
+        let id = info.id;
+        self.localstore.add_keyset_info(info.clone()).await?;
+        self.localstore.set_active_keyset(args.unit, id).await?;
+
+        self.reload_keys_from_db().await?;
+
+        Ok((&(info, keyset)).into())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::collections::HashSet;
+
+    use bitcoin::key::Secp256k1;
+    use bitcoin::Network;
+    use cdk_common::{Amount, MintKeySet, 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);
+    }
+}

+ 147 - 0
crates/cdk-signatory/src/embedded.rs

@@ -0,0 +1,147 @@
+//! Run a Signatory in a embedded environment, inside a CDK instance, but this wrapper makes sure to
+//! run the Signatory in another thread, isolated form the main CDK, communicating through messages
+use std::sync::Arc;
+
+use cdk_common::{BlindSignature, BlindedMessage, Error, Proof};
+use tokio::sync::{mpsc, oneshot};
+use tokio::task::JoinHandle;
+
+use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet, SignatoryKeysets};
+
+enum Request {
+    BlindSign(
+        (
+            Vec<BlindedMessage>,
+            oneshot::Sender<Result<Vec<BlindSignature>, Error>>,
+        ),
+    ),
+    VerifyProof((Vec<Proof>, oneshot::Sender<Result<(), Error>>)),
+    Keysets(oneshot::Sender<Result<SignatoryKeysets, Error>>),
+    RotateKeyset(
+        (
+            RotateKeyArguments,
+            oneshot::Sender<Result<SignatoryKeySet, Error>>,
+        ),
+    ),
+}
+
+/// Creates a service-like to wrap an implementation of the Signatory
+///
+/// This implements the actor model, ensuring the Signatory and their private key is moved from the
+/// main thread to their own tokio task, and communicates with the main program by passing messages,
+/// an extra layer of security to move the keys to another layer.
+pub struct Service {
+    pipeline: mpsc::Sender<Request>,
+    runner: Option<JoinHandle<()>>,
+}
+
+impl Drop for Service {
+    fn drop(&mut self) {
+        if let Some(runner) = self.runner.take() {
+            runner.abort();
+        }
+    }
+}
+
+impl Service {
+    /// Takes a signatory and spawns it into a Tokio task, isolating its implementation with the
+    /// main thread, communicating with it through messages
+    pub fn new(handler: Arc<dyn Signatory + Send + Sync>) -> Self {
+        let (tx, rx) = mpsc::channel(10_000);
+        let runner = Some(tokio::spawn(Self::runner(rx, handler)));
+
+        Self {
+            pipeline: tx,
+            runner,
+        }
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn runner(
+        mut receiver: mpsc::Receiver<Request>,
+        handler: Arc<dyn Signatory + Send + Sync>,
+    ) {
+        while let Some(request) = receiver.recv().await {
+            match request {
+                Request::BlindSign((blinded_message, response)) => {
+                    let output = handler.blind_sign(blinded_message).await;
+                    if let Err(err) = response.send(output) {
+                        tracing::error!("Error sending response: {:?}", err);
+                    }
+                }
+                Request::VerifyProof((proof, response)) => {
+                    let output = handler.verify_proofs(proof).await;
+                    if let Err(err) = response.send(output) {
+                        tracing::error!("Error sending response: {:?}", err);
+                    }
+                }
+                Request::Keysets(response) => {
+                    let output = handler.keysets().await;
+                    if let Err(err) = response.send(output) {
+                        tracing::error!("Error sending response: {:?}", err);
+                    }
+                }
+                Request::RotateKeyset((args, response)) => {
+                    let output = handler.rotate_keyset(args).await;
+                    if let Err(err) = response.send(output) {
+                        tracing::error!("Error sending response: {:?}", err);
+                    }
+                }
+            }
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl Signatory for Service {
+    fn name(&self) -> String {
+        "Embedded".to_owned()
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn blind_sign(
+        &self,
+        blinded_messages: Vec<BlindedMessage>,
+    ) -> Result<Vec<BlindSignature>, Error> {
+        let (tx, rx) = oneshot::channel();
+        self.pipeline
+            .send(Request::BlindSign((blinded_messages, tx)))
+            .await
+            .map_err(|e| Error::SendError(e.to_string()))?;
+
+        rx.await.map_err(|e| Error::RecvError(e.to_string()))?
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn verify_proofs(&self, proofs: Vec<Proof>) -> Result<(), Error> {
+        let (tx, rx) = oneshot::channel();
+        self.pipeline
+            .send(Request::VerifyProof((proofs, tx)))
+            .await
+            .map_err(|e| Error::SendError(e.to_string()))?;
+
+        rx.await.map_err(|e| Error::RecvError(e.to_string()))?
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn keysets(&self) -> Result<SignatoryKeysets, Error> {
+        let (tx, rx) = oneshot::channel();
+        self.pipeline
+            .send(Request::Keysets(tx))
+            .await
+            .map_err(|e| Error::SendError(e.to_string()))?;
+
+        rx.await.map_err(|e| Error::RecvError(e.to_string()))?
+    }
+
+    #[tracing::instrument(skip(self))]
+    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<SignatoryKeySet, Error> {
+        let (tx, rx) = oneshot::channel();
+        self.pipeline
+            .send(Request::RotateKeyset((args, tx)))
+            .await
+            .map_err(|e| Error::SendError(e.to_string()))?;
+
+        rx.await.map_err(|e| Error::RecvError(e.to_string()))?
+    }
+}

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

@@ -0,0 +1,21 @@
+//! 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.
+#![deny(missing_docs)]
+#![deny(warnings)]
+
+#[cfg(feature = "grpc")]
+mod proto;
+
+#[cfg(feature = "grpc")]
+pub use proto::{client::SignatoryRpcClient, server::grpc_server};
+
+mod common;
+
+pub mod db_signatory;
+pub mod embedded;
+pub mod signatory;

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

@@ -0,0 +1,158 @@
+use std::path::Path;
+
+use cdk_common::error::Error;
+use cdk_common::{BlindSignature, BlindedMessage, Proof};
+use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
+
+use crate::proto::signatory_client::SignatoryClient;
+use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet, SignatoryKeysets};
+
+/// A client for the Signatory service.
+pub struct SignatoryRpcClient {
+    client: SignatoryClient<tonic::transport::Channel>,
+    url: String,
+}
+
+#[derive(thiserror::Error, Debug)]
+/// Client Signatory Error
+pub enum ClientError {
+    /// Transport error
+    #[error(transparent)]
+    Transport(#[from] tonic::transport::Error),
+
+    /// IO-related errors
+    #[error(transparent)]
+    Io(#[from] std::io::Error),
+
+    /// Signatory Error
+    #[error(transparent)]
+    Signatory(#[from] cdk_common::error::Error),
+
+    /// Invalid URL
+    #[error("Invalid URL")]
+    InvalidUrl,
+}
+
+impl SignatoryRpcClient {
+    /// Create a new RemoteSigner from a tonic transport channel.
+    pub async fn new<A: AsRef<Path>>(url: String, tls_dir: Option<A>) -> Result<Self, ClientError> {
+        let channel = if let Some(tls_dir) = tls_dir {
+            let tls_dir = tls_dir.as_ref();
+            let server_root_ca_cert = std::fs::read_to_string(tls_dir.join("ca.pem"))?;
+            let server_root_ca_cert = Certificate::from_pem(server_root_ca_cert);
+            let client_cert = std::fs::read_to_string(tls_dir.join("client.pem"))?;
+            let client_key = std::fs::read_to_string(tls_dir.join("client.key"))?;
+            let client_identity = Identity::from_pem(client_cert, client_key);
+            let tls = ClientTlsConfig::new()
+                .ca_certificate(server_root_ca_cert)
+                .identity(client_identity);
+
+            Channel::from_shared(url.clone())
+                .map_err(|_| ClientError::InvalidUrl)?
+                .tls_config(tls)?
+                .connect()
+                .await?
+        } else {
+            Channel::from_shared(url.clone())
+                .map_err(|_| ClientError::InvalidUrl)?
+                .connect()
+                .await?
+        };
+
+        Ok(Self {
+            client: SignatoryClient::new(channel),
+            url,
+        })
+    }
+}
+
+macro_rules! handle_error {
+    ($x:expr, $y:ident, scalar) => {{
+        let mut obj = $x.into_inner();
+        if let Some(err) = obj.error.take() {
+            return Err(err.into());
+        }
+
+        obj.$y
+    }};
+    ($x:expr, $y:ident) => {{
+        let mut obj = $x.into_inner();
+        if let Some(err) = obj.error.take() {
+            return Err(err.into());
+        }
+
+        obj.$y
+            .take()
+            .ok_or(Error::Custom("Internal error".to_owned()))?
+    }};
+}
+
+#[async_trait::async_trait]
+impl Signatory for SignatoryRpcClient {
+    fn name(&self) -> String {
+        format!("Rpc Signatory {}", self.url)
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn blind_sign(&self, request: Vec<BlindedMessage>) -> Result<Vec<BlindSignature>, Error> {
+        let req = super::BlindedMessages {
+            blinded_messages: request
+                .into_iter()
+                .map(|blind_message| blind_message.into())
+                .collect(),
+            operation: super::Operation::Unspecified.into(),
+            correlation_id: "".to_owned(),
+        };
+
+        self.client
+            .clone()
+            .blind_sign(req)
+            .await
+            .map(|response| {
+                handle_error!(response, sigs)
+                    .blind_signatures
+                    .into_iter()
+                    .map(|blinded_signature| blinded_signature.try_into())
+                    .collect()
+            })
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn verify_proofs(&self, proofs: Vec<Proof>) -> Result<(), Error> {
+        let req: super::Proofs = proofs.into();
+        self.client
+            .clone()
+            .verify_proofs(req)
+            .await
+            .map(|response| {
+                if handle_error!(response, success, scalar) {
+                    Ok(())
+                } else {
+                    Err(Error::SignatureMissingOrInvalid)
+                }
+            })
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn keysets(&self) -> Result<SignatoryKeysets, Error> {
+        self.client
+            .clone()
+            .keysets(super::EmptyRequest {})
+            .await
+            .map(|response| handle_error!(response, keysets).try_into())
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+
+    #[tracing::instrument(skip(self))]
+    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<SignatoryKeySet, Error> {
+        let req: super::RotationRequest = args.into();
+        self.client
+            .clone()
+            .rotate_keyset(req)
+            .await
+            .map(|response| handle_error!(response, keyset).try_into())
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+}

+ 455 - 0
crates/cdk-signatory/src/proto/convert.rs

@@ -0,0 +1,455 @@
+//! Type conversions between Rust types and the generated protobuf types.
+use std::collections::BTreeMap;
+
+use cdk_common::secret::Secret;
+use cdk_common::util::hex;
+use cdk_common::{Amount, HTLCWitness, P2PKWitness, PublicKey};
+use tonic::Status;
+
+use super::*;
+
+const INTERNAL_ERROR: &str = "Missing property";
+
+impl From<crate::signatory::SignatoryKeysets> for SignatoryKeysets {
+    fn from(keyset: crate::signatory::SignatoryKeysets) -> Self {
+        Self {
+            pubkey: keyset.pubkey.to_bytes().to_vec(),
+            keysets: keyset
+                .keysets
+                .into_iter()
+                .map(|keyset| keyset.into())
+                .collect(),
+        }
+    }
+}
+
+impl TryInto<crate::signatory::SignatoryKeysets> for SignatoryKeysets {
+    /// TODO: Make sure that all type Error here are cdk_common::Error
+    type Error = cdk_common::Error;
+
+    fn try_into(self) -> Result<crate::signatory::SignatoryKeysets, Self::Error> {
+        Ok(crate::signatory::SignatoryKeysets {
+            pubkey: PublicKey::from_slice(&self.pubkey)?,
+            keysets: self
+                .keysets
+                .into_iter()
+                .map(|keyset| keyset.try_into())
+                .collect::<Result<Vec<_>, _>>()?,
+        })
+    }
+}
+
+impl TryInto<crate::signatory::SignatoryKeySet> for KeySet {
+    type Error = cdk_common::Error;
+
+    fn try_into(self) -> Result<crate::signatory::SignatoryKeySet, Self::Error> {
+        Ok(crate::signatory::SignatoryKeySet {
+            id: self.id.parse()?,
+            unit: self
+                .unit
+                .ok_or(cdk_common::Error::Custom(INTERNAL_ERROR.to_owned()))?
+                .try_into()
+                .map_err(|_| cdk_common::Error::Custom("Invalid currency unit".to_owned()))?,
+            active: self.active,
+            input_fee_ppk: self.input_fee_ppk,
+            keys: cdk_common::Keys::new(
+                self.keys
+                    .ok_or(cdk_common::Error::Custom(INTERNAL_ERROR.to_owned()))?
+                    .keys
+                    .into_iter()
+                    .map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk)))
+                    .collect::<Result<BTreeMap<Amount, _>, _>>()?,
+            ),
+        })
+    }
+}
+
+impl From<crate::signatory::SignatoryKeySet> for KeySet {
+    fn from(keyset: crate::signatory::SignatoryKeySet) -> Self {
+        Self {
+            id: keyset.id.to_string(),
+            unit: Some(keyset.unit.into()),
+            active: keyset.active,
+            input_fee_ppk: keyset.input_fee_ppk,
+            keys: Some(Keys {
+                keys: keyset
+                    .keys
+                    .iter()
+                    .map(|(key, value)| ((*key).into(), value.to_bytes().to_vec()))
+                    .collect(),
+            }),
+        }
+    }
+}
+
+impl From<cdk_common::Error> for Error {
+    fn from(err: cdk_common::Error) -> Self {
+        let code = match err {
+            cdk_common::Error::AmountError(_) => ErrorCode::AmountOutsideLimit,
+            cdk_common::Error::DuplicateInputs => ErrorCode::DuplicateInputsProvided,
+            cdk_common::Error::DuplicateOutputs => ErrorCode::DuplicateInputsProvided,
+            cdk_common::Error::UnknownKeySet => ErrorCode::KeysetNotKnown,
+            cdk_common::Error::InactiveKeyset => ErrorCode::KeysetInactive,
+            _ => ErrorCode::Unspecified,
+        };
+
+        Error {
+            code: code.into(),
+            detail: err.to_string(),
+        }
+    }
+}
+
+impl From<Error> for cdk_common::Error {
+    fn from(val: Error) -> Self {
+        match val.code.try_into().expect("valid code") {
+            ErrorCode::AmountOutsideLimit => {
+                cdk_common::Error::AmountError(cdk_common::amount::Error::AmountOverflow)
+            }
+            ErrorCode::DuplicateInputsProvided => cdk_common::Error::DuplicateInputs,
+            ErrorCode::KeysetNotKnown => cdk_common::Error::UnknownKeySet,
+            ErrorCode::KeysetInactive => cdk_common::Error::InactiveKeyset,
+            ErrorCode::Unspecified => cdk_common::Error::Custom(val.detail),
+            _ => todo!(),
+        }
+    }
+}
+
+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 From<Vec<cdk_common::Proof>> for Proofs {
+    fn from(value: Vec<cdk_common::Proof>) -> Self {
+        Proofs {
+            proof: value.into_iter().map(|x| x.into()).collect(),
+            operation: Operation::Unspecified.into(),
+            correlation_id: "".to_owned(),
+        }
+    }
+}
+
+impl From<cdk_common::Proof> for Proof {
+    fn from(value: cdk_common::Proof) -> Self {
+        Proof {
+            amount: value.amount.into(),
+            keyset_id: value.keyset_id.to_string(),
+            secret: value.secret.to_bytes(),
+            c: value.c.to_bytes().to_vec(),
+        }
+    }
+}
+
+impl TryInto<cdk_common::Proof> for Proof {
+    type Error = Status;
+    fn try_into(self) -> Result<cdk_common::Proof, Self::Error> {
+        let secret = if let Ok(str) = String::from_utf8(self.secret.clone()) {
+            str
+        } else {
+            hex::encode(&self.secret)
+        };
+
+        Ok(cdk_common::Proof {
+            amount: self.amount.into(),
+            keyset_id: self
+                .keyset_id
+                .parse()
+                .map_err(|e| Status::from_error(Box::new(e)))?,
+            secret: Secret::new(secret),
+            c: cdk_common::PublicKey::from_slice(&self.c)
+                .map_err(|e| Status::from_error(Box::new(e)))?,
+            witness: None,
+            dleq: None,
+        })
+    }
+}
+
+impl From<cdk_common::ProofDleq> for ProofDleq {
+    fn from(value: cdk_common::ProofDleq) -> Self {
+        ProofDleq {
+            e: value.e.as_secret_bytes().to_vec(),
+            s: value.s.as_secret_bytes().to_vec(),
+            r: value.r.as_secret_bytes().to_vec(),
+        }
+    }
+}
+
+impl TryInto<cdk_common::ProofDleq> for ProofDleq {
+    type Error = Status;
+
+    fn try_into(self) -> Result<cdk_common::ProofDleq, Self::Error> {
+        Ok(cdk_common::ProofDleq {
+            e: cdk_common::SecretKey::from_slice(&self.e)
+                .map_err(|e| Status::from_error(Box::new(e)))?,
+            s: cdk_common::SecretKey::from_slice(&self.s)
+                .map_err(|e| Status::from_error(Box::new(e)))?,
+            r: cdk_common::SecretKey::from_slice(&self.r)
+                .map_err(|e| Status::from_error(Box::new(e)))?,
+        })
+    }
+}
+
+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::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(),
+        }
+    }
+}
+
+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: None,
+        })
+    }
+}
+
+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")),
+        }
+    }
+}
+
+impl From<()> for EmptyRequest {
+    fn from(_: ()) -> Self {
+        EmptyRequest {}
+    }
+}
+
+impl TryInto<()> for EmptyRequest {
+    type Error = cdk_common::error::Error;
+
+    fn try_into(self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
+
+impl From<cdk_common::CurrencyUnit> for CurrencyUnit {
+    fn from(value: cdk_common::CurrencyUnit) -> Self {
+        match value {
+            cdk_common::CurrencyUnit::Sat => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::Sat.into(),
+                )),
+            },
+            cdk_common::CurrencyUnit::Msat => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::Msat.into(),
+                )),
+            },
+            cdk_common::CurrencyUnit::Usd => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::Usd.into(),
+                )),
+            },
+            cdk_common::CurrencyUnit::Eur => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::Eur.into(),
+                )),
+            },
+            cdk_common::CurrencyUnit::Auth => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::Auth.into(),
+                )),
+            },
+            cdk_common::CurrencyUnit::Custom(name) => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::CustomUnit(name)),
+            },
+            _ => unreachable!(),
+        }
+    }
+}
+
+impl TryInto<cdk_common::CurrencyUnit> for CurrencyUnit {
+    type Error = Status;
+
+    fn try_into(self) -> Result<cdk_common::CurrencyUnit, Self::Error> {
+        match self.currency_unit {
+            Some(currency_unit::CurrencyUnit::Unit(u)) => match u
+                .try_into()
+                .map_err(|_| Status::invalid_argument("Invalid currency unit"))?
+            {
+                CurrencyUnitType::Sat => Ok(cdk_common::CurrencyUnit::Sat),
+                CurrencyUnitType::Msat => Ok(cdk_common::CurrencyUnit::Msat),
+                CurrencyUnitType::Usd => Ok(cdk_common::CurrencyUnit::Usd),
+                CurrencyUnitType::Eur => Ok(cdk_common::CurrencyUnit::Eur),
+                CurrencyUnitType::Auth => Ok(cdk_common::CurrencyUnit::Auth),
+                CurrencyUnitType::Unspecified => {
+                    Err(Status::invalid_argument("Current unit is not specified"))
+                }
+            },
+            Some(currency_unit::CurrencyUnit::CustomUnit(name)) => {
+                Ok(cdk_common::CurrencyUnit::Custom(name))
+            }
+            None => Err(Status::invalid_argument("Currency unit not set")),
+        }
+    }
+}
+
+impl TryInto<cdk_common::KeySet> for KeySet {
+    type Error = cdk_common::error::Error;
+    fn try_into(self) -> Result<cdk_common::KeySet, Self::Error> {
+        Ok(cdk_common::KeySet {
+            id: self
+                .id
+                .parse()
+                .map_err(|_| cdk_common::error::Error::Custom("Invalid ID".to_owned()))?,
+            unit: self
+                .unit
+                .ok_or(cdk_common::error::Error::Custom(INTERNAL_ERROR.to_owned()))?
+                .try_into()
+                .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
+            keys: cdk_common::Keys::new(
+                self.keys
+                    .ok_or(cdk_common::error::Error::Custom(INTERNAL_ERROR.to_owned()))?
+                    .keys
+                    .into_iter()
+                    .map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk)))
+                    .collect::<Result<BTreeMap<cdk_common::Amount, cdk_common::PublicKey>, _>>()?,
+            ),
+        })
+    }
+}
+
+impl From<crate::signatory::RotateKeyArguments> for RotationRequest {
+    fn from(value: crate::signatory::RotateKeyArguments) -> Self {
+        Self {
+            unit: Some(value.unit.into()),
+            max_order: value.max_order.into(),
+            input_fee_ppk: value.input_fee_ppk,
+        }
+    }
+}
+
+impl TryInto<crate::signatory::RotateKeyArguments> for RotationRequest {
+    type Error = Status;
+
+    fn try_into(self) -> Result<crate::signatory::RotateKeyArguments, Self::Error> {
+        Ok(crate::signatory::RotateKeyArguments {
+            unit: self
+                .unit
+                .ok_or(Status::invalid_argument("unit not set"))?
+                .try_into()?,
+            max_order: self
+                .max_order
+                .try_into()
+                .map_err(|_| Status::invalid_argument("Invalid max_order"))?,
+            input_fee_ppk: self.input_fee_ppk,
+        })
+    }
+}
+
+impl From<cdk_common::KeySetInfo> for KeySet {
+    fn from(value: cdk_common::KeySetInfo) -> Self {
+        Self {
+            id: value.id.into(),
+            unit: Some(value.unit.into()),
+            active: value.active,
+            input_fee_ppk: value.input_fee_ppk,
+            keys: Default::default(),
+        }
+    }
+}
+
+impl TryInto<cdk_common::KeySetInfo> for KeySet {
+    type Error = cdk_common::Error;
+
+    fn try_into(self) -> Result<cdk_common::KeySetInfo, Self::Error> {
+        Ok(cdk_common::KeySetInfo {
+            id: self.id.try_into()?,
+            unit: self
+                .unit
+                .ok_or(cdk_common::Error::Custom(INTERNAL_ERROR.to_owned()))?
+                .try_into()
+                .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
+            active: self.active,
+            input_fee_ppk: self.input_fee_ppk,
+        })
+    }
+}

+ 6 - 0
crates/cdk-signatory/src/proto/mod.rs

@@ -0,0 +1,6 @@
+mod convert;
+
+tonic::include_proto!("signatory");
+
+pub mod client;
+pub mod server;

+ 222 - 0
crates/cdk-signatory/src/proto/server.rs

@@ -0,0 +1,222 @@
+use std::net::SocketAddr;
+use std::path::Path;
+
+use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig};
+use tonic::{Request, Response, Status};
+
+use crate::proto::{self, signatory_server};
+use crate::signatory::Signatory;
+
+pub struct CdkSignatoryServer<T>
+where
+    T: Signatory + Send + Sync + 'static,
+{
+    inner: T,
+}
+
+#[tonic::async_trait]
+impl<T> signatory_server::Signatory for CdkSignatoryServer<T>
+where
+    T: Signatory + Send + Sync + 'static,
+{
+    #[tracing::instrument(skip_all)]
+    async fn blind_sign(
+        &self,
+        request: Request<proto::BlindedMessages>,
+    ) -> Result<Response<proto::BlindSignResponse>, Status> {
+        let result = match self
+            .inner
+            .blind_sign(
+                request
+                    .into_inner()
+                    .blinded_messages
+                    .into_iter()
+                    .map(|blind_message| blind_message.try_into())
+                    .collect::<Result<Vec<_>, _>>()?,
+            )
+            .await
+        {
+            Ok(blind_signatures) => proto::BlindSignResponse {
+                sigs: Some(proto::BlindSignatures {
+                    blind_signatures: blind_signatures
+                        .into_iter()
+                        .map(|blind_sign| blind_sign.into())
+                        .collect(),
+                }),
+                ..Default::default()
+            },
+            Err(err) => proto::BlindSignResponse {
+                error: Some(err.into()),
+                ..Default::default()
+            },
+        };
+
+        Ok(Response::new(result))
+    }
+
+    #[tracing::instrument(skip_all)]
+    async fn verify_proofs(
+        &self,
+        request: Request<proto::Proofs>,
+    ) -> Result<Response<proto::BooleanResponse>, Status> {
+        let result = match self
+            .inner
+            .verify_proofs(
+                request
+                    .into_inner()
+                    .proof
+                    .into_iter()
+                    .map(|x| x.try_into())
+                    .collect::<Result<Vec<_>, _>>()?,
+            )
+            .await
+        {
+            Ok(()) => proto::BooleanResponse {
+                success: true,
+                ..Default::default()
+            },
+
+            Err(cdk_common::Error::DHKE(_)) => proto::BooleanResponse {
+                success: false,
+                ..Default::default()
+            },
+            Err(err) => proto::BooleanResponse {
+                error: Some(err.into()),
+                ..Default::default()
+            },
+        };
+
+        Ok(Response::new(result))
+    }
+
+    async fn keysets(
+        &self,
+        _request: Request<proto::EmptyRequest>,
+    ) -> Result<Response<proto::KeysResponse>, Status> {
+        let result = match self.inner.keysets().await {
+            Ok(result) => proto::KeysResponse {
+                keysets: Some(result.into()),
+                ..Default::default()
+            },
+            Err(err) => proto::KeysResponse {
+                error: Some(err.into()),
+                ..Default::default()
+            },
+        };
+
+        Ok(Response::new(result))
+    }
+
+    async fn rotate_keyset(
+        &self,
+        request: Request<proto::RotationRequest>,
+    ) -> Result<Response<proto::KeyRotationResponse>, Status> {
+        let mint_keyset_info = match self
+            .inner
+            .rotate_keyset(request.into_inner().try_into()?)
+            .await
+        {
+            Ok(result) => proto::KeyRotationResponse {
+                keyset: Some(result.into()),
+                ..Default::default()
+            },
+            Err(err) => proto::KeyRotationResponse {
+                error: Some(err.into()),
+                ..Default::default()
+            },
+        };
+
+        Ok(Response::new(mint_keyset_info))
+    }
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    /// Transport error
+    #[error(transparent)]
+    Transport(#[from] tonic::transport::Error),
+    /// Io error
+    #[error(transparent)]
+    Io(#[from] std::io::Error),
+}
+
+/// Runs the signatory server
+pub async fn grpc_server<T, I: AsRef<Path>>(
+    signatory: T,
+    addr: SocketAddr,
+    tls_dir: Option<I>,
+) -> Result<(), Error>
+where
+    T: Signatory + Send + Sync + 'static,
+{
+    tracing::info!("Starting RPC server {}", addr);
+
+    let mut server = match tls_dir {
+        Some(tls_dir) => {
+            tracing::info!("TLS configuration found, starting secure server");
+            let tls_dir = tls_dir.as_ref();
+            let server_pem_path = tls_dir.join("server.pem");
+            let server_key_path = tls_dir.join("server.key");
+            let ca_pem_path = tls_dir.join("ca.pem");
+
+            if !server_pem_path.exists() {
+                tracing::error!(
+                    "Server certificate file does not exist: {}",
+                    server_pem_path.display()
+                );
+                return Err(Error::Io(std::io::Error::new(
+                    std::io::ErrorKind::NotFound,
+                    format!(
+                        "Server certificate file not found: {}",
+                        server_pem_path.display()
+                    ),
+                )));
+            }
+
+            if !server_key_path.exists() {
+                tracing::error!(
+                    "Server key file does not exist: {}",
+                    server_key_path.display()
+                );
+                return Err(Error::Io(std::io::Error::new(
+                    std::io::ErrorKind::NotFound,
+                    format!("Server key file not found: {}", server_key_path.display()),
+                )));
+            }
+
+            if !ca_pem_path.exists() {
+                tracing::error!(
+                    "CA certificate file does not exist: {}",
+                    ca_pem_path.display()
+                );
+                return Err(Error::Io(std::io::Error::new(
+                    std::io::ErrorKind::NotFound,
+                    format!("CA certificate file not found: {}", ca_pem_path.display()),
+                )));
+            }
+
+            let cert = std::fs::read_to_string(&server_pem_path)?;
+            let key = std::fs::read_to_string(&server_key_path)?;
+            let client_ca_cert = std::fs::read_to_string(&ca_pem_path)?;
+            let client_ca_cert = Certificate::from_pem(client_ca_cert);
+            let server_identity = Identity::from_pem(cert, key);
+            let tls_config = ServerTlsConfig::new()
+                .identity(server_identity)
+                .client_ca_root(client_ca_cert);
+
+            Server::builder().tls_config(tls_config)?
+        }
+        None => {
+            tracing::warn!("No valid TLS configuration found, starting insecure server");
+            Server::builder()
+        }
+    };
+
+    server
+        .add_service(signatory_server::SignatoryServer::new(CdkSignatoryServer {
+            inner: signatory,
+        }))
+        .serve(addr)
+        .await?;
+    Ok(())
+}

+ 152 - 0
crates/cdk-signatory/src/signatory.rs

@@ -0,0 +1,152 @@
+//! 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 cdk_common::error::Error;
+use cdk_common::mint::MintKeySetInfo;
+use cdk_common::{
+    BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, Keys, MintKeySet, Proof, PublicKey,
+};
+
+#[derive(Debug)]
+/// 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)
+    }
+}
+
+/// RotateKeyArguments
+///
+/// This struct is used to pass the arguments to the rotate_keyset function
+///
+/// TODO: Change argument to accept a vector of Amount instead of max_order.
+#[derive(Debug, Clone)]
+pub struct RotateKeyArguments {
+    /// Unit
+    pub unit: CurrencyUnit,
+    /// Max order
+    pub max_order: u8,
+    /// Input fee
+    pub input_fee_ppk: u64,
+}
+
+#[derive(Debug, Clone)]
+/// Signatory keysets
+pub struct SignatoryKeysets {
+    /// The public key
+    pub pubkey: PublicKey,
+    /// The list of keysets
+    pub keysets: Vec<SignatoryKeySet>,
+}
+
+#[derive(Debug, Clone)]
+/// SignatoryKeySet
+///
+/// This struct is used to represent a keyset and its info, pretty much all the information but the
+/// private key, that will never leave the signatory
+pub struct SignatoryKeySet {
+    /// The keyset Id
+    pub id: Id,
+    /// The Currency Unit
+    pub unit: CurrencyUnit,
+    /// Whether to set it as active or not
+    pub active: bool,
+    /// The list of public keys
+    pub keys: Keys,
+    /// Information about the fee per public key
+    pub input_fee_ppk: u64,
+}
+
+impl From<&SignatoryKeySet> for KeySet {
+    fn from(val: &SignatoryKeySet) -> Self {
+        val.to_owned().into()
+    }
+}
+
+impl From<SignatoryKeySet> for KeySet {
+    fn from(val: SignatoryKeySet) -> Self {
+        KeySet {
+            id: val.id,
+            unit: val.unit,
+            keys: val.keys,
+        }
+    }
+}
+
+impl From<&SignatoryKeySet> for MintKeySetInfo {
+    fn from(val: &SignatoryKeySet) -> Self {
+        val.to_owned().into()
+    }
+}
+
+impl From<SignatoryKeySet> for MintKeySetInfo {
+    fn from(val: SignatoryKeySet) -> Self {
+        MintKeySetInfo {
+            id: val.id,
+            unit: val.unit,
+            active: val.active,
+            input_fee_ppk: val.input_fee_ppk,
+            derivation_path: Default::default(),
+            derivation_path_index: Default::default(),
+            max_order: 0,
+            valid_to: None,
+            valid_from: 0,
+        }
+    }
+}
+
+impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
+    fn from((info, key): &(MintKeySetInfo, MintKeySet)) -> Self {
+        Self {
+            id: info.id,
+            unit: key.unit.clone(),
+            active: info.active,
+            input_fee_ppk: info.input_fee_ppk,
+            keys: key.keys.clone().into(),
+        }
+    }
+}
+
+#[async_trait::async_trait]
+/// Signatory trait
+pub trait Signatory {
+    /// The Signatory implementation name. This may be exposed, so being as discreet as possible is
+    /// advised.
+    fn name(&self) -> String;
+
+    /// Blind sign a message.
+    ///
+    /// The message can be for a coin or an auth token.
+    async fn blind_sign(
+        &self,
+        blinded_messages: Vec<BlindedMessage>,
+    ) -> Result<Vec<BlindSignature>, Error>;
+
+    /// Verify [`Proof`] meets conditions and is signed
+    async fn verify_proofs(&self, proofs: Vec<Proof>) -> Result<(), Error>;
+
+    /// Retrieve the list of all mint keysets
+    async fn keysets(&self) -> Result<SignatoryKeysets, Error>;
+
+    /// Add current keyset to inactive keysets
+    /// Generate new keyset
+    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<SignatoryKeySet, Error>;
+}

+ 31 - 0
crates/cdk-sqlite/src/mint/migrations/20250415093121_drop_keystore_foreign.sql

@@ -0,0 +1,31 @@
+CREATE TABLE proof_new (
+    y BLOB PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    keyset_id TEXT NOT NULL, -- no FK constraint here
+    secret TEXT NOT NULL,
+    c BLOB NOT NULL,
+    witness TEXT,
+    state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
+    quote_id TEXT,
+    created_time INTEGER NOT NULL DEFAULT 0
+);
+
+INSERT INTO proof_new SELECT * FROM proof;
+DROP TABLE proof;
+ALTER TABLE proof_new RENAME TO proof;
+
+
+CREATE TABLE blind_signature_new (
+    y BLOB PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    keyset_id TEXT NOT NULL,  -- FK removed
+    c BLOB NOT NULL,
+    dleq_e TEXT,
+    dleq_s TEXT,
+    quote_id TEXT,
+    created_time INTEGER NOT NULL DEFAULT 0
+);
+
+INSERT INTO blind_signature_new SELECT * FROM blind_signature;
+DROP TABLE blind_signature;
+ALTER TABLE blind_signature_new RENAME TO blind_signature;

+ 7 - 0
crates/cdk/Cargo.toml

@@ -42,6 +42,11 @@ utoipa = { workspace = true, optional = true }
 uuid.workspace = true
 jsonwebtoken = { workspace = true, optional = true }
 
+# -Z minimal-versions
+sync_wrapper = "0.1.2"
+bech32 = "0.9.1"
+arc-swap = "1.7.1"
+
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 tokio = { workspace = true, features = [
     "rt-multi-thread",
@@ -50,6 +55,7 @@ tokio = { workspace = true, features = [
     "sync",
 ] }
 getrandom = { version = "0.2" }
+cdk-signatory = { workspace = true, features = ["grpc"] }
 tokio-tungstenite = { workspace = true, features = [
     "rustls",
     "rustls-tls-native-roots",
@@ -58,6 +64,7 @@ tokio-tungstenite = { workspace = true, features = [
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
+cdk-signatory = { workspace = true, default-features = false }
 getrandom = { version = "0.2", features = ["js"] }
 
 [[example]]

+ 79 - 269
crates/cdk/src/mint/auth/mod.rs

@@ -1,13 +1,10 @@
-use cdk_common::{CurrencyUnit, MintKeySet};
 use tracing::instrument;
 
 use super::nut21::ProtectedEndpoint;
 use super::{
-    AuthProof, AuthRequired, AuthToken, BlindAuthToken, BlindSignature, BlindedMessage, Error, Id,
+    AuthProof, AuthRequired, AuthToken, BlindAuthToken, BlindSignature, BlindedMessage, Error,
     Mint, State,
 };
-use crate::dhke::{sign_message, verify_message};
-use crate::Amount;
 
 impl Mint {
     /// Check if and what kind of auth is required for a method
@@ -34,109 +31,12 @@ impl Mint {
             .await?)
     }
 
-    /// Ensure Keyset is loaded in mint
-    #[instrument(skip(self))]
-    pub async fn ensure_blind_auth_keyset_loaded(&self, id: &Id) -> Result<MintKeySet, Error> {
-        {
-            if let Some(keyset) = self.keysets.read().await.get(id) {
-                return Ok(keyset.clone());
-            }
-        }
-
-        tracing::info!(
-            "Keyset {:?} not found in memory, attempting to load from storage",
-            id
-        );
-
-        let mut keysets = self.keysets.write().await;
-
-        // Get auth_localstore reference
-        let auth_localstore = match self.auth_localstore.as_ref() {
-            Some(store) => store,
-            None => {
-                tracing::error!("Auth localstore is not configured");
-                return Err(Error::AmountKey);
-            }
-        };
-
-        // Get keyset info from storage
-        let keyset_info = match auth_localstore.get_keyset_info(id).await {
-            Ok(Some(info)) => {
-                tracing::debug!("Found keyset info in storage for ID {:?}", id);
-                info
-            }
-            Ok(None) => {
-                tracing::error!("Keyset with ID {:?} not found in storage", id);
-                return Err(Error::KeysetUnknown(*id));
-            }
-            Err(e) => {
-                tracing::error!("Error retrieving keyset info from storage: {:?}", e);
-                return Err(e.into());
-            }
-        };
-
-        let id = keyset_info.id;
-        tracing::info!("Generating and inserting keyset {:?} into memory", id);
-        let keyset = self.generate_keyset(keyset_info);
-
-        keysets.insert(id, keyset.clone());
-        tracing::debug!("Keyset {:?} successfully loaded", id);
-        Ok(keyset)
-    }
-
     /// Verify Blind auth
     #[instrument(skip(self, token))]
     pub async fn verify_blind_auth(&self, token: &BlindAuthToken) -> Result<(), Error> {
-        let proof = &token.auth_proof;
-        let keyset_id = proof.keyset_id;
-
-        tracing::trace!(
-            "Starting blind auth verification for keyset ID: {:?}",
-            keyset_id
-        );
-
-        // Ensure the keyset is loaded
-        let keyset = self
-            .ensure_blind_auth_keyset_loaded(&keyset_id)
+        self.signatory
+            .verify_proofs(vec![token.auth_proof.clone().into()])
             .await
-            .map_err(|err| {
-                tracing::error!("Failed to load keyset: {:?}", err);
-                err
-            })?;
-
-        // Verify keyset is for auth
-        if keyset.unit != CurrencyUnit::Auth {
-            tracing::warn!(
-                "Blind auth attempted with non-auth keyset. Found unit: {:?}",
-                keyset.unit
-            );
-            return Err(Error::BlindAuthFailed);
-        }
-
-        // Get the keypair for amount 1
-        let keypair = match keyset.keys.get(&Amount::from(1)) {
-            Some(key_pair) => key_pair,
-            None => {
-                tracing::error!("No keypair found for amount 1 in keyset {:?}", keyset_id);
-                return Err(Error::AmountKey);
-            }
-        };
-
-        // Verify the message
-        match verify_message(&keypair.secret_key, proof.c, proof.secret.as_bytes()) {
-            Ok(_) => {
-                tracing::trace!(
-                    "Blind signature verification successful for keyset ID: {:?}",
-                    keyset_id
-                );
-            }
-            Err(e) => {
-                tracing::error!("Blind signature verification failed: {:?}", e);
-                return Err(e.into());
-            }
-        }
-
-        Ok(())
     }
 
     /// Verify Auth
@@ -148,84 +48,92 @@ impl Mint {
         auth_token: Option<AuthToken>,
         endpoint: &ProtectedEndpoint,
     ) -> Result<(), Error> {
-        if let Some(auth_required) = self.is_protected(endpoint).await? {
+        let auth_required = if let Some(auth_required) = self.is_protected(endpoint).await? {
             tracing::info!(
                 "Auth required for endpoint: {:?}, type: {:?}",
                 endpoint,
                 auth_required
             );
+            auth_required
+        } else {
+            tracing::debug!("No auth required for endpoint: {:?}", endpoint);
+            return Ok(());
+        };
 
-            let auth_token = match auth_token {
-                Some(token) => token,
-                None => match auth_required {
-                    AuthRequired::Clear => {
-                        tracing::warn!(
-                            "No auth token provided for protected endpoint: {:?}, expected clear auth.",
-                            endpoint
-                        );
-                        return Err(Error::ClearAuthRequired);
-                    }
-                    AuthRequired::Blind => {
-                        tracing::warn!(
-                            "No auth token provided for protected endpoint: {:?}, expected blind auth.",
-                            endpoint
-                        );
-                        return Err(Error::BlindAuthRequired);
-                    }
-                },
-            };
+        tracing::info!(
+            "Auth required for endpoint: {:?}, type: {:?}",
+            endpoint,
+            auth_required
+        );
 
-            match (auth_required, auth_token) {
-                (AuthRequired::Clear, AuthToken::ClearAuth(token)) => {
-                    tracing::debug!("Verifying clear auth token");
-                    match self.verify_clear_auth(token.clone()).await {
-                        Ok(_) => tracing::info!("Clear auth verification successful"),
-                        Err(e) => {
-                            tracing::error!("Clear auth verification failed: {:?}", e);
-                            return Err(e);
-                        }
-                    }
-                }
-                (AuthRequired::Blind, AuthToken::BlindAuth(token)) => {
-                    tracing::debug!(
-                        "Verifying blind auth token with keyset_id: {:?}",
-                        token.auth_proof.keyset_id
+        let auth_token = match auth_token {
+            Some(token) => token,
+            None => match auth_required {
+                AuthRequired::Clear => {
+                    tracing::warn!(
+                        "No auth token provided for protected endpoint: {:?}, expected clear auth.",
+                        endpoint
                     );
-
-                    match self.verify_blind_auth(&token).await {
-                        Ok(_) => tracing::debug!("Blind auth signature verification successful"),
-                        Err(e) => {
-                            tracing::error!("Blind auth verification failed: {:?}", e);
-                            return Err(e);
-                        }
-                    }
-
-                    let auth_proof = token.auth_proof;
-
-                    self.check_blind_auth_proof_spendable(auth_proof)
-                        .await
-                        .map_err(|err| {
-                            tracing::error!("Failed to spend blind auth proof: {:?}", err);
-                            err
-                        })?;
+                    return Err(Error::ClearAuthRequired);
                 }
-                (AuthRequired::Blind, other) => {
+                AuthRequired::Blind => {
                     tracing::warn!(
-                        "Blind auth required but received different auth type: {:?}",
-                        other
+                        "No auth token provided for protected endpoint: {:?}, expected blind auth.",
+                        endpoint
                     );
                     return Err(Error::BlindAuthRequired);
                 }
-                (AuthRequired::Clear, other) => {
-                    tracing::warn!(
-                        "Clear auth required but received different auth type: {:?}",
-                        other
-                    );
-                    return Err(Error::ClearAuthRequired);
+            },
+        };
+
+        match (auth_required, auth_token) {
+            (AuthRequired::Clear, AuthToken::ClearAuth(token)) => {
+                tracing::debug!("Verifying clear auth token");
+                match self.verify_clear_auth(token.clone()).await {
+                    Ok(_) => tracing::info!("Clear auth verification successful"),
+                    Err(e) => {
+                        tracing::error!("Clear auth verification failed: {:?}", e);
+                        return Err(e);
+                    }
                 }
             }
-        } else {
-            tracing::debug!("No auth required for endpoint: {:?}", endpoint);
+            (AuthRequired::Blind, AuthToken::BlindAuth(token)) => {
+                tracing::debug!(
+                    "Verifying blind auth token with keyset_id: {:?}",
+                    token.auth_proof.keyset_id
+                );
+
+                match self.verify_blind_auth(&token).await {
+                    Ok(_) => tracing::debug!("Blind auth signature verification successful"),
+                    Err(e) => {
+                        tracing::error!("Blind auth verification failed: {:?}", e);
+                        return Err(e);
+                    }
+                }
+
+                let auth_proof = token.auth_proof;
+
+                self.check_blind_auth_proof_spendable(auth_proof)
+                    .await
+                    .map_err(|err| {
+                        tracing::error!("Failed to spend blind auth proof: {:?}", err);
+                        err
+                    })?;
+            }
+            (AuthRequired::Blind, other) => {
+                tracing::warn!(
+                    "Blind auth required but received different auth type: {:?}",
+                    other
+                );
+                return Err(Error::BlindAuthRequired);
+            }
+            (AuthRequired::Clear, other) => {
+                tracing::warn!(
+                    "Clear auth required but received different auth type: {:?}",
+                    other
+                );
+                return Err(Error::ClearAuthRequired);
+            }
         }
 
         tracing::debug!("Auth verification completed successfully");
@@ -306,108 +214,10 @@ impl Mint {
         &self,
         blinded_message: &BlindedMessage,
     ) -> Result<BlindSignature, Error> {
-        let BlindedMessage {
-            amount,
-            blinded_secret,
-            keyset_id,
-            ..
-        } = blinded_message;
-
-        // Ensure the keyset is loaded
-        let keyset = match self.ensure_blind_auth_keyset_loaded(keyset_id).await {
-            Ok(keyset) => keyset,
-            Err(e) => {
-                tracing::error!("Failed to load keyset: {:?}", e);
-                return Err(e);
-            }
-        };
-
-        // Get auth_localstore reference
-        let auth_localstore = match self.auth_localstore.as_ref() {
-            Some(store) => store,
-            None => {
-                tracing::error!("Auth localstore is not configured");
-                return Err(Error::AuthSettingsUndefined);
-            }
-        };
-
-        // Get keyset info
-        let keyset_info = match auth_localstore.get_keyset_info(keyset_id).await {
-            Ok(Some(info)) => info,
-            Ok(None) => {
-                tracing::error!("Keyset with ID {:?} not found in storage", keyset_id);
-                return Err(Error::UnknownKeySet);
-            }
-            Err(e) => {
-                tracing::error!("Error retrieving keyset info from storage: {:?}", e);
-                return Err(e.into());
-            }
-        };
-
-        // Get active keyset ID
-        let active = match auth_localstore.get_active_keyset_id().await {
-            Ok(Some(id)) => id,
-            Ok(None) => {
-                tracing::error!("No active keyset found");
-                return Err(Error::InactiveKeyset);
-            }
-            Err(e) => {
-                tracing::error!("Error retrieving active keyset ID: {:?}", e);
-                return Err(e.into());
-            }
-        };
-
-        // Check that the keyset is active and should be used to sign
-        if keyset_info.id.ne(&active) {
-            tracing::warn!(
-                "Keyset {:?} is not active. Active keyset is {:?}",
-                keyset_info.id,
-                active
-            );
-            return Err(Error::InactiveKeyset);
-        }
-
-        // Get the keypair for the specified amount
-        let key_pair = match keyset.keys.get(amount) {
-            Some(key_pair) => key_pair,
-            None => {
-                tracing::error!(
-                    "No keypair found for amount {:?} in keyset {:?}",
-                    amount,
-                    keyset_id
-                );
-                return Err(Error::AmountKey);
-            }
-        };
-
-        // Sign the message
-        let c = match sign_message(&key_pair.secret_key, blinded_secret) {
-            Ok(signature) => signature,
-            Err(e) => {
-                tracing::error!("Failed to sign message: {:?}", e);
-                return Err(e.into());
-            }
-        };
-
-        // Create blinded signature
-        let blinded_signature = match BlindSignature::new(
-            *amount,
-            c,
-            keyset_info.id,
-            &blinded_message.blinded_secret,
-            key_pair.secret_key.clone(),
-        ) {
-            Ok(sig) => sig,
-            Err(e) => {
-                tracing::error!("Failed to create blinded signature: {:?}", e);
-                return Err(e.into());
-            }
-        };
-
-        tracing::trace!(
-            "Blind signing completed successfully for keyset ID: {:?}",
-            keyset_id
-        );
-        Ok(blinded_signature)
+        self.signatory
+            .blind_sign(vec![blinded_message.to_owned()])
+            .await?
+            .pop()
+            .ok_or(Error::Internal)
     }
 }

+ 47 - 21
crates/cdk/src/mint/builder.rs

@@ -5,12 +5,13 @@ use std::sync::Arc;
 
 use anyhow::anyhow;
 use bitcoin::bip32::DerivationPath;
-use cdk_common::database::{self, MintDatabase};
+use cdk_common::database::{self, MintDatabase, MintKeysDatabase};
 use cdk_common::error::Error;
 use cdk_common::nut04::MintMethodOptions;
 use cdk_common::nut05::MeltMethodOptions;
 use cdk_common::payment::Bolt11Settings;
 use cdk_common::{nut21, nut22};
+use cdk_signatory::signatory::Signatory;
 
 use super::nut17::SupportedMethods;
 use super::nut19::{self, CachedEndpoint};
@@ -34,7 +35,9 @@ pub struct MintBuilder {
     /// Mint Info
     pub mint_info: MintInfo,
     /// Mint Storage backend
-    localstore: Option<Arc<dyn MintDatabase<database::Error> + Send + Sync>>,
+    pub localstore: Option<Arc<dyn MintDatabase<database::Error> + Send + Sync>>,
+    /// Database for the Signatory
+    keystore: Option<Arc<dyn MintKeysDatabase<Err = database::Error> + Send + Sync>>,
     /// Mint Storage backend
     #[cfg(feature = "auth")]
     auth_localstore: Option<Arc<dyn MintAuthDatabase<Err = cdk_database::Error> + Send + Sync>>,
@@ -47,6 +50,7 @@ pub struct MintBuilder {
     custom_paths: HashMap<CurrencyUnit, DerivationPath>,
     // protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
     openid_discovery: Option<String>,
+    signatory: Option<Arc<dyn Signatory + Sync + Send + 'static>>,
 }
 
 impl MintBuilder {
@@ -69,6 +73,18 @@ 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 seed
+    pub fn with_seed(mut self, seed: Vec<u8>) -> Self {
+        self.seed = Some(seed);
+        self
+    }
+
     /// Set localstore
     pub fn with_localstore(
         mut self,
@@ -78,6 +94,15 @@ impl MintBuilder {
         self
     }
 
+    /// Set keystore database
+    pub fn with_keystore(
+        mut self,
+        keystore: Arc<dyn MintKeysDatabase<Err = database::Error> + Send + Sync>,
+    ) -> MintBuilder {
+        self.keystore = Some(keystore);
+        self
+    }
+
     /// Set auth localstore
     #[cfg(feature = "auth")]
     pub fn with_auth_localstore(
@@ -88,18 +113,12 @@ impl MintBuilder {
         self
     }
 
-    /// Set Openid discovery url    
+    /// Set Openid discovery url
     pub fn with_openid_discovery(mut self, openid_discovery: String) -> Self {
         self.openid_discovery = Some(openid_discovery);
         self
     }
 
-    /// Set seed
-    pub fn with_seed(mut self, seed: Vec<u8>) -> Self {
-        self.seed = Some(seed);
-        self
-    }
-
     /// Set name
     pub fn with_name(mut self, name: String) -> Self {
         self.mint_info.name = Some(name);
@@ -319,9 +338,25 @@ impl MintBuilder {
             .localstore
             .clone()
             .ok_or(anyhow!("Localstore not set"))?;
-        let seed = self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?;
         let ln = self.ln.clone().ok_or(anyhow!("Ln backends not set"))?;
 
+        let signatory = if let Some(signatory) = self.signatory.as_ref() {
+            signatory.clone()
+        } else {
+            let seed = self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?;
+            let in_memory_signatory = cdk_signatory::db_signatory::DbSignatory::new(
+                self.keystore.clone().ok_or(anyhow!("keystore not set"))?,
+                seed,
+                self.supported_units.clone(),
+                HashMap::new(),
+            )
+            .await?;
+
+            Arc::new(cdk_signatory::embedded::Service::new(Arc::new(
+                in_memory_signatory,
+            )))
+        };
+
         #[cfg(feature = "auth")]
         if let Some(openid_discovery) = &self.openid_discovery {
             let auth_localstore = self
@@ -330,12 +365,10 @@ impl MintBuilder {
                 .ok_or(anyhow!("Auth localstore not set"))?;
 
             return Ok(Mint::new_with_auth(
-                seed,
+                signatory,
                 localstore,
                 auth_localstore,
                 ln,
-                self.supported_units.clone(),
-                self.custom_paths.clone(),
                 openid_discovery.clone(),
             )
             .await?);
@@ -348,14 +381,7 @@ impl MintBuilder {
             ));
         }
 
-        Ok(Mint::new(
-            seed,
-            localstore,
-            ln,
-            self.supported_units.clone(),
-            self.custom_paths.clone(),
-        )
-        .await?)
+        Ok(Mint::new(signatory, localstore, ln).await?)
     }
 }
 

+ 1 - 1
crates/cdk/src/mint/issue/issue_nut04.rs

@@ -309,7 +309,7 @@ impl Mint {
         let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
 
         for blinded_message in mint_request.outputs.iter() {
-            let blind_signature = self.blind_sign(blinded_message).await?;
+            let blind_signature = self.blind_sign(blinded_message.clone()).await?;
             blind_signatures.push(blind_signature);
         }
 

+ 31 - 46
crates/cdk/src/mint/keysets/auth.rs

@@ -1,65 +1,50 @@
 //! Auth keyset functions
 
+use cdk_common::{CurrencyUnit, KeySetInfo};
 use tracing::instrument;
 
-use crate::mint::{CurrencyUnit, Id, KeySetInfo, KeysResponse, KeysetResponse};
+use crate::mint::{KeysResponse, KeysetResponse};
 use crate::{Error, Mint};
 
 impl Mint {
     /// Retrieve the auth public keys of the active keyset for distribution to wallet
     /// clients
     #[instrument(skip_all)]
-    pub async fn auth_pubkeys(&self) -> Result<KeysResponse, Error> {
-        let active_keyset_id = self
-            .auth_localstore
-            .as_ref()
-            .ok_or(Error::AuthLocalstoreUndefined)?
-            .get_active_keyset_id()
-            .await?
-            .ok_or(Error::AmountKey)?;
-
-        self.ensure_blind_auth_keyset_loaded(&active_keyset_id)
-            .await?;
-
-        let keysets = self.keysets.read().await;
+    pub fn auth_pubkeys(&self) -> Result<KeysResponse, Error> {
+        let key = self
+            .keysets
+            .load()
+            .iter()
+            .find(|key| key.unit == CurrencyUnit::Auth)
+            .ok_or(Error::NoActiveKeyset)?
+            .clone();
 
         Ok(KeysResponse {
-            keysets: vec![keysets
-                .get(&active_keyset_id)
-                .ok_or(Error::KeysetUnknown(active_keyset_id))?
-                .clone()
-                .into()],
+            keysets: vec![key.into()],
         })
     }
 
     /// Return a list of auth keysets
     #[instrument(skip_all)]
-    pub async fn auth_keysets(&self) -> Result<KeysetResponse, Error> {
-        let keysets = self
-            .auth_localstore
-            .clone()
-            .ok_or(Error::AuthLocalstoreUndefined)?
-            .get_keyset_infos()
-            .await?;
-        let active_keysets: Id = self
-            .auth_localstore
-            .as_ref()
-            .ok_or(Error::AuthLocalstoreUndefined)?
-            .get_active_keyset_id()
-            .await?
-            .ok_or(Error::NoActiveKeyset)?;
-
-        let keysets = keysets
-            .into_iter()
-            .filter(|k| k.unit == CurrencyUnit::Auth)
-            .map(|k| KeySetInfo {
-                id: k.id,
-                unit: k.unit,
-                active: active_keysets == k.id,
-                input_fee_ppk: k.input_fee_ppk,
-            })
-            .collect();
-
-        Ok(KeysetResponse { keysets })
+    pub fn auth_keysets(&self) -> KeysetResponse {
+        KeysetResponse {
+            keysets: self
+                .keysets
+                .load()
+                .iter()
+                .filter_map(|key| {
+                    if key.unit == CurrencyUnit::Auth {
+                        Some(KeySetInfo {
+                            id: key.id,
+                            unit: key.unit.clone(),
+                            active: key.active,
+                            input_fee_ppk: key.input_fee_ppk,
+                        })
+                    } else {
+                        None
+                    }
+                })
+                .collect(),
+        }
     }
 }

+ 48 - 268
crates/cdk/src/mint/keysets/mod.rs

@@ -1,15 +1,8 @@
-use std::collections::{HashMap, HashSet};
-use std::sync::Arc;
-
-use bitcoin::bip32::{DerivationPath, Xpriv};
-use bitcoin::key::Secp256k1;
-use bitcoin::secp256k1::All;
-use cdk_common::database::{self, MintDatabase};
+use cdk_signatory::signatory::RotateKeyArguments;
 use tracing::instrument;
 
 use super::{
-    create_new_keyset, derivation_path_from_unit, CurrencyUnit, Id, KeySet, KeySetInfo,
-    KeysResponse, KeysetResponse, Mint, MintKeySet, MintKeySetInfo,
+    CurrencyUnit, Id, KeySet, KeySetInfo, KeysResponse, KeysetResponse, Mint, MintKeySetInfo,
 };
 use crate::Error;
 
@@ -17,298 +10,85 @@ use crate::Error;
 mod auth;
 
 impl Mint {
-    /// Initialize keysets and returns a [`Result`] with a tuple of the following:
-    /// * a [`HashMap`] mapping each active keyset `Id` to `MintKeySet`
-    /// * a [`Vec`] of `CurrencyUnit` containing active keysets units
-    pub async fn init_keysets(
-        xpriv: Xpriv,
-        secp_ctx: &Secp256k1<All>,
-        localstore: &Arc<dyn MintDatabase<database::Error> + Send + Sync>,
-        supported_units: &HashMap<CurrencyUnit, (u64, u8)>,
-        custom_paths: &HashMap<CurrencyUnit, DerivationPath>,
-    ) -> Result<(HashMap<Id, MintKeySet>, Vec<CurrencyUnit>), Error> {
-        let mut active_keysets: HashMap<Id, MintKeySet> = HashMap::new();
-        let mut active_keyset_units: Vec<CurrencyUnit> = vec![];
-
-        // Get keysets info from DB
-        let keysets_infos = localstore.get_keyset_infos().await?;
-
-        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));
-
-                // Get the keyset with the highest counter
-                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) {
-                    if !keysets.is_empty()
-                        && &highest_index_keyset.input_fee_ppk == input_fee_ppk
-                        && &highest_index_keyset.max_order == max_order
-                    {
-                        tracing::debug!("Current highest index keyset matches expect fee and max order. Setting active");
-                        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?;
-                        active_keyset_units.push(unit.clone());
-                        localstore.set_active_keyset(unit, id).await?;
-                    } else {
-                        // Check to see if there are not keysets by this unit
-                        let derivation_path_index = if keysets.is_empty() {
-                            1
-                        } 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());
-                    };
-                }
-            }
-        }
-
-        Ok((active_keysets, active_keyset_units))
-    }
-
     /// Retrieve the public keys of the active keyset for distribution to wallet
     /// 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()],
-        })
+    pub fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<KeysResponse, Error> {
+        self.keysets
+            .load()
+            .iter()
+            .find(|keyset| &keyset.id == keyset_id)
+            .ok_or(Error::UnknownKeySet)
+            .map(|key| KeysResponse {
+                keysets: vec![key.into()],
+            })
     }
 
     /// 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 mut active_keysets = self.localstore.get_active_keysets().await?;
-
-        // We don't want to return auth keys here even though in the db we treat them the same
-        active_keysets.remove(&CurrencyUnit::Auth);
-
-        let active_keysets: HashSet<&Id> = active_keysets.values().collect();
-
-        for id in active_keysets.iter() {
-            self.ensure_keyset_loaded(id).await?;
-        }
-
-        Ok(KeysResponse {
+    pub fn pubkeys(&self) -> KeysResponse {
+        KeysResponse {
             keysets: self
                 .keysets
-                .read()
-                .await
-                .values()
-                .filter_map(|k| match active_keysets.contains(&k.id) {
-                    true => Some(k.clone().into()),
-                    false => None,
-                })
-                .collect(),
-        })
+                .load()
+                .iter()
+                .filter(|keyset| keyset.active && keyset.unit != CurrencyUnit::Auth)
+                .map(|key| key.into())
+                .collect::<Vec<_>>(),
+        }
     }
 
     /// 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()
-            .filter(|k| k.unit != CurrencyUnit::Auth)
-            .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 })
+    pub fn keysets(&self) -> KeysetResponse {
+        KeysetResponse {
+            keysets: self
+                .keysets
+                .load()
+                .iter()
+                .filter(|k| k.unit != CurrencyUnit::Auth)
+                .map(|k| KeySetInfo {
+                    id: k.id,
+                    unit: k.unit.clone(),
+                    active: k.active,
+                    input_fee_ppk: k.input_fee_ppk,
+                })
+                .collect(),
+        }
     }
 
     /// 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)
+    pub fn keyset(&self, id: &Id) -> Option<KeySet> {
+        self.keysets
+            .load()
+            .iter()
+            .find(|key| &key.id == id)
+            .map(|x| x.into())
     }
 
     /// Add current keyset to inactive keysets
     /// Generate new keyset
-    #[instrument(skip(self, custom_paths))]
-    pub 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.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)
-    }
-
-    /// Rotate to next keyset for unit
     #[instrument(skip(self))]
-    pub async fn rotate_next_keyset(
+    pub async fn rotate_keyset(
         &self,
         unit: CurrencyUnit,
         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)?;
-
-        tracing::debug!(
-            "Current active keyset {} path index {:?}",
-            keyset_info.id,
-            keyset_info.derivation_path_index
-        );
-
-        let keyset_info = self
-            .rotate_keyset(
+        let result = self
+            .signatory
+            .rotate_keyset(RotateKeyArguments {
                 unit,
-                keyset_info.derivation_path_index.unwrap_or(1) + 1,
                 max_order,
                 input_fee_ppk,
-                &self.custom_paths,
-            )
+            })
             .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));
+        let new_keyset = self.signatory.keysets().await?;
+        self.keysets.store(new_keyset.keysets.into());
 
-        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,
-        )
+        Ok(result.into())
     }
 }

+ 1 - 1
crates/cdk/src/mint/melt.rs

@@ -701,7 +701,7 @@ impl Mint {
                 for (amount, blinded_message) in amounts.iter().zip(&mut outputs) {
                     blinded_message.amount = *amount;
 
-                    let blinded_signature = self.blind_sign(blinded_message).await?;
+                    let blinded_signature = self.blind_sign(blinded_message.clone()).await?;
                     change_sigs.push(blinded_signature)
                 }
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 149 - 324
crates/cdk/src/mint/mod.rs


+ 13 - 3
crates/cdk/src/mint/swap.rs

@@ -3,7 +3,7 @@ use tracing::instrument;
 use super::nut11::{enforce_sig_flag, EnforceSigFlag};
 use super::{Mint, PublicKey, SigFlag, State, SwapRequest, SwapResponse};
 use crate::nuts::nut00::ProofsMethods;
-use crate::Error;
+use crate::{cdk_database, Error};
 
 impl Mint {
     /// Process Swap
@@ -32,13 +32,23 @@ impl Mint {
         let mut promises = Vec::with_capacity(swap_request.outputs().len());
 
         for blinded_message in swap_request.outputs() {
-            let blinded_signature = self.blind_sign(blinded_message).await?;
+            let blinded_signature = self.blind_sign(blinded_message.clone()).await?;
             promises.push(blinded_signature);
         }
 
+        // TODO: It may be possible to have a race condition, that's why an error when changing the
+        // state can be converted to a TokenAlreadySpent error.
+        //
+        // A concept of transaction/writer for the Database trait would eliminate this problem and
+        // will remove all the "reset" codebase, resulting in fewer lines of code, and less
+        // error-prone database updates
         self.localstore
             .update_proofs_states(&input_ys, State::Spent)
-            .await?;
+            .await
+            .map_err(|e| match e {
+                cdk_database::Error::AttemptUpdateSpentProof => Error::TokenAlreadySpent,
+                e => e.into(),
+            })?;
 
         for pub_key in input_ys {
             self.pubsub_manager.proof_state((pub_key, State::Spent));

+ 3 - 5
crates/cdk/src/mint/verification.rs

@@ -66,7 +66,7 @@ impl Mint {
         let output_keyset_ids: HashSet<Id> = outputs.iter().map(|p| p.keyset_id).collect();
 
         for id in &output_keyset_ids {
-            match self.localstore.get_keyset_info(id).await? {
+            match self.get_keyset_info(id) {
                 Some(keyset) => {
                     if !keyset.active {
                         tracing::debug!(
@@ -114,7 +114,7 @@ impl Mint {
         let inputs_keyset_ids: HashSet<Id> = inputs.iter().map(|p| p.keyset_id).collect();
 
         for id in &inputs_keyset_ids {
-            match self.localstore.get_keyset_info(id).await? {
+            match self.get_keyset_info(id) {
                 Some(keyset) => {
                     keyset_units.insert(keyset.unit);
                 }
@@ -203,9 +203,7 @@ impl Mint {
         let unit = self.verify_inputs_keyset(inputs).await?;
         let amount = inputs.total_amount()?;
 
-        for proof in inputs {
-            self.verify_proof(proof).await?;
-        }
+        self.verify_proofs(inputs.clone()).await?;
 
         Ok(Verification {
             amount,

+ 3 - 1
justfile

@@ -57,12 +57,13 @@ test-pure db="memory": build
   fi
 
   # Run pure integration tests
-  CDK_TEST_DB_TYPE={{db}} cargo test -p cdk-integration-tests --test integration_tests_pure
+  CDK_TEST_DB_TYPE={{db}} cargo test -p cdk-integration-tests --test integration_tests_pure -- --test-threads 1
 
 test-all db="memory":
     #!/usr/bin/env bash
     just test {{db}}
     ./misc/itests.sh "{{db}}"
+    ./misc/fake_itests.sh "{{db}}" external_signatory
     ./misc/fake_itests.sh "{{db}}"
     
 test-nutshell:
@@ -119,6 +120,7 @@ itest db:
   
 fake-mint-itest db:
   #!/usr/bin/env bash
+  ./misc/fake_itests.sh "{{db}}" external_signatory
   ./misc/fake_itests.sh "{{db}}"
 
   

+ 14 - 4
misc/fake_itests.sh

@@ -7,13 +7,15 @@ cleanup() {
     echo "Killing the cdk mintd"
     kill -2 $CDK_MINTD_PID
     wait $CDK_MINTD_PID
+    kill -9 $CDK_SIGNATORY_PID
+    wait $CDK_SIGNATORY_PID
 
     echo "Mint binary terminated"
-    
+
     # Remove the temporary directory
     rm -rf "$CDK_ITESTS_DIR"
     echo "Temp directory removed: $CDK_ITESTS_DIR"
-    
+
     # Unset all environment variables
     unset CDK_ITESTS_DIR
     unset CDK_ITESTS_MINT_ADDR
@@ -49,7 +51,7 @@ fi
 echo "Temp directory created: $CDK_ITESTS_DIR"
 export CDK_MINTD_DATABASE="$1"
 
-cargo build -p cdk-integration-tests 
+cargo build -p cdk-integration-tests
 
 
 export CDK_MINTD_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT"
@@ -62,6 +64,14 @@ export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt lugg
 export CDK_MINTD_FAKE_WALLET_FEE_PERCENT="0"
 export CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN="1"
 
+if [ "$2" = "external_signatory" ]; then
+    export CDK_MINTD_SIGNATORY_URL="https://127.0.0.1:15060"
+    export CDK_MINTD_SIGNATORY_CERTS="$CDK_ITESTS_DIR"
+    bash -x `dirname $0`/../crates/cdk-signatory/generate_certs.sh $CDK_ITESTS_DIR
+    cargo run --bin signatory -- -w $CDK_ITESTS_DIR -u "sat" -u "usd"  &
+    export CDK_SIGNATORY_PID=$!
+    sleep 5
+fi
 
 echo "Starting fake mintd"
 cargo run --bin cdk-mintd --features "redb" &
@@ -74,7 +84,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))
 

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels