Browse Source

Introduce a SignatoryManager service.

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

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

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

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

+ 1 - 0
Cargo.lock

@@ -745,6 +745,7 @@ dependencies = [
  "instant",
  "lightning-invoice",
  "once_cell",
+ "paste",
  "rand",
  "regex",
  "reqwest",

+ 12 - 5
crates/cdk-integration-tests/src/init_regtest.rs

@@ -8,7 +8,7 @@ use axum::Router;
 use bip39::Mnemonic;
 use cdk::cdk_database::{self, MintDatabase};
 use cdk::cdk_lightning::MintLightning;
-use cdk::mint::{FeeReserve, Mint};
+use cdk::mint::{FeeReserve, MemorySignatory, Mint};
 use cdk::nuts::{CurrencyUnit, MintInfo};
 use cdk::types::{LnKey, QuoteTTL};
 use cdk_cln::Cln as CdkCln;
@@ -173,16 +173,23 @@ where
     supported_units.insert(CurrencyUnit::Sat, (0, 32));
 
     let quote_ttl = QuoteTTL::new(10000, 10000);
+    let db = Arc::new(database);
+    let signatory = MemorySignatory::new(
+        db.clone(),
+        &mnemonic.to_seed_normalized(""),
+        supported_units,
+        HashMap::new(),
+    )
+    .await
+    .expect("valid signatory");
 
     let mint = Mint::new(
         &get_mint_url(),
-        &mnemonic.to_seed_normalized(""),
         mint_info,
         quote_ttl,
-        Arc::new(database),
+        db,
         ln_backends,
-        supported_units,
-        HashMap::new(),
+        Arc::new(signatory.into()),
     )
     .await?;
 

+ 14 - 5
crates/cdk-integration-tests/src/lib.rs

@@ -9,7 +9,7 @@ use cdk::amount::{Amount, SplitTarget};
 use cdk::cdk_database::mint_memory::MintMemoryDatabase;
 use cdk::cdk_lightning::MintLightning;
 use cdk::dhke::construct_proofs;
-use cdk::mint::FeeReserve;
+use cdk::mint::{FeeReserve, MemorySignatory};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut17::Params;
 use cdk::nuts::{
@@ -75,15 +75,24 @@ pub async fn start_mint(
 
     let quote_ttl = QuoteTTL::new(10000, 10000);
 
+    let db = Arc::new(MintMemoryDatabase::default());
+
+    let signatory = MemorySignatory::new(
+        db.clone(),
+        &mnemonic.to_seed_normalized(""),
+        supported_units,
+        HashMap::new(),
+    )
+    .await
+    .expect("valid signatory");
+
     let mint = Mint::new(
         &get_mint_url(),
-        &mnemonic.to_seed_normalized(""),
         mint_info,
         quote_ttl,
-        Arc::new(MintMemoryDatabase::default()),
+        db,
         ln_backends.clone(),
-        supported_units,
-        HashMap::new(),
+        Arc::new(signatory.into()),
     )
     .await?;
 

+ 8 - 5
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -10,6 +10,7 @@ mod integration_tests_pure {
     use cdk::amount::SplitTarget;
     use cdk::cdk_database::mint_memory::MintMemoryDatabase;
     use cdk::cdk_database::WalletMemoryDatabase;
+    use cdk::mint::MemorySignatory;
     use cdk::nuts::{
         CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse,
         MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
@@ -161,17 +162,19 @@ mod integration_tests_pure {
         let quote_ttl = QuoteTTL::new(10000, 10000);
 
         let mint_url = "http://aaa";
-
+        let db = Arc::new(MintMemoryDatabase::default());
         let seed = random::<[u8; 32]>();
+        let signatory = MemorySignatory::new(db.clone(), &seed, supported_units, HashMap::new())
+            .await
+            .expect("valid signatory");
+
         let mint: Mint = Mint::new(
             mint_url,
-            &seed,
             mint_info,
             quote_ttl,
-            Arc::new(MintMemoryDatabase::default()),
+            db,
             create_backends_fake_wallet(),
-            supported_units,
-            HashMap::new(),
+            Arc::new(signatory.into()),
         )
         .await?;
 

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

@@ -9,7 +9,7 @@ use bip39::Mnemonic;
 use cdk::amount::{Amount, SplitTarget};
 use cdk::cdk_database::mint_memory::MintMemoryDatabase;
 use cdk::dhke::construct_proofs;
-use cdk::mint::MintQuote;
+use cdk::mint::{MemorySignatory, MintQuote};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::nut17::Params;
 use cdk::nuts::{
@@ -45,15 +45,23 @@ async fn new_mint(fee: u64) -> Mint {
 
     let quote_ttl = QuoteTTL::new(10000, 10000);
 
+    let db = Arc::new(MintMemoryDatabase::default());
+    let signatory = MemorySignatory::new(
+        db.clone(),
+        &mnemonic.to_seed_normalized(""),
+        supported_units,
+        HashMap::new(),
+    )
+    .await
+    .expect("valid signatory");
+
     Mint::new(
         MINT_URL,
-        &mnemonic.to_seed_normalized(""),
         mint_info,
         quote_ttl,
-        Arc::new(MintMemoryDatabase::default()),
-        HashMap::new(),
-        supported_units,
+        db,
         HashMap::new(),
+        Arc::new(signatory.into()),
     )
     .await
     .unwrap()

+ 1 - 0
crates/cdk/Cargo.toml

@@ -60,6 +60,7 @@ uuid = { version = "1", features = ["v4", "serde"] }
 # -Z minimal-versions
 sync_wrapper = "0.1.2"
 bech32 = "0.9.1"
+paste = "1.0.15"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 tokio = { version = "1.21", features = [

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

@@ -49,6 +49,14 @@ pub enum Error {
     #[error("Signature missing or invalid")]
     SignatureMissingOrInvalid,
 
+    /// 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")]

+ 10 - 5
crates/cdk/src/mint/builder.rs

@@ -7,7 +7,7 @@ use anyhow::anyhow;
 
 use super::nut17::SupportedMethods;
 use super::nut19::{self, CachedEndpoint};
-use super::Nuts;
+use super::{Nuts, SignatoryManager};
 use crate::amount::Amount;
 use crate::cdk_database::{self, MintDatabase};
 use crate::cdk_lightning::{self, MintLightning};
@@ -31,6 +31,7 @@ pub struct MintBuilder {
     ln: Option<HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>>,
     seed: Option<Vec<u8>>,
     quote_ttl: Option<QuoteTTL>,
+    signatory: Option<SignatoryManager>,
     supported_units: HashMap<CurrencyUnit, (u64, u8)>,
 }
 
@@ -54,6 +55,12 @@ impl MintBuilder {
         builder
     }
 
+    /// Set signatory
+    pub fn with_signatory(mut self, signatory: SignatoryManager) -> MintBuilder {
+        self.signatory = Some(signatory);
+        self
+    }
+
     /// Set localstore
     pub fn with_localstore(
         mut self,
@@ -227,18 +234,16 @@ impl MintBuilder {
     }
 
     /// Build mint
-    pub async fn build(&self) -> anyhow::Result<Mint> {
+    pub async fn build(self) -> anyhow::Result<Mint> {
         Ok(Mint::new(
             self.mint_url.as_ref().ok_or(anyhow!("Mint url not set"))?,
-            self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?,
             self.mint_info.clone(),
             self.quote_ttl.ok_or(anyhow!("Quote ttl not set"))?,
             self.localstore
                 .clone()
                 .ok_or(anyhow!("Localstore not set"))?,
             self.ln.clone().ok_or(anyhow!("Ln backends not set"))?,
-            self.supported_units.clone(),
-            HashMap::new(),
+            Arc::new(self.signatory.ok_or(anyhow!("Signatory not set"))?),
         )
         .await?)
     }

+ 2 - 27
crates/cdk/src/mint/config.rs

@@ -1,19 +1,16 @@
 //! Active mint configuration
 //!
 //! This is the active configuration that can be updated at runtime.
-use std::collections::HashMap;
 use std::sync::Arc;
 
 use arc_swap::ArcSwap;
 
-use super::{Id, MintInfo, MintKeySet};
+use super::MintInfo;
 use crate::mint_url::MintUrl;
 use crate::types::QuoteTTL;
 
 /// Mint Inner configuration
 pub struct Config {
-    /// Active Mint Keysets
-    pub keysets: HashMap<Id, MintKeySet>,
     /// Mint url
     pub mint_info: MintInfo,
     /// Mint config
@@ -36,14 +33,8 @@ pub struct SwappableConfig {
 
 impl SwappableConfig {
     /// Creates a new configuration instance
-    pub fn new(
-        mint_url: MintUrl,
-        quote_ttl: QuoteTTL,
-        mint_info: MintInfo,
-        keysets: HashMap<Id, MintKeySet>,
-    ) -> Self {
+    pub fn new(mint_url: MintUrl, quote_ttl: QuoteTTL, mint_info: MintInfo) -> Self {
         let inner = Config {
-            keysets,
             quote_ttl,
             mint_info,
             mint_url,
@@ -71,7 +62,6 @@ impl SwappableConfig {
             mint_url,
             quote_ttl: current_inner.quote_ttl,
             mint_info: current_inner.mint_info.clone(),
-            keysets: current_inner.keysets.clone(),
         };
 
         self.config.store(Arc::new(new_inner));
@@ -89,7 +79,6 @@ impl SwappableConfig {
             mint_info: current_inner.mint_info.clone(),
             mint_url: current_inner.mint_url.clone(),
             quote_ttl,
-            keysets: current_inner.keysets.clone(),
         };
 
         self.config.store(Arc::new(new_inner));
@@ -107,20 +96,6 @@ impl SwappableConfig {
             mint_info,
             mint_url: current_inner.mint_url.clone(),
             quote_ttl: current_inner.quote_ttl,
-            keysets: current_inner.keysets.clone(),
-        };
-
-        self.config.store(Arc::new(new_inner));
-    }
-
-    /// Replaces the current keysets with a new one
-    pub fn set_keysets(&self, keysets: HashMap<Id, MintKeySet>) {
-        let current_inner = self.load();
-        let new_inner = Config {
-            mint_info: current_inner.mint_info.clone(),
-            quote_ttl: current_inner.quote_ttl,
-            mint_url: current_inner.mint_url.clone(),
-            keysets,
         };
 
         self.config.store(Arc::new(new_inner));

+ 15 - 117
crates/cdk/src/mint/keysets.rs

@@ -1,12 +1,9 @@
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
 
 use bitcoin::bip32::DerivationPath;
 use tracing::instrument;
 
-use super::{
-    create_new_keyset, derivation_path_from_unit, CurrencyUnit, Id, KeySet, KeySetInfo,
-    KeysResponse, KeysetResponse, Mint, MintKeySet, MintKeySetInfo,
-};
+use super::{CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Mint};
 use crate::Error;
 
 impl Mint {
@@ -14,78 +11,26 @@ impl Mint {
     /// clients
     #[instrument(skip(self))]
     pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<KeysResponse, Error> {
-        self.ensure_keyset_loaded(keyset_id).await?;
-        let keyset = self
-            .config
-            .load()
-            .keysets
-            .get(keyset_id)
-            .ok_or(Error::UnknownKeySet)?
-            .clone();
-        Ok(KeysResponse {
-            keysets: vec![keyset.into()],
-        })
+        self.signatory.keyset_pubkeys(keyset_id.to_owned()).await
     }
 
     /// Retrieve the public keys of the active keyset for distribution to wallet
     /// clients
     #[instrument(skip_all)]
     pub async fn pubkeys(&self) -> Result<KeysResponse, Error> {
-        let active_keysets = self.localstore.get_active_keysets().await?;
-
-        let active_keysets: HashSet<&Id> = active_keysets.values().collect();
-
-        for id in active_keysets.iter() {
-            self.ensure_keyset_loaded(id).await?;
-        }
-
-        Ok(KeysResponse {
-            keysets: self
-                .config
-                .load()
-                .keysets
-                .values()
-                .filter_map(|k| match active_keysets.contains(&k.id) {
-                    true => Some(k.clone().into()),
-                    false => None,
-                })
-                .collect(),
-        })
+        self.signatory.pubkeys().await
     }
 
     /// Return a list of all supported keysets
     #[instrument(skip_all)]
     pub async fn keysets(&self) -> Result<KeysetResponse, Error> {
-        let keysets = self.localstore.get_keyset_infos().await?;
-        let active_keysets: HashSet<Id> = self
-            .localstore
-            .get_active_keysets()
-            .await?
-            .values()
-            .cloned()
-            .collect();
-
-        let keysets = keysets
-            .into_iter()
-            .map(|k| KeySetInfo {
-                id: k.id,
-                unit: k.unit,
-                active: active_keysets.contains(&k.id),
-                input_fee_ppk: k.input_fee_ppk,
-            })
-            .collect();
-
-        Ok(KeysetResponse { keysets })
+        self.signatory.keysets().await
     }
 
     /// Get keysets
     #[instrument(skip(self))]
     pub async fn keyset(&self, id: &Id) -> Result<Option<KeySet>, Error> {
-        self.ensure_keyset_loaded(id).await?;
-        let config = self.config.load();
-        let keysets = &config.keysets;
-        let keyset = keysets.get(id).map(|k| k.clone().into());
-        Ok(keyset)
+        self.signatory.keyset(id.to_owned()).await
     }
 
     /// Add current keyset to inactive keysets
@@ -99,61 +44,14 @@ impl Mint {
         input_fee_ppk: u64,
         custom_paths: HashMap<CurrencyUnit, DerivationPath>,
     ) -> Result<(), Error> {
-        let derivation_path = match custom_paths.get(&unit) {
-            Some(path) => path.clone(),
-            None => derivation_path_from_unit(unit.clone(), derivation_path_index)
-                .ok_or(Error::UnsupportedUnit)?,
-        };
-
-        let (keyset, keyset_info) = create_new_keyset(
-            &self.secp_ctx,
-            self.xpriv,
-            derivation_path,
-            Some(derivation_path_index),
-            unit.clone(),
-            max_order,
-            input_fee_ppk,
-        );
-        let id = keyset_info.id;
-        self.localstore.add_keyset_info(keyset_info).await?;
-        self.localstore.set_active_keyset(unit, id).await?;
-
-        let mut keysets = self.config.load().keysets.clone();
-        keysets.insert(id, keyset);
-        self.config.set_keysets(keysets);
-
-        Ok(())
-    }
-
-    /// Ensure Keyset is loaded in mint
-    #[instrument(skip(self))]
-    pub async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> {
-        if self.config.load().keysets.contains_key(id) {
-            return Ok(());
-        }
-
-        let mut keysets = self.config.load().keysets.clone();
-        let keyset_info = self
-            .localstore
-            .get_keyset_info(id)
-            .await?
-            .ok_or(Error::UnknownKeySet)?;
-        let id = keyset_info.id;
-        keysets.insert(id, self.generate_keyset(keyset_info));
-        self.config.set_keysets(keysets);
-
-        Ok(())
-    }
-
-    /// Generate [`MintKeySet`] from [`MintKeySetInfo`]
-    #[instrument(skip_all)]
-    pub fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet {
-        MintKeySet::generate_from_xpriv(
-            &self.secp_ctx,
-            self.xpriv,
-            keyset_info.max_order,
-            keyset_info.unit,
-            keyset_info.derivation_path,
-        )
+        self.signatory
+            .rotate_keyset(
+                unit,
+                derivation_path_index,
+                max_order,
+                input_fee_ppk,
+                custom_paths,
+            )
+            .await
     }
 }

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


+ 39 - 0
crates/cdk/src/mint/signatory/common.rs

@@ -0,0 +1,39 @@
+use bitcoin::bip32::{DerivationPath, Xpriv};
+use bitcoin::secp256k1;
+use tracing::instrument;
+
+use crate::mint::{CurrencyUnit, MintKeySet, MintKeySetInfo};
+use crate::util::unix_time;
+
+/// Generate new [`MintKeySetInfo`] from path
+#[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)
+}

+ 117 - 0
crates/cdk/src/mint/signatory/manager.rs

@@ -0,0 +1,117 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use bitcoin::bip32::DerivationPath;
+use tokio::sync::{mpsc, oneshot};
+use tokio::task::JoinHandle;
+
+use super::{
+    BlindSignature, BlindedMessage, CurrencyUnit, Error, Id, KeySet, KeysResponse, KeysetResponse,
+    Proof, Signatory,
+};
+
+macro_rules! signatory_manager {
+    (
+        $(
+            $variant:ident($($input:ty),*) -> $output:ty,
+        )* $(,)?
+    ) => {
+        paste::paste! {
+        #[allow(non_camel_case_types, unused_parens)]
+        enum Request {
+            $(
+                $variant((($($input),*), oneshot::Sender<Result<$output, Error>>)),
+            )*
+        }
+
+        /// Manager for handling signatory requests.
+        pub struct SignatoryManager {
+            pipeline: mpsc::Sender<Request>,
+            runner: JoinHandle<()>,
+        }
+
+        #[allow(non_camel_case_types, unused_parens, non_snake_case)]
+        impl SignatoryManager {
+            /// Creates a new SignatoryManager with the given signatory.
+            ///
+            /// # Arguments
+            /// * `signatory` - An `Arc` of a signatory object implementing the required trait.
+            pub fn new(signatory: Arc<dyn Signatory + Send + Sync + 'static>) -> Self {
+                let (sender, receiver) = mpsc::channel(10_000);
+                let runner = tokio::spawn(async move {
+                    let mut receiver = receiver;
+                    loop {
+                        let request = if let Some(request) = receiver.recv().await {
+                            request
+                        } else {
+                            continue;
+                        };
+                        let signatory = signatory.clone();
+                        match request {
+                            $(
+                                Request::$variant((( $($input),* ), response)) => {
+                                    tokio::spawn(async move {
+                                        let output = signatory.[<$variant:lower>]($($input),*).await;
+                                        response.send(output).unwrap();
+                                    });
+                                }
+                            )*
+                        }
+                    }
+                });
+
+                Self {
+                    pipeline: sender,
+                    runner,
+                }
+            }
+
+            $(
+                /// Asynchronous method to handle the `$variant` request.
+                ///
+                /// # Arguments
+                /// * $($input: $input),* - The inputs required for the `$variant` request.
+                ///
+                /// # Returns
+                /// * `Result<$output, Error>` - The result of processing the request.
+                pub async fn [<$variant:lower>](&self, $($input: $input),*) -> Result<$output, Error> {
+                    let (sender, receiver) = oneshot::channel();
+
+                    self.pipeline
+                        .try_send(Request::$variant((($($input),*), sender)))
+                        .map_err(|e| Error::SendError(e.to_string()))?;
+
+                    receiver
+                        .await
+                        .map_err(|e| Error::RecvError(e.to_string()))?
+                }
+            )*
+        }
+
+        impl Drop for SignatoryManager {
+            fn drop(&mut self) {
+                self.runner.abort();
+            }
+        }
+
+        impl<T: Signatory + Send + Sync + 'static> From<T> for SignatoryManager {
+            fn from(signatory: T) -> Self {
+                Self::new(Arc::new(signatory))
+            }
+        }
+
+        }
+    };
+}
+
+type Map = HashMap<CurrencyUnit, DerivationPath>;
+
+signatory_manager! {
+    blind_sign(BlindedMessage) -> BlindSignature,
+    verify_proof(Proof) -> (),
+    keyset(Id) -> Option<KeySet>,
+    keysets() -> KeysetResponse,
+    keyset_pubkeys(Id) -> KeysResponse,
+    pubkeys() -> KeysResponse,
+    rotate_keyset(CurrencyUnit, u32, u8, u64, Map) -> (),
+}

+ 369 - 0
crates/cdk/src/mint/signatory/memory.rs

@@ -0,0 +1,369 @@
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+
+use bitcoin::bip32::{DerivationPath, Xpriv};
+use bitcoin::secp256k1::{self, Secp256k1};
+use tokio::sync::RwLock;
+
+use super::Signatory;
+use crate::cdk_database::{self, MintDatabase};
+use crate::dhke::{sign_message, verify_message};
+use crate::mint::nut01::MintKeyPair;
+use crate::mint::signatory::create_new_keyset;
+use crate::mint::{
+    derivation_path_from_unit, BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet,
+    KeySetInfo, KeysResponse, KeysetResponse, Kind, MintKeySet, MintKeySetInfo, Proof,
+};
+use crate::{Amount, Error};
+
+/// In-memory Signatory
+///
+/// This is the default signatory implementation for the mint.
+///
+/// The private keys and the all key-related data is stored in memory, in the same process, but it
+/// is not accessible from the outside.
+pub struct MemorySignatory {
+    keysets: RwLock<HashMap<Id, MintKeySet>>,
+    localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
+    secp_ctx: Secp256k1<secp256k1::All>,
+    xpriv: Xpriv,
+}
+
+impl MemorySignatory {
+    /// Creates a new MemorySignatory instance
+    pub async fn new(
+        localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
+        seed: &[u8],
+        supported_units: HashMap<CurrencyUnit, (u64, u8)>,
+        custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    ) -> Result<Self, Error> {
+        let secp_ctx = Secp256k1::new();
+        let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
+
+        let mut active_keysets = HashMap::new();
+        let keysets_infos = localstore.get_keyset_infos().await?;
+        let mut active_keyset_units = vec![];
+
+        if !keysets_infos.is_empty() {
+            tracing::debug!("Setting all saved keysets to inactive");
+            for keyset in keysets_infos.clone() {
+                // Set all to in active
+                let mut keyset = keyset;
+                keyset.active = false;
+                localstore.add_keyset_info(keyset).await?;
+            }
+
+            let keysets_by_unit: HashMap<CurrencyUnit, Vec<MintKeySetInfo>> =
+                keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| {
+                    acc.entry(ks.unit.clone()).or_default().push(ks.clone());
+                    acc
+                });
+
+            for (unit, keysets) in keysets_by_unit {
+                let mut keysets = keysets;
+                keysets.sort_by(|a, b| b.derivation_path_index.cmp(&a.derivation_path_index));
+
+                let highest_index_keyset = keysets
+                    .first()
+                    .cloned()
+                    .expect("unit will not be added to hashmap if empty");
+
+                let keysets: Vec<MintKeySetInfo> = keysets
+                    .into_iter()
+                    .filter(|ks| ks.derivation_path_index.is_some())
+                    .collect();
+
+                if let Some((input_fee_ppk, max_order)) = supported_units.get(&unit) {
+                    let derivation_path_index = if keysets.is_empty() {
+                        1
+                    } else if &highest_index_keyset.input_fee_ppk == input_fee_ppk
+                        && &highest_index_keyset.max_order == max_order
+                    {
+                        let id = highest_index_keyset.id;
+                        let keyset = MintKeySet::generate_from_xpriv(
+                            &secp_ctx,
+                            xpriv,
+                            highest_index_keyset.max_order,
+                            highest_index_keyset.unit.clone(),
+                            highest_index_keyset.derivation_path.clone(),
+                        );
+                        active_keysets.insert(id, keyset);
+                        let mut keyset_info = highest_index_keyset;
+                        keyset_info.active = true;
+                        localstore.add_keyset_info(keyset_info).await?;
+                        localstore.set_active_keyset(unit, id).await?;
+                        continue;
+                    } else {
+                        highest_index_keyset.derivation_path_index.unwrap_or(0) + 1
+                    };
+
+                    let derivation_path = match custom_paths.get(&unit) {
+                        Some(path) => path.clone(),
+                        None => derivation_path_from_unit(unit.clone(), derivation_path_index)
+                            .ok_or(Error::UnsupportedUnit)?,
+                    };
+
+                    let (keyset, keyset_info) = create_new_keyset(
+                        &secp_ctx,
+                        xpriv,
+                        derivation_path,
+                        Some(derivation_path_index),
+                        unit.clone(),
+                        *max_order,
+                        *input_fee_ppk,
+                    );
+
+                    let id = keyset_info.id;
+                    localstore.add_keyset_info(keyset_info).await?;
+                    localstore.set_active_keyset(unit.clone(), id).await?;
+                    active_keysets.insert(id, keyset);
+                    active_keyset_units.push(unit.clone());
+                }
+            }
+        }
+
+        for (unit, (fee, max_order)) in supported_units {
+            if !active_keyset_units.contains(&unit) {
+                let derivation_path = match custom_paths.get(&unit) {
+                    Some(path) => path.clone(),
+                    None => {
+                        derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)?
+                    }
+                };
+
+                let (keyset, keyset_info) = create_new_keyset(
+                    &secp_ctx,
+                    xpriv,
+                    derivation_path,
+                    Some(0),
+                    unit.clone(),
+                    max_order,
+                    fee,
+                );
+
+                let id = keyset_info.id;
+                localstore.add_keyset_info(keyset_info).await?;
+                localstore.set_active_keyset(unit, id).await?;
+                active_keysets.insert(id, keyset);
+            }
+        }
+
+        Ok(Self {
+            keysets: RwLock::new(HashMap::new()),
+            secp_ctx,
+            localstore,
+            xpriv,
+        })
+    }
+}
+
+impl MemorySignatory {
+    fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet {
+        MintKeySet::generate_from_xpriv(
+            &self.secp_ctx,
+            self.xpriv,
+            keyset_info.max_order,
+            keyset_info.unit,
+            keyset_info.derivation_path,
+        )
+    }
+
+    async fn load_and_get_keyset(&self, id: &Id) -> Result<MintKeySetInfo, Error> {
+        let keysets = self.keysets.read().await;
+        let keyset_info = self
+            .localstore
+            .get_keyset_info(id)
+            .await?
+            .ok_or(Error::UnknownKeySet)?;
+
+        if keysets.contains_key(id) {
+            return Ok(keyset_info);
+        }
+        drop(keysets);
+
+        let id = keyset_info.id;
+        let mut keysets = self.keysets.write().await;
+        keysets.insert(id, self.generate_keyset(keyset_info.clone()));
+        Ok(keyset_info)
+    }
+
+    async fn get_keypair_for_amount(
+        &self,
+        keyset_id: &Id,
+        amount: &Amount,
+    ) -> Result<MintKeyPair, Error> {
+        let keyset_info = self.load_and_get_keyset(keyset_id).await?;
+        let active = self
+            .localstore
+            .get_active_keyset_id(&keyset_info.unit)
+            .await?
+            .ok_or(Error::InactiveKeyset)?;
+
+        // Check that the keyset is active and should be used to sign
+        if keyset_info.id != active {
+            return Err(Error::InactiveKeyset);
+        }
+
+        let keysets = self.keysets.read().await;
+        let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?;
+
+        match keyset.keys.get(amount) {
+            Some(key_pair) => Ok(key_pair.clone()),
+            None => Err(Error::AmountKey),
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl Signatory for MemorySignatory {
+    async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result<BlindSignature, Error> {
+        let BlindedMessage {
+            amount,
+            blinded_secret,
+            keyset_id,
+            ..
+        } = blinded_message;
+        let key_pair = self.get_keypair_for_amount(&keyset_id, &amount).await?;
+        let c = sign_message(&key_pair.secret_key, &blinded_secret)?;
+
+        let blinded_signature = BlindSignature::new(
+            amount,
+            c,
+            keyset_id,
+            &blinded_message.blinded_secret,
+            key_pair.secret_key,
+        )?;
+
+        Ok(blinded_signature)
+    }
+
+    async fn verify_proof(&self, proof: Proof) -> Result<(), Error> {
+        // Check if secret is a nut10 secret with conditions
+        if let Ok(secret) =
+            <&crate::secret::Secret as TryInto<crate::nuts::nut10::Secret>>::try_into(&proof.secret)
+        {
+            // Checks and verifes known secret kinds.
+            // If it is an unknown secret kind it will be treated as a normal secret.
+            // Spending conditions will **not** be check. It is up to the wallet to ensure
+            // only supported secret kinds are used as there is no way for the mint to
+            // enforce only signing supported secrets as they are blinded at
+            // that point.
+            match secret.kind {
+                Kind::P2PK => {
+                    proof.verify_p2pk()?;
+                }
+                Kind::HTLC => {
+                    proof.verify_htlc()?;
+                }
+            }
+        }
+
+        let key_pair = self
+            .get_keypair_for_amount(&proof.keyset_id, &proof.amount)
+            .await?;
+
+        verify_message(&key_pair.secret_key, proof.c, proof.secret.as_bytes())?;
+
+        Ok(())
+    }
+
+    async fn keyset(&self, keyset_id: Id) -> Result<Option<KeySet>, Error> {
+        self.load_and_get_keyset(&keyset_id).await?;
+        Ok(self
+            .keysets
+            .read()
+            .await
+            .get(&keyset_id)
+            .map(|k| k.clone().into()))
+    }
+
+    async fn keyset_pubkeys(&self, keyset_id: Id) -> Result<KeysResponse, Error> {
+        self.load_and_get_keyset(&keyset_id).await?;
+        Ok(KeysResponse {
+            keysets: vec![self
+                .keysets
+                .read()
+                .await
+                .get(&keyset_id)
+                .ok_or(Error::UnknownKeySet)?
+                .clone()
+                .into()],
+        })
+    }
+
+    async fn pubkeys(&self) -> Result<KeysResponse, Error> {
+        let active_keysets = self.localstore.get_active_keysets().await?;
+        let active_keysets: HashSet<&Id> = active_keysets.values().collect();
+        for id in active_keysets.iter() {
+            let _ = self.load_and_get_keyset(id).await?;
+        }
+        let keysets = self.keysets.read().await;
+        Ok(KeysResponse {
+            keysets: keysets
+                .values()
+                .filter_map(|k| match active_keysets.contains(&k.id) {
+                    true => Some(k.clone().into()),
+                    false => None,
+                })
+                .collect(),
+        })
+    }
+
+    async fn keysets(&self) -> Result<KeysetResponse, Error> {
+        let keysets = self.localstore.get_keyset_infos().await?;
+        let active_keysets: HashSet<Id> = self
+            .localstore
+            .get_active_keysets()
+            .await?
+            .values()
+            .cloned()
+            .collect();
+
+        Ok(KeysetResponse {
+            keysets: keysets
+                .into_iter()
+                .map(|k| KeySetInfo {
+                    id: k.id,
+                    unit: k.unit,
+                    active: active_keysets.contains(&k.id),
+                    input_fee_ppk: k.input_fee_ppk,
+                })
+                .collect(),
+        })
+    }
+
+    /// Add current keyset to inactive keysets
+    /// Generate new keyset
+    async fn rotate_keyset(
+        &self,
+        unit: CurrencyUnit,
+        derivation_path_index: u32,
+        max_order: u8,
+        input_fee_ppk: u64,
+        custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    ) -> Result<(), Error> {
+        let derivation_path = match custom_paths.get(&unit) {
+            Some(path) => path.clone(),
+            None => derivation_path_from_unit(unit.clone(), derivation_path_index)
+                .ok_or(Error::UnsupportedUnit)?,
+        };
+
+        let (keyset, keyset_info) = create_new_keyset(
+            &self.secp_ctx,
+            self.xpriv,
+            derivation_path,
+            Some(derivation_path_index),
+            unit.clone(),
+            max_order,
+            input_fee_ppk,
+        );
+        let id = keyset_info.id;
+        self.localstore.add_keyset_info(keyset_info).await?;
+        self.localstore.set_active_keyset(unit, id).await?;
+
+        let mut keysets = self.keysets.write().await;
+        keysets.insert(id, keyset);
+
+        Ok(())
+    }
+}

+ 58 - 0
crates/cdk/src/mint/signatory/mod.rs

@@ -0,0 +1,58 @@
+//! Signatory mod
+//!
+//! This module abstract all the key related operations, defining an interface for the necessary
+//! operations, to be implemented by the different signatory implementations.
+//!
+//! There is an in memory implementation, when the keys are stored in memory, in the same process,
+//! but it is isolated from the rest of the application, and they communicate through a channel with
+//! the defined API.
+use std::collections::HashMap;
+
+use bitcoin::bip32::DerivationPath;
+
+use super::{
+    BlindSignature, BlindedMessage, CurrencyUnit, Error, Id, KeySet, KeysResponse, KeysetResponse,
+    Proof,
+};
+
+mod common;
+mod manager;
+mod memory;
+
+pub use self::common::*;
+pub use self::manager::SignatoryManager;
+pub use self::memory::MemorySignatory;
+
+#[async_trait::async_trait]
+/// Signatory trait
+pub trait Signatory {
+    /// Blind sign a message
+    async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result<BlindSignature, Error>;
+
+    /// Verify [`Proof`] meets conditions and is signed
+    async fn verify_proof(&self, proof: Proof) -> Result<(), Error>;
+
+    /// Retrieve a keyset by id
+    async fn keyset(&self, keyset_id: Id) -> Result<Option<KeySet>, Error>;
+
+    /// Retrieve the public keys of a keyset
+    async fn keyset_pubkeys(&self, keyset_id: Id) -> Result<KeysResponse, Error>;
+
+    /// Retrieve the public keys of the active keyset for distribution to wallet
+    /// clients
+    async fn pubkeys(&self) -> Result<KeysResponse, Error>;
+
+    /// Return a list of all supported keysets
+    async fn keysets(&self) -> Result<KeysetResponse, Error>;
+
+    /// Add current keyset to inactive keysets
+    /// Generate new keyset
+    async fn rotate_keyset(
+        &self,
+        unit: CurrencyUnit,
+        derivation_path_index: u32,
+        max_order: u8,
+        input_fee_ppk: u64,
+        custom_paths: HashMap<CurrencyUnit, DerivationPath>,
+    ) -> Result<(), Error>;
+}

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