Преглед на файлове

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.
Cesar Rodas преди 1 месец
родител
ревизия
15e60ef274

+ 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.0" }
 cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.9.0" }
 cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.9.0" }
+cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.9.0" }
 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(

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

+ 3 - 3
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -527,7 +527,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, 1, 32, 1)
         .await
         .unwrap();
 
@@ -597,7 +597,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, 1, 32, 1)
         .await
         .unwrap();
 
@@ -689,7 +689,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, 1, 32, 1)
         .await
         .unwrap();
 

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

@@ -0,0 +1,24 @@
+[package]
+name = "cdk-signatory"
+version = "0.9.0"
+edition = "2021"
+description = "CDK signatory default implementation"
+
+[features]
+default = []
+grpc = ["dep:tonic", "tokio/full", "dep:prost", "dep:tonic-build"]
+
+[dependencies]
+async-trait = "0.1.83"
+cashu.workspace = true
+bitcoin.workspace = true
+cdk-common = { workspace = true, default-features=false, features = [
+    "mint",
+] }
+tracing.workspace = true
+tokio.workspace = true
+tonic = { workspace = true, optional = true }
+prost = { workspace = true, optional = true }
+
+[build-dependencies]
+tonic-build = { workspace = true, features = ["prost"], optional = true }

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

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

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

@@ -0,0 +1,157 @@
+syntax = "proto3";
+
+package signatory;
+
+service Signatory {
+    rpc BlindSign(BlindedMessage) returns (BlindSignature);
+
+    rpc VerifyProof(Proof) returns (Empty);
+
+    rpc AuthKeysets(Empty) returns (VecSignatoryKeySet);
+
+    rpc Keysets(Empty) returns (VecSignatoryKeySet);
+
+    rpc RotateKeyset(RotateKeyArguments) returns (MintKeySetInfo);
+}
+
+message Empty {}
+
+message VecSignatoryKeySet {
+    repeated SignatoryKeySet keysets = 1;
+
+    optional bool is_none = 2;
+}
+
+message SignatoryKeySet {
+    KeySet key = 1;
+    MintKeySetInfo info = 2;
+}
+
+message KeySet {
+    Id id = 1;
+    CurrencyUnit unit = 2;
+    Keys keys = 3;
+}
+
+message Keys {
+    map<uint64, bytes> keys = 1;
+}
+
+
+message RotateKeyArguments {
+    CurrencyUnit unit = 1;
+    uint32 derivation_path_index = 2;
+    uint32 max_order = 3;
+    uint64 input_fee_ppk = 4;
+}
+
+message CustomDerivationPath {
+    CurrencyUnit unit = 1;
+    repeated DerivationPath derivation_path = 2;
+}
+
+enum CurrencyUnitType {
+  CURRENCY_UNIT_SAT = 0;
+  CURRENCY_UNIT_MSAT = 1;
+  CURRENCY_UNIT_USD = 2;
+  CURRENCY_UNIT_EUR = 3;
+}
+
+message CurrencyUnit {
+    oneof currency_unit {
+        CurrencyUnitType unit = 1;
+        string custom_unit = 2;
+    }
+}
+
+message Proof {
+    uint64 amount = 1;
+    string keyset_id = 2;
+    string secret = 3;
+    bytes C = 4;
+    optional Witness witness = 5;
+    optional ProofDLEQ dleq = 6;
+}
+
+message ProofDLEQ {
+    bytes e = 1;
+    bytes s = 2;
+    bytes r = 3;
+}
+
+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;
+}
+
+message KeySetInfo {
+    Id id = 1;
+    CurrencyUnit unit = 2;
+    bool active = 3;
+    uint64 input_fee_ppk = 4;
+}
+
+// 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;
+}
+
+message BlindedMessage {
+    uint64 amount = 1;
+    string keyset_id = 2;
+    bytes blinded_secret = 3;
+    optional Witness witness = 4; // This field is optional by default in proto3
+}
+
+message KeysResponse {
+    repeated KeySet keysets = 1;
+}
+
+
+message Id {
+    bytes inner = 1;
+}
+
+message DerivationPath {
+    oneof child_number {
+        uint32 normal = 1;
+        uint32 hardened = 2;
+    }
+}
+
+message MintKeySetInfo {
+    Id id = 1;
+    CurrencyUnit unit = 2;
+    bool active = 3;
+    uint64 valid_from = 4;
+    optional uint64 valid_to = 5;
+    repeated DerivationPath derivation_path = 6;
+    optional uint32 derivation_path_index = 7;
+    uint32 max_order = 8;
+    uint64 input_fee_ppk = 9;
+}

+ 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::{self, MintDatabase};
+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 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))
+}
+
+/// 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"),
+    ]))
+}

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

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

+ 435 - 0
crates/cdk-signatory/src/memory.rs

@@ -0,0 +1,435 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use bitcoin::bip32::{DerivationPath, Xpriv};
+use bitcoin::secp256k1::{self, Secp256k1};
+use cdk_common::amount::Amount;
+use cdk_common::database::{self, MintDatabase};
+use cdk_common::dhke::{sign_message, verify_message};
+use cdk_common::error::Error;
+use cdk_common::mint::MintKeySetInfo;
+use cdk_common::nuts::nut01::MintKeyPair;
+use cdk_common::nuts::{
+    self, BlindSignature, BlindedMessage, CurrencyUnit, Id, Kind, MintKeySet, Proof,
+};
+use cdk_common::secret;
+use tokio::sync::RwLock;
+
+use crate::common::{create_new_keyset, derivation_path_from_unit, init_keysets};
+use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet};
+
+/// 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 Memory {
+    keysets: RwLock<HashMap<Id, (MintKeySetInfo, MintKeySet)>>,
+    localstore: Arc<dyn MintDatabase<database::Error> + Send + Sync>,
+    auth_localstore:
+        Option<Arc<dyn database::MintAuthDatabase<Err = database::Error> + Send + Sync>>,
+    secp_ctx: Secp256k1<secp256k1::All>,
+    custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    xpriv: Xpriv,
+}
+
+impl Memory {
+    /// Creates a new MemorySignatory instance
+    pub async fn new(
+        localstore: Arc<dyn MintDatabase<database::Error> + Send + Sync>,
+        auth_localstore: Option<
+            Arc<dyn database::MintAuthDatabase<Err = database::Error> + Send + Sync>,
+        >,
+        seed: &[u8],
+        supported_units: HashMap<CurrencyUnit, (u64, u8)>,
+        custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    ) -> Result<Self, Error> {
+        let secp_ctx = Secp256k1::new();
+        let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
+
+        let (mut active_keysets, active_keyset_units) = init_keysets(
+            xpriv,
+            &secp_ctx,
+            &localstore,
+            &supported_units,
+            &custom_paths,
+        )
+        .await?;
+
+        if let Some(auth_localstore) = auth_localstore.as_ref() {
+            tracing::info!("Auth enabled creating auth keysets");
+            let derivation_path = match custom_paths.get(&CurrencyUnit::Auth) {
+                Some(path) => path.clone(),
+                None => derivation_path_from_unit(CurrencyUnit::Auth, 0)
+                    .ok_or(Error::UnsupportedUnit)?,
+            };
+
+            let (keyset, keyset_info) = create_new_keyset(
+                &secp_ctx,
+                xpriv,
+                derivation_path,
+                Some(0),
+                CurrencyUnit::Auth,
+                1,
+                0,
+            );
+
+            let id = keyset_info.id;
+            auth_localstore.add_keyset_info(keyset_info).await?;
+            auth_localstore.set_active_keyset(id).await?;
+            active_keysets.insert(id, keyset);
+        }
+
+        // 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);
+            }
+        }
+
+        Ok(Self {
+            keysets: RwLock::new(HashMap::new()),
+            auth_localstore,
+            secp_ctx,
+            localstore,
+            custom_paths,
+            xpriv,
+        })
+    }
+}
+
+impl Memory {
+    fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet {
+        MintKeySet::generate_from_xpriv(
+            &self.secp_ctx,
+            self.xpriv,
+            keyset_info.max_order,
+            keyset_info.unit,
+            keyset_info.derivation_path,
+        )
+    }
+
+    async fn load_and_get_keyset(&self, id: &Id) -> Result<MintKeySetInfo, Error> {
+        let keysets = self.keysets.read().await;
+        let keyset_info = if let Some(info) = self.localstore.get_keyset_info(id).await? {
+            info
+        } else {
+            let auth_localstore = self.auth_localstore.as_ref().ok_or(Error::UnknownKeySet)?;
+            let keyset_info = auth_localstore
+                .get_keyset_info(id)
+                .await?
+                .ok_or(Error::UnknownKeySet)?;
+
+            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);
+            }
+
+            keyset_info
+        };
+
+        if keysets.contains_key(id) {
+            return Ok(keyset_info);
+        }
+        drop(keysets);
+
+        let id = keyset_info.id;
+        let mut keysets = self.keysets.write().await;
+        keysets.insert(
+            id,
+            (
+                keyset_info.clone(),
+                self.generate_keyset(keyset_info.clone()),
+            ),
+        );
+        Ok(keyset_info)
+    }
+
+    #[tracing::instrument(skip(self))]
+    async fn get_keypair_for_amount(
+        &self,
+        keyset_id: &Id,
+        amount: &Amount,
+    ) -> Result<MintKeyPair, Error> {
+        let keyset_info = self.load_and_get_keyset(keyset_id).await?;
+        let active = self
+            .localstore
+            .get_active_keyset_id(&keyset_info.unit)
+            .await?
+            .ok_or(Error::InactiveKeyset)?;
+
+        // Check that the keyset is active and should be used to sign
+        if keyset_info.id != active {
+            return Err(Error::InactiveKeyset);
+        }
+
+        let keysets = self.keysets.read().await;
+        let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?;
+
+        match keyset.1.keys.get(amount) {
+            Some(key_pair) => Ok(key_pair.clone()),
+            None => Err(Error::AmountKey),
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl Signatory for Memory {
+    async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result<BlindSignature, Error> {
+        let BlindedMessage {
+            amount,
+            blinded_secret,
+            keyset_id,
+            ..
+        } = blinded_message;
+        let key_pair = self.get_keypair_for_amount(&keyset_id, &amount).await?;
+        let c = sign_message(&key_pair.secret_key, &blinded_secret)?;
+
+        let blinded_signature = BlindSignature::new(
+            amount,
+            c,
+            keyset_id,
+            &blinded_message.blinded_secret,
+            key_pair.secret_key,
+        )?;
+
+        Ok(blinded_signature)
+    }
+
+    async fn verify_proof(&self, proof: Proof) -> Result<(), Error> {
+        // Check if secret is a nut10 secret with conditions
+        if let Ok(secret) =
+            <&secret::Secret as TryInto<nuts::nut10::Secret>>::try_into(&proof.secret)
+        {
+            // Checks and verifies known secret kinds.
+            // If it is an unknown secret kind it will be treated as a normal secret.
+            // Spending conditions will **not** be check. It is up to the wallet to ensure
+            // only supported secret kinds are used as there is no way for the mint to
+            // enforce only signing supported secrets as they are blinded at
+            // that point.
+            match secret.kind {
+                Kind::P2PK => {
+                    proof.verify_p2pk()?;
+                }
+                Kind::HTLC => {
+                    proof.verify_htlc()?;
+                }
+            }
+        }
+
+        let key_pair = self
+            .get_keypair_for_amount(&proof.keyset_id, &proof.amount)
+            .await?;
+
+        verify_message(&key_pair.secret_key, proof.c, proof.secret.as_bytes())?;
+
+        Ok(())
+    }
+
+    async fn auth_keysets(&self) -> Result<Option<Vec<SignatoryKeySet>>, Error> {
+        let db = if let Some(db) = self.auth_localstore.as_ref() {
+            db.clone()
+        } else {
+            return Ok(None);
+        };
+
+        let keyset_id: Id = db
+            .get_active_keyset_id()
+            .await?
+            .ok_or(Error::NoActiveKeyset)?;
+
+        _ = self.load_and_get_keyset(&keyset_id).await?;
+
+        let active_keyset = self
+            .keysets
+            .read()
+            .await
+            .get(&keyset_id)
+            .ok_or(Error::UnknownKeySet)?
+            .into();
+
+        Ok(Some(vec![active_keyset]))
+    }
+
+    async fn keysets(&self) -> Result<Vec<SignatoryKeySet>, Error> {
+        for (_, id) in self.localstore.get_active_keysets().await? {
+            let _ = self.load_and_get_keyset(&id).await?;
+        }
+
+        Ok(self
+            .keysets
+            .read()
+            .await
+            .values()
+            .filter_map(|k| match k.0.active {
+                true => Some(k.into()),
+                false => None,
+            })
+            .collect::<Vec<_>>())
+    }
+
+    /// Add current keyset to inactive keysets
+    /// Generate new keyset
+    #[tracing::instrument(skip(self))]
+    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<MintKeySetInfo, Error> {
+        let derivation_path = match self.custom_paths.get(&args.unit) {
+            Some(path) => path.clone(),
+            None => derivation_path_from_unit(args.unit.clone(), args.derivation_path_index)
+                .ok_or(Error::UnsupportedUnit)?,
+        };
+
+        let (keyset, keyset_info) = create_new_keyset(
+            &self.secp_ctx,
+            self.xpriv,
+            derivation_path,
+            Some(args.derivation_path_index),
+            args.unit.clone(),
+            args.max_order,
+            args.input_fee_ppk,
+        );
+        let id = keyset_info.id;
+        self.localstore.add_keyset_info(keyset_info.clone()).await?;
+        self.localstore.set_active_keyset(args.unit, id).await?;
+
+        let mut keysets = self.keysets.write().await;
+        keysets.insert(id, (keyset_info.clone(), keyset));
+
+        Ok(keyset_info)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::collections::HashSet;
+
+    use bitcoin::key::Secp256k1;
+    use bitcoin::Network;
+    use cdk_common::MintKeySet;
+    use nuts::PublicKey;
+
+    use super::*;
+
+    #[test]
+    fn mint_mod_generate_keyset_from_seed() {
+        let seed = "test_seed".as_bytes();
+        let keyset = MintKeySet::generate_from_seed(
+            &Secp256k1::new(),
+            seed,
+            2,
+            CurrencyUnit::Sat,
+            derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
+        );
+
+        assert_eq!(keyset.unit, CurrencyUnit::Sat);
+        assert_eq!(keyset.keys.len(), 2);
+
+        let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![
+            (
+                Amount::from(1),
+                PublicKey::from_hex(
+                    "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e",
+                )
+                .unwrap(),
+            ),
+            (
+                Amount::from(2),
+                PublicKey::from_hex(
+                    "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e",
+                )
+                .unwrap(),
+            ),
+        ]
+        .into_iter()
+        .collect();
+
+        let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset
+            .keys
+            .iter()
+            .map(|(amount, pair)| (*amount, pair.public_key))
+            .collect();
+
+        assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys);
+    }
+
+    #[test]
+    fn mint_mod_generate_keyset_from_xpriv() {
+        let seed = "test_seed".as_bytes();
+        let network = Network::Bitcoin;
+        let xpriv = Xpriv::new_master(network, seed).expect("Failed to create xpriv");
+        let keyset = MintKeySet::generate_from_xpriv(
+            &Secp256k1::new(),
+            xpriv,
+            2,
+            CurrencyUnit::Sat,
+            derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
+        );
+
+        assert_eq!(keyset.unit, CurrencyUnit::Sat);
+        assert_eq!(keyset.keys.len(), 2);
+
+        let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![
+            (
+                Amount::from(1),
+                PublicKey::from_hex(
+                    "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e",
+                )
+                .unwrap(),
+            ),
+            (
+                Amount::from(2),
+                PublicKey::from_hex(
+                    "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e",
+                )
+                .unwrap(),
+            ),
+        ]
+        .into_iter()
+        .collect();
+
+        let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset
+            .keys
+            .iter()
+            .map(|(amount, pair)| (*amount, pair.public_key))
+            .collect();
+
+        assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys);
+    }
+}

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

@@ -0,0 +1,91 @@
+use cdk_common::error::Error;
+use cdk_common::mint::MintKeySetInfo;
+use cdk_common::{BlindSignature, BlindedMessage, Proof};
+
+use crate::proto::signatory_client::SignatoryClient;
+use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet};
+
+/// A client for the Signatory service.
+pub struct SignatoryRpcClient {
+    client: SignatoryClient<tonic::transport::Channel>,
+}
+
+impl SignatoryRpcClient {
+    /// Create a new RemoteSigner from a tonic transport channel.
+    pub async fn new(url: String) -> Result<Self, tonic::transport::Error> {
+        Ok(Self {
+            client: SignatoryClient::connect(url).await?,
+        })
+    }
+}
+
+#[async_trait::async_trait]
+impl Signatory for SignatoryRpcClient {
+    async fn blind_sign(&self, request: BlindedMessage) -> Result<BlindSignature, Error> {
+        let req: super::BlindedMessage = request.into();
+        self.client
+            .clone()
+            .blind_sign(req)
+            .await
+            .map(|response| response.into_inner().try_into())
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+
+    async fn verify_proof(&self, proof: Proof) -> Result<(), Error> {
+        let req: super::Proof = proof.into();
+        self.client
+            .clone()
+            .verify_proof(req)
+            .await
+            .map(|response| response.into_inner().try_into())
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+
+    async fn auth_keysets(&self) -> Result<Option<Vec<SignatoryKeySet>>, Error> {
+        self.client
+            .clone()
+            .auth_keysets(super::Empty {})
+            .await
+            .map(|response| {
+                let response = response.into_inner();
+
+                if response.is_none == Some(true) {
+                    Ok(None)
+                } else {
+                    response
+                        .keysets
+                        .into_iter()
+                        .map(|x| x.try_into())
+                        .collect::<Result<Vec<SignatoryKeySet>, _>>()
+                        .map(Some)
+                }
+            })
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+
+    async fn keysets(&self) -> Result<Vec<SignatoryKeySet>, Error> {
+        self.client
+            .clone()
+            .keysets(super::Empty {})
+            .await
+            .map(|response| {
+                response
+                    .into_inner()
+                    .keysets
+                    .into_iter()
+                    .map(|x| x.try_into())
+                    .collect::<Result<Vec<SignatoryKeySet>, _>>()
+            })
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+
+    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<MintKeySetInfo, Error> {
+        let req: super::RotateKeyArguments = args.into();
+        self.client
+            .clone()
+            .rotate_keyset(req)
+            .await
+            .map(|response| response.into_inner().try_into())
+            .map_err(|e| Error::Custom(e.to_string()))?
+    }
+}

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

@@ -0,0 +1,504 @@
+//! Type conversions between Rust types and the generated protobuf types.
+use std::collections::BTreeMap;
+use std::str::FromStr;
+
+use cashu::secret::Secret;
+use cdk_common::{HTLCWitness, P2PKWitness};
+use tonic::Status;
+
+use super::*;
+
+impl From<cashu::Id> for Id {
+    fn from(value: cashu::Id) -> Self {
+        Id {
+            inner: value.to_bytes().to_vec(),
+        }
+    }
+}
+
+impl TryInto<cashu::Id> for Id {
+    type Error = cdk_common::error::Error;
+
+    fn try_into(self) -> Result<cashu::Id, Self::Error> {
+        Ok(cashu::Id::from_bytes(&self.inner)?)
+    }
+}
+
+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<crate::signatory::SignatoryKeySet> for SignatoryKeySet {
+    fn from(value: crate::signatory::SignatoryKeySet) -> Self {
+        SignatoryKeySet {
+            key: Some(value.key.into()),
+            info: Some(value.info.into()),
+        }
+    }
+}
+
+impl TryInto<crate::signatory::SignatoryKeySet> for SignatoryKeySet {
+    type Error = cdk_common::error::Error;
+
+    fn try_into(self) -> Result<crate::signatory::SignatoryKeySet, Self::Error> {
+        Ok(crate::signatory::SignatoryKeySet {
+            key: self
+                .key
+                .ok_or(cdk_common::Error::RecvError(
+                    "Missing property key".to_owned(),
+                ))?
+                .try_into()?,
+            info: self
+                .info
+                .ok_or(cdk_common::Error::RecvError(
+                    "Missing property info".to_owned(),
+                ))?
+                .try_into()?,
+        })
+    }
+}
+
+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<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_string(),
+            c: value.c.to_bytes().to_vec(),
+            witness: value.witness.map(|w| w.into()),
+            dleq: value.dleq.map(|dleq| dleq.into()),
+        }
+    }
+}
+
+impl TryInto<cdk_common::Proof> for Proof {
+    type Error = Status;
+    fn try_into(self) -> Result<cdk_common::Proof, Self::Error> {
+        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::from_str(&self.secret).map_err(|e| Status::from_error(Box::new(e)))?,
+            c: cdk_common::PublicKey::from_slice(&self.c)
+                .map_err(|e| Status::from_error(Box::new(e)))?,
+            witness: self.witness.map(|w| w.try_into()).transpose()?,
+            dleq: self.dleq.map(|x| x.try_into()).transpose()?,
+        })
+    }
+}
+
+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(),
+            witness: value.witness.map(|x| x.into()),
+        }
+    }
+}
+
+impl TryInto<cdk_common::BlindedMessage> for BlindedMessage {
+    type Error = Status;
+    fn try_into(self) -> Result<cdk_common::BlindedMessage, Self::Error> {
+        Ok(cdk_common::BlindedMessage {
+            amount: self.amount.into(),
+            keyset_id: self
+                .keyset_id
+                .parse()
+                .map_err(|e| Status::from_error(Box::new(e)))?,
+            blinded_secret: cdk_common::PublicKey::from_slice(&self.blinded_secret)
+                .map_err(|e| Status::from_error(Box::new(e)))?,
+            witness: self.witness.map(|x| x.try_into()).transpose()?,
+        })
+    }
+}
+
+impl From<cdk_common::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 Empty {
+    fn from(_: ()) -> Self {
+        Empty {}
+    }
+}
+
+impl TryInto<()> for Empty {
+    type Error = cdk_common::error::Error;
+
+    fn try_into(self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
+
+impl From<cashu::CurrencyUnit> for CurrencyUnit {
+    fn from(value: cashu::CurrencyUnit) -> Self {
+        match value {
+            cashu::CurrencyUnit::Sat => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::CurrencyUnitSat.into(),
+                )),
+            },
+            cashu::CurrencyUnit::Msat => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::CurrencyUnitMsat.into(),
+                )),
+            },
+            cashu::CurrencyUnit::Usd => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::CurrencyUnitUsd.into(),
+                )),
+            },
+            cashu::CurrencyUnit::Eur => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::Unit(
+                    CurrencyUnitType::CurrencyUnitEur.into(),
+                )),
+            },
+            cashu::CurrencyUnit::Custom(name) => CurrencyUnit {
+                currency_unit: Some(currency_unit::CurrencyUnit::CustomUnit(name)),
+            },
+            _ => unreachable!(),
+        }
+    }
+}
+
+impl TryInto<cashu::CurrencyUnit> for CurrencyUnit {
+    type Error = Status;
+
+    fn try_into(self) -> Result<cashu::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::CurrencyUnitSat => Ok(cashu::CurrencyUnit::Sat),
+                CurrencyUnitType::CurrencyUnitMsat => Ok(cashu::CurrencyUnit::Msat),
+                CurrencyUnitType::CurrencyUnitUsd => Ok(cashu::CurrencyUnit::Usd),
+                CurrencyUnitType::CurrencyUnitEur => Ok(cashu::CurrencyUnit::Eur),
+            },
+            Some(currency_unit::CurrencyUnit::CustomUnit(name)) => {
+                Ok(cashu::CurrencyUnit::Custom(name))
+            }
+            None => Err(Status::invalid_argument("Currency unit not set")),
+        }
+    }
+}
+
+impl From<&bitcoin::bip32::ChildNumber> for derivation_path::ChildNumber {
+    fn from(value: &bitcoin::bip32::ChildNumber) -> Self {
+        match value {
+            bitcoin::bip32::ChildNumber::Normal { index } => {
+                derivation_path::ChildNumber::Normal(*index)
+            }
+            bitcoin::bip32::ChildNumber::Hardened { index } => {
+                derivation_path::ChildNumber::Hardened(*index)
+            }
+        }
+    }
+}
+
+impl TryInto<bitcoin::bip32::ChildNumber> for derivation_path::ChildNumber {
+    type Error = cdk_common::error::Error;
+
+    fn try_into(self) -> Result<bitcoin::bip32::ChildNumber, Self::Error> {
+        Ok(match self {
+            derivation_path::ChildNumber::Normal(index) => {
+                bitcoin::bip32::ChildNumber::Normal { index }
+            }
+            derivation_path::ChildNumber::Hardened(index) => {
+                bitcoin::bip32::ChildNumber::Hardened { index }
+            }
+        })
+    }
+}
+
+impl From<cdk_common::mint::MintKeySetInfo> for MintKeySetInfo {
+    fn from(value: cdk_common::mint::MintKeySetInfo) -> Self {
+        Self {
+            id: Some(value.id.into()),
+            unit: Some(value.unit.into()),
+            active: value.active,
+            valid_from: value.valid_from,
+            valid_to: value.valid_to,
+            derivation_path: value
+                .derivation_path
+                .into_iter()
+                .map(|x| DerivationPath {
+                    child_number: Some(x.into()),
+                })
+                .collect(),
+            derivation_path_index: value.derivation_path_index,
+            max_order: value.max_order.into(),
+            input_fee_ppk: value.input_fee_ppk,
+        }
+    }
+}
+
+impl TryInto<cdk_common::mint::MintKeySetInfo> for MintKeySetInfo {
+    type Error = cdk_common::error::Error;
+
+    fn try_into(self) -> Result<cdk_common::mint::MintKeySetInfo, Self::Error> {
+        Ok(cdk_common::mint::MintKeySetInfo {
+            id: self
+                .id
+                .ok_or(cdk_common::error::Error::Custom("id not set".to_owned()))?
+                .try_into()?,
+            unit: self
+                .unit
+                .ok_or(cdk_common::error::Error::Custom("unit not set".to_owned()))?
+                .try_into()
+                .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
+            active: self.active,
+            valid_from: self.valid_from,
+            valid_to: self.valid_to,
+            max_order: self
+                .max_order
+                .try_into()
+                .map_err(|_| cdk_common::Error::Custom("Invalid max_order".to_owned()))?,
+            input_fee_ppk: self.input_fee_ppk,
+            derivation_path: self
+                .derivation_path
+                .into_iter()
+                .map(|derivation_path| {
+                    derivation_path
+                        .child_number
+                        .ok_or(cdk_common::error::Error::Custom(
+                            "child_number not set".to_owned(),
+                        ))?
+                        .try_into()
+                })
+                .collect::<Result<Vec<bitcoin::bip32::ChildNumber>, _>>()?
+                .into(),
+            derivation_path_index: self.derivation_path_index,
+        })
+    }
+}
+
+impl From<cashu::KeySet> for KeySet {
+    fn from(value: cashu::KeySet) -> Self {
+        Self {
+            id: Some(value.id.into()),
+            unit: Some(value.unit.into()),
+            keys: Some(Keys {
+                keys: value
+                    .keys
+                    .iter()
+                    .map(|(amount, pk)| (*(amount.as_ref()), pk.to_bytes().to_vec()))
+                    .collect(),
+            }),
+        }
+    }
+}
+
+impl TryInto<cashu::KeySet> for KeySet {
+    type Error = cdk_common::error::Error;
+    fn try_into(self) -> Result<cashu::KeySet, Self::Error> {
+        Ok(cashu::KeySet {
+            id: self
+                .id
+                .ok_or(cdk_common::error::Error::Custom("id not set".to_owned()))?
+                .try_into()?,
+            unit: self
+                .unit
+                .ok_or(cdk_common::error::Error::Custom("unit not set".to_owned()))?
+                .try_into()
+                .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
+            keys: cashu::Keys::new(
+                self.keys
+                    .ok_or(cdk_common::error::Error::Custom("keys not set".to_owned()))?
+                    .keys
+                    .into_iter()
+                    .map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk)))
+                    .collect::<Result<BTreeMap<cashu::Amount, cdk_common::PublicKey>, _>>()?,
+            ),
+        })
+    }
+}
+
+impl From<cashu::KeysResponse> for KeysResponse {
+    fn from(value: cashu::KeysResponse) -> Self {
+        Self {
+            keysets: value.keysets.into_iter().map(|x| x.into()).collect(),
+        }
+    }
+}
+
+impl TryInto<cashu::KeysResponse> for KeysResponse {
+    type Error = cdk_common::error::Error;
+
+    fn try_into(self) -> Result<cashu::KeysResponse, Self::Error> {
+        Ok(cashu::KeysResponse {
+            keysets: self
+                .keysets
+                .into_iter()
+                .map(|x| x.try_into())
+                .collect::<Result<Vec<cashu::KeySet>, _>>()?,
+        })
+    }
+}
+
+impl From<crate::signatory::RotateKeyArguments> for RotateKeyArguments {
+    fn from(value: crate::signatory::RotateKeyArguments) -> Self {
+        Self {
+            unit: Some(value.unit.into()),
+            derivation_path_index: value.derivation_path_index,
+            max_order: value.max_order.into(),
+            input_fee_ppk: value.input_fee_ppk,
+        }
+    }
+}
+
+impl TryInto<crate::signatory::RotateKeyArguments> for RotateKeyArguments {
+    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()?,
+            derivation_path_index: self.derivation_path_index,
+            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 KeySetInfo {
+    fn from(value: cdk_common::KeySetInfo) -> Self {
+        Self {
+            id: Some(value.id.into()),
+            unit: Some(value.unit.into()),
+            active: value.active,
+            input_fee_ppk: value.input_fee_ppk,
+        }
+    }
+}
+
+impl TryInto<cdk_common::KeySetInfo> for KeySetInfo {
+    type Error = cdk_common::Error;
+
+    fn try_into(self) -> Result<cdk_common::KeySetInfo, Self::Error> {
+        Ok(cdk_common::KeySetInfo {
+            id: self
+                .id
+                .ok_or(cdk_common::Error::Custom("id not set".to_owned()))?
+                .try_into()?,
+            unit: self
+                .unit
+                .ok_or(cdk_common::Error::Custom("unit not set".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;

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

@@ -0,0 +1,107 @@
+use std::net::SocketAddr;
+
+use tonic::transport::{Error, Server};
+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,
+{
+    async fn blind_sign(
+        &self,
+        request: Request<proto::BlindedMessage>,
+    ) -> Result<Response<proto::BlindSignature>, Status> {
+        let blind_signature = self
+            .inner
+            .blind_sign(request.into_inner().try_into()?)
+            .await
+            .map_err(|e| Status::from_error(Box::new(e)))?;
+        Ok(Response::new(blind_signature.into()))
+    }
+
+    async fn verify_proof(
+        &self,
+        request: Request<proto::Proof>,
+    ) -> Result<Response<proto::Empty>, Status> {
+        self.inner
+            .verify_proof(request.into_inner().try_into()?)
+            .await
+            .map_err(|e| Status::from_error(Box::new(e)))?;
+        Ok(Response::new(proto::Empty {}))
+    }
+
+    async fn auth_keysets(
+        &self,
+        _request: Request<proto::Empty>,
+    ) -> Result<Response<proto::VecSignatoryKeySet>, Status> {
+        let keys_response = self
+            .inner
+            .auth_keysets()
+            .await
+            .map_err(|e| Status::from_error(Box::new(e)))?;
+        Ok(Response::new(if let Some(keys_response) = keys_response {
+            proto::VecSignatoryKeySet {
+                keysets: keys_response.into_iter().map(|k| k.into()).collect(),
+                is_none: Some(false),
+            }
+        } else {
+            proto::VecSignatoryKeySet {
+                keysets: vec![],
+                is_none: Some(true),
+            }
+        }))
+    }
+
+    async fn keysets(
+        &self,
+        _request: Request<proto::Empty>,
+    ) -> Result<Response<proto::VecSignatoryKeySet>, Status> {
+        let keys_response = self
+            .inner
+            .keysets()
+            .await
+            .map_err(|e| Status::from_error(Box::new(e)))?;
+        Ok(Response::new(proto::VecSignatoryKeySet {
+            keysets: keys_response.into_iter().map(|k| k.into()).collect(),
+            is_none: Some(false),
+        }))
+    }
+
+    async fn rotate_keyset(
+        &self,
+        request: Request<proto::RotateKeyArguments>,
+    ) -> Result<Response<proto::MintKeySetInfo>, Status> {
+        let mint_keyset_info = self
+            .inner
+            .rotate_keyset(request.into_inner().try_into()?)
+            .await
+            .map_err(|e| Status::from_error(Box::new(e)))?;
+        Ok(Response::new(mint_keyset_info.into()))
+    }
+}
+
+/// Runs the signatory server
+pub async fn grpc_server<T>(signatory: T, addr: SocketAddr) -> Result<(), Error>
+where
+    T: Signatory + Send + Sync + 'static,
+{
+    tracing::info!("grpc_server listening on {}", addr);
+    Server::builder()
+        .add_service(signatory_server::SignatoryServer::new(CdkSignatoryServer {
+            inner: signatory,
+        }))
+        .serve(addr)
+        .await?;
+    Ok(())
+}

+ 150 - 0
crates/cdk-signatory/src/service.rs

@@ -0,0 +1,150 @@
+use std::sync::Arc;
+
+use cashu::{BlindSignature, BlindedMessage, Proof};
+use cdk_common::error::Error;
+use cdk_common::mint::MintKeySetInfo;
+use tokio::sync::{mpsc, oneshot};
+use tokio::task::JoinHandle;
+
+use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet};
+
+enum Request {
+    BlindSign(
+        (
+            BlindedMessage,
+            oneshot::Sender<Result<BlindSignature, Error>>,
+        ),
+    ),
+    VerifyProof((Proof, oneshot::Sender<Result<(), Error>>)),
+    AuthKeysets(oneshot::Sender<Result<Option<Vec<SignatoryKeySet>>, Error>>),
+    Keysets(oneshot::Sender<Result<Vec<SignatoryKeySet>, Error>>),
+    RotateKeyset(
+        (
+            RotateKeyArguments,
+            oneshot::Sender<Result<MintKeySetInfo, 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 {
+    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,
+        }
+    }
+
+    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_proof(proof).await;
+                    if let Err(err) = response.send(output) {
+                        tracing::error!("Error sending response: {:?}", err);
+                    }
+                }
+                Request::AuthKeysets(response) => {
+                    let output = handler.auth_keysets().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 {
+    async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result<BlindSignature, Error> {
+        let (tx, rx) = oneshot::channel();
+        self.pipeline
+            .send(Request::BlindSign((blinded_message, tx)))
+            .await
+            .map_err(|e| Error::SendError(e.to_string()))?;
+
+        rx.await.map_err(|e| Error::RecvError(e.to_string()))?
+    }
+
+    async fn verify_proof(&self, proof: Proof) -> Result<(), Error> {
+        let (tx, rx) = oneshot::channel();
+        self.pipeline
+            .send(Request::VerifyProof((proof, tx)))
+            .await
+            .map_err(|e| Error::SendError(e.to_string()))?;
+
+        rx.await.map_err(|e| Error::RecvError(e.to_string()))?
+    }
+
+    async fn auth_keysets(&self) -> Result<Option<Vec<SignatoryKeySet>>, Error> {
+        let (tx, rx) = oneshot::channel();
+        self.pipeline
+            .send(Request::AuthKeysets(tx))
+            .await
+            .map_err(|e| Error::SendError(e.to_string()))?;
+
+        rx.await.map_err(|e| Error::RecvError(e.to_string()))?
+    }
+
+    async fn keysets(&self) -> Result<Vec<SignatoryKeySet>, 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()))?
+    }
+
+    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<MintKeySetInfo, 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()))?
+    }
+}

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

@@ -0,0 +1,83 @@
+//! 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 cashu::{BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, MintKeySet, Proof};
+use cdk_common::error::Error;
+use cdk_common::mint::MintKeySetInfo;
+
+#[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)
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct RotateKeyArguments {
+    pub unit: CurrencyUnit,
+    pub derivation_path_index: u32,
+    pub max_order: u8,
+    pub input_fee_ppk: u64,
+}
+
+#[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 {
+    /// KeySet
+    pub key: KeySet,
+    /// MintSetInfo
+    pub info: MintKeySetInfo,
+}
+
+impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
+    fn from((info, key): &(MintKeySetInfo, MintKeySet)) -> Self {
+        Self {
+            key: key.clone().into(),
+            info: info.clone(),
+        }
+    }
+}
+
+#[async_trait::async_trait]
+/// Signatory trait
+pub trait Signatory {
+    /// Get all the mint keysets for authentication
+    async fn auth_keysets(&self) -> Result<Option<Vec<SignatoryKeySet>>, Error>;
+
+    /// Blind sign a message.
+    ///
+    /// The message can be for a coin or an auth token.
+    async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result<BlindSignature, Error>;
+
+    /// Verify [`Proof`] meets conditions and is signed
+    async fn verify_proof(&self, proofs: Proof) -> Result<(), Error>;
+
+    /// Retrieve the list of all mint keysets
+    async fn keysets(&self) -> Result<Vec<SignatoryKeySet>, Error>;
+
+    /// Add current keyset to inactive keysets
+    /// Generate new keyset
+    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<MintKeySetInfo, Error>;
+}

+ 3 - 1
crates/cdk/Cargo.toml

@@ -11,17 +11,19 @@ license.workspace = true
 
 
 [features]
-default = ["mint", "wallet", "auth"]
+default = ["mint", "wallet", "auth", "grpc"]
 wallet = ["dep:reqwest", "cdk-common/wallet"]
 mint = ["dep:futures", "dep:reqwest", "cdk-common/mint"]
 auth = ["dep:jsonwebtoken", "cdk-common/auth", "cdk-common/auth"]
 # We do not commit to a MSRV with swagger enabled
 swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
 bench = []
+grpc = ["cdk-signatory/grpc"]
 http_subscription = []
 
 
 [dependencies]
+cdk-signatory.workspace = true
 cdk-common.workspace = true
 cbor-diag.workspace = true
 async-trait.workspace = true

+ 75 - 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_proof(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,6 @@ 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(blinded_message.to_owned()).await
     }
 }

+ 35 - 19
crates/cdk/src/mint/builder.rs

@@ -9,6 +9,7 @@ use cdk_common::database::{self, MintDatabase};
 use cdk_common::error::Error;
 use cdk_common::payment::Bolt11Settings;
 use cdk_common::{nut21, nut22};
+use cdk_signatory::signatory::Signatory;
 
 use super::nut17::SupportedMethods;
 use super::nut19::{self, CachedEndpoint};
@@ -45,6 +46,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 {
@@ -67,6 +69,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,
@@ -86,18 +100,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);
@@ -313,9 +321,26 @@ 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::memory::Memory::new(
+                localstore.clone(),
+                None,
+                seed,
+                self.supported_units.clone(),
+                HashMap::new(),
+            )
+            .await?;
+
+            Arc::new(cdk_signatory::service::Service::new(Arc::new(
+                in_memory_signatory,
+            )))
+        };
+
         #[cfg(feature = "auth")]
         if let Some(openid_discovery) = &self.openid_discovery {
             let auth_localstore = self
@@ -324,12 +349,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?);
@@ -342,14 +365,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?)
     }
 }
 

+ 13 - 41
crates/cdk/src/mint/keysets/auth.rs

@@ -2,7 +2,7 @@
 
 use tracing::instrument;
 
-use crate::mint::{CurrencyUnit, Id, KeySetInfo, KeysResponse, KeysetResponse};
+use crate::mint::{KeysResponse, KeysetResponse};
 use crate::{Error, Mint};
 
 impl Mint {
@@ -10,56 +10,28 @@ impl Mint {
     /// 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()
+        let key = self
+            .signatory
+            .auth_keysets()
             .await?
-            .ok_or(Error::AmountKey)?;
-
-        self.ensure_blind_auth_keyset_loaded(&active_keyset_id)
-            .await?;
-
-        let keysets = self.keysets.read().await;
+            .ok_or(Error::AuthLocalstoreUndefined)?
+            .pop()
+            .ok_or(Error::AuthLocalstoreUndefined)?;
 
         Ok(KeysResponse {
-            keysets: vec![keysets
-                .get(&active_keyset_id)
-                .ok_or(Error::KeysetUnknown(active_keyset_id))?
-                .clone()
-                .into()],
+            keysets: vec![key.key],
         })
     }
 
     /// 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()
+        self.signatory
+            .auth_keysets()
             .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,
+            .map(|all_keysets| KeysetResponse {
+                keysets: all_keysets.into_iter().map(|k| k.info.into()).collect(),
             })
-            .collect();
-
-        Ok(KeysetResponse { keysets })
+            .ok_or(Error::AuthLocalstoreUndefined)
     }
 }

+ 34 - 208
crates/cdk/src/mint/keysets/mod.rs

@@ -1,15 +1,10 @@
-use std::collections::{HashMap, HashSet};
-use std::sync::Arc;
+use std::collections::HashSet;
 
-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,152 +12,33 @@ 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()],
-        })
+        self.signatory
+            .keysets()
+            .await?
+            .into_iter()
+            .find(|keyset| &keyset.key.id == keyset_id)
+            .ok_or(Error::UnknownKeySet)
+            .map(|key| KeysResponse {
+                keysets: vec![key.key],
+            })
     }
 
     /// 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 {
             keysets: self
-                .keysets
-                .read()
-                .await
-                .values()
-                .filter_map(|k| match active_keysets.contains(&k.id) {
-                    true => Some(k.clone().into()),
-                    false => None,
-                })
-                .collect(),
+                .signatory
+                .keysets()
+                .await?
+                .into_iter()
+                .map(|key| key.key)
+                .collect::<Vec<_>>(),
         })
     }
 
@@ -195,48 +71,33 @@ impl Mint {
     /// 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)
+        Ok(self
+            .signatory
+            .keysets()
+            .await?
+            .into_iter()
+            .find(|key| &key.key.id == id)
+            .map(|x| x.key))
     }
 
     /// Add current keyset to inactive keysets
     /// Generate new keyset
-    #[instrument(skip(self, custom_paths))]
+    #[instrument(skip(self))]
     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)
+        self.signatory
+            .rotate_keyset(RotateKeyArguments {
+                unit,
+                derivation_path_index,
+                max_order,
+                input_fee_ppk,
+            })
+            .await
     }
 
     /// Rotate to next keyset for unit
@@ -271,44 +132,9 @@ impl Mint {
                 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));
-
-        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,
-        )
-    }
 }

Файловите разлики са ограничени, защото са твърде много
+ 36 - 309
crates/cdk/src/mint/mod.rs


Някои файлове не бяха показани, защото твърде много файлове са промени