From 3e16038952f797f425d85fc4fde200ab428353c0 Mon Sep 17 00:00:00 2001 From: Cesar Rodas Date: Mon, 13 Jan 2025 20:46:45 -0300 Subject: [PATCH] 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. --- crates/cashu/src/nuts/nut00/mod.rs | 12 + crates/cashu/src/nuts/nut01/mod.rs | 8 + crates/cdk-axum/Cargo.toml | 2 +- crates/cdk-cln/Cargo.toml | 13 +- crates/cdk-common/src/error.rs | 8 + crates/cdk-common/src/lib.rs | 2 + crates/cdk-common/src/signatory.rs | 74 +++ crates/cdk-integration-tests/Cargo.toml | 2 +- .../cdk-integration-tests/src/init_regtest.rs | 17 +- crates/cdk-integration-tests/tests/mint.rs | 41 +- crates/cdk-mintd/Cargo.toml | 3 + crates/cdk-mintd/src/bin/signatory.rs | 61 ++ crates/cdk-mintd/src/config.rs | 31 +- crates/cdk-mintd/src/env_vars.rs | 10 +- crates/cdk-mintd/src/lib.rs | 12 + crates/cdk-mintd/src/main.rs | 43 +- crates/cdk-phoenixd/Cargo.toml | 13 +- crates/cdk-redb/Cargo.toml | 2 +- crates/cdk-signatory/Cargo.toml | 28 + crates/cdk-signatory/build.rs | 4 + crates/cdk-signatory/src/lib.rs | 541 ++++++++++++++++++ crates/cdk-signatory/src/proto/client.rs | 72 +++ crates/cdk-signatory/src/proto/mod.rs | 119 ++++ crates/cdk-signatory/src/proto/server.rs | 38 ++ .../cdk-signatory/src/proto/signatory.proto | 52 ++ crates/cdk-sqlite/Cargo.toml | 2 +- crates/cdk-strike/Cargo.toml | 13 +- crates/cdk/Cargo.toml | 9 +- crates/cdk/src/mint/builder.rs | 75 ++- crates/cdk/src/mint/config.rs | 103 ++++ crates/cdk/src/mint/keysets.rs | 153 +---- crates/cdk/src/mint/mod.rs | 389 ++----------- crates/cdk/src/mint/signatory.rs | 134 +++++ misc/itests.sh | 7 +- 34 files changed, 1531 insertions(+), 562 deletions(-) create mode 100644 crates/cdk-common/src/signatory.rs create mode 100644 crates/cdk-mintd/src/bin/signatory.rs create mode 100644 crates/cdk-signatory/Cargo.toml create mode 100644 crates/cdk-signatory/build.rs create mode 100644 crates/cdk-signatory/src/lib.rs create mode 100644 crates/cdk-signatory/src/proto/client.rs create mode 100644 crates/cdk-signatory/src/proto/mod.rs create mode 100644 crates/cdk-signatory/src/proto/server.rs create mode 100644 crates/cdk-signatory/src/proto/signatory.proto create mode 100644 crates/cdk/src/mint/config.rs create mode 100644 crates/cdk/src/mint/signatory.rs diff --git a/crates/cashu/src/nuts/nut00/mod.rs b/crates/cashu/src/nuts/nut00/mod.rs index 4d8bbb81b..2d36ef749 100644 --- a/crates/cashu/src/nuts/nut00/mod.rs +++ b/crates/cashu/src/nuts/nut00/mod.rs @@ -200,6 +200,18 @@ pub enum Witness { HTLCWitness(HTLCWitness), } +impl From for Witness { + fn from(witness: P2PKWitness) -> Self { + Self::P2PKWitness(witness) + } +} + +impl From for Witness { + fn from(witness: HTLCWitness) -> Self { + Self::HTLCWitness(witness) + } +} + impl Witness { /// Add signatures to [`Witness`] pub fn add_signatures(&mut self, signatues: Vec) { diff --git a/crates/cashu/src/nuts/nut01/mod.rs b/crates/cashu/src/nuts/nut01/mod.rs index bda245fd4..5a04c1501 100644 --- a/crates/cashu/src/nuts/nut01/mod.rs +++ b/crates/cashu/src/nuts/nut01/mod.rs @@ -46,6 +46,14 @@ pub enum Error { #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct Keys(BTreeMap); +impl Deref for Keys { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl From for Keys { fn from(keys: MintKeys) -> Self { Self( diff --git a/crates/cdk-axum/Cargo.toml b/crates/cdk-axum/Cargo.toml index cccf692df..5628a9be0 100644 --- a/crates/cdk-axum/Cargo.toml +++ b/crates/cdk-axum/Cargo.toml @@ -29,7 +29,7 @@ moka = { version = "0.11.1", features = ["future"] } serde_json = "1" paste = "1.0.15" serde = { version = "1.0.210", features = ["derive"] } -uuid = { version = "1", features = ["v4", "serde"] } +uuid = { version = "=1.12.1", features = ["v4", "serde"] } sha2 = "0.10.8" redis = { version = "0.23.3", features = [ "tokio-rustls-comp", diff --git a/crates/cdk-cln/Cargo.toml b/crates/cdk-cln/Cargo.toml index 534ac0f09..2dd24e131 100644 --- a/crates/cdk-cln/Cargo.toml +++ b/crates/cdk-cln/Cargo.toml @@ -6,17 +6,22 @@ authors = ["CDK Developers"] license = "MIT" homepage = "https://github.com/cashubtc/cdk" repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV +rust-version = "1.63.0" # MSRV description = "CDK ln backend for cln" [dependencies] async-trait = "0.1" bitcoin = { version = "0.32.2", default-features = false } -cdk = { path = "../cdk", version = "0.6.0", default-features = false, features = ["mint"] } +cdk = { path = "../cdk", version = "0.6.0", default-features = false, features = [ + "mint", +] } cln-rpc = "0.3.0" futures = { version = "0.3.28", default-features = false } tokio = { version = "1", default-features = false } tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } thiserror = "1" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "=1.12.1", features = ["v4"] } diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index e179c7efe..7aa17a16c 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -59,6 +59,14 @@ pub enum Error { #[error("Multi-Part payment is not supported for unit `{0}` and method `{1}`")] MppUnitMethodNotSupported(CurrencyUnit, PaymentMethod), + /// Internal Error - Send error + #[error("Internal send error: {0}")] + SendError(String), + + /// Internal Error - Recv error + #[error("Internal receive error: {0}")] + RecvError(String), + // Mint Errors /// Minting is disabled #[error("Minting is disabled")] diff --git a/crates/cdk-common/src/lib.rs b/crates/cdk-common/src/lib.rs index 52e7068eb..73ed62474 100644 --- a/crates/cdk-common/src/lib.rs +++ b/crates/cdk-common/src/lib.rs @@ -13,6 +13,8 @@ pub mod error; pub mod lightning; pub mod pub_sub; #[cfg(feature = "mint")] +pub mod signatory; +#[cfg(feature = "mint")] pub mod subscription; pub mod ws; diff --git a/crates/cdk-common/src/signatory.rs b/crates/cdk-common/src/signatory.rs new file mode 100644 index 000000000..2cb4b3db0 --- /dev/null +++ b/crates/cdk-common/src/signatory.rs @@ -0,0 +1,74 @@ +//! Signatory mod +//! +//! This module abstract all the key related operations, defining an interface for the necessary +//! operations, to be implemented by the different signatory implementations. +//! +//! There is an in memory implementation, when the keys are stored in memory, in the same process, +//! but it is isolated from the rest of the application, and they communicate through a channel with +//! the defined API. +use std::collections::HashMap; + +use bitcoin::bip32::DerivationPath; +use cashu::mint::MintKeySetInfo; +use cashu::{ + BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Proof, +}; + +use super::error::Error; + +/// Type alias to make the keyset info API more useful, queryable by unit and Id +pub enum KeysetIdentifier { + /// Mint Keyset by unit + Unit(CurrencyUnit), + /// Mint Keyset by Id + Id(Id), +} + +impl From for KeysetIdentifier { + fn from(id: Id) -> Self { + Self::Id(id) + } +} + +impl From for KeysetIdentifier { + fn from(unit: CurrencyUnit) -> Self { + Self::Unit(unit) + } +} + +#[async_trait::async_trait] +/// Signatory trait +pub trait Signatory { + /// Blind sign a message + async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result; + + /// Verify [`Proof`] meets conditions and is signed + async fn verify_proof(&self, proof: Proof) -> Result<(), Error>; + + /// Retrieve a keyset by id + async fn keyset(&self, keyset_id: Id) -> Result, Error>; + + /// Retrieve the public keys of a keyset + async fn keyset_pubkeys(&self, keyset_id: Id) -> Result; + + /// Retrieve the public keys of the active keyset for distribution to wallet + /// clients + async fn pubkeys(&self) -> Result; + + /// Return a list of all supported keysets + async fn keysets(&self) -> Result; + + /// Add current keyset to inactive keysets + /// Generate new keyset + async fn rotate_keyset( + &self, + unit: CurrencyUnit, + derivation_path_index: u32, + max_order: u8, + input_fee_ppk: u64, + custom_paths: HashMap, + ) -> Result; + + /// Get Mint Keyset Info by Unit or Id + async fn get_keyset_info(&self, keyset_id: KeysetIdentifier) -> Result; +} diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml index b7f23b6c0..182a0406a 100644 --- a/crates/cdk-integration-tests/Cargo.toml +++ b/crates/cdk-integration-tests/Cargo.toml @@ -31,7 +31,7 @@ futures = { version = "0.3.28", default-features = false, features = [ "executor", ] } once_cell = "1.19.0" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "=1.12.1", features = ["v4"] } serde = "1" serde_json = "1" # ln-regtest-rs = { path = "../../../../ln-regtest-rs" } diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index ca9bfdd9c..33c298434 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::env; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -6,7 +7,7 @@ use anyhow::Result; use bip39::Mnemonic; use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning::{self, MintLightning}; -use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; +use cdk::mint::{FeeReserve, MemorySignatory, MintBuilder, MintMeltLimits}; use cdk::nuts::{CurrencyUnit, PaymentMethod}; use cdk_cln::Cln as CdkCln; use cdk_lnd::Lnd as CdkLnd; @@ -156,7 +157,9 @@ where { let mut mint_builder = MintBuilder::new(); - mint_builder = mint_builder.with_localstore(Arc::new(database)); + let localstore = Arc::new(database); + + mint_builder = mint_builder.with_localstore(localstore.clone()); mint_builder = mint_builder.add_ln_backend( CurrencyUnit::Sat, @@ -167,8 +170,18 @@ where let mnemonic = Mnemonic::generate(12)?; + let signatory_manager = MemorySignatory::new( + localstore, + &mnemonic.to_seed_normalized(""), + mint_builder.supported_units.clone(), + HashMap::new(), + ) + .await + .expect("valid signatory"); + mint_builder = mint_builder .with_name("regtest mint".to_string()) + .with_signatory(Arc::new(signatory_manager)) .with_description("regtest mint".to_string()) .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 265c031bc..03c0ceaa9 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -1,6 +1,7 @@ //! Mint tests use std::collections::{HashMap, HashSet}; +use std::ops::Deref; use std::sync::Arc; use std::time::Duration; @@ -10,7 +11,8 @@ use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk::cdk_database::MintDatabase; use cdk::dhke::construct_proofs; -use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits, MintQuote}; +use cdk::mint::signatory::SignatoryManager; +use cdk::mint::{FeeReserve, MemorySignatory, MintBuilder, MintMeltLimits, MintQuote}; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod, @@ -25,7 +27,7 @@ use tokio::time::sleep; pub const MINT_URL: &str = "http://127.0.0.1:8088"; -static INSTANCE: OnceCell = OnceCell::const_new(); +static INSTANCE: OnceCell> = OnceCell::const_new(); async fn new_mint(fee: u64) -> Mint { let mut supported_units = HashMap::new(); @@ -50,19 +52,29 @@ async fn new_mint(fee: u64) -> Mint { .expect("Could not set mint info"); let mnemonic = Mnemonic::generate(12).unwrap(); + let localstore = Arc::new(MintMemoryDatabase::default()); + let seed = mnemonic.to_seed_normalized(""); + let signatory_manager = Arc::new(SignatoryManager::new(Arc::new( + MemorySignatory::new(localstore.clone(), &seed, supported_units, HashMap::new()) + .await + .expect("valid signatory"), + ))); + Mint::new( - &mnemonic.to_seed_normalized(""), - Arc::new(localstore), + localstore, HashMap::new(), - supported_units, + signatory_manager, HashMap::new(), ) .await .unwrap() } -async fn initialize() -> &'static Mint { - INSTANCE.get_or_init(|| new_mint(0)).await +async fn initialize() -> Arc { + INSTANCE + .get_or_init(|| async { Arc::new(new_mint(0).await) }) + .await + .clone() } async fn mint_proofs( @@ -114,7 +126,7 @@ async fn test_mint_double_spend() -> Result<()> { let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); - let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?; + let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?; let preswap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?; @@ -148,7 +160,7 @@ async fn test_attempt_to_swap_by_overflowing() -> Result<()> { let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); - let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?; + let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?; let amount = 2_u64.pow(63); @@ -185,7 +197,7 @@ pub async fn test_p2pk_swap() -> Result<()> { let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); - let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?; + let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?; let secret = SecretKey::generate(); @@ -303,7 +315,7 @@ async fn test_swap_unbalanced() -> Result<()> { let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); - let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?; + let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?; let preswap = PreMintSecrets::random(keyset_id, 95.into(), &SplitTarget::default())?; @@ -468,7 +480,7 @@ async fn test_correct_keyset() -> Result<()> { .with_quote_ttl(10000, 1000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); - let mint = mint_builder.build().await?; + let mint = mint_builder.clone().build().await?; mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0).await?; mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0).await?; @@ -487,7 +499,10 @@ async fn test_correct_keyset() -> Result<()> { assert!(keyset_info.derivation_path_index == Some(2)); - let mint = mint_builder.build().await?; + let mint = mint_builder + .with_signatory(mint.signatory.deref().deref().to_owned()) + .build() + .await?; let active = mint.localstore.get_active_keysets().await?; diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index 550d415a5..c481b5124 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -26,6 +26,9 @@ cdk-lnbits = { path = "../cdk-lnbits", version = "0.6.0", default-features = fal cdk-phoenixd = { path = "../cdk-phoenixd", version = "0.6.0", default-features = false } cdk-lnd = { path = "../cdk-lnd", version = "0.6.0", default-features = false } cdk-fake-wallet = { path = "../cdk-fake-wallet", version = "0.6.0", default-features = false } +cdk-signatory = { path = "../cdk-signatory", default-features = false, features = [ + "grpc", +] } cdk-strike = { path = "../cdk-strike", version = "0.6.0" } cdk-axum = { path = "../cdk-axum", version = "0.6.0", default-features = false } config = { version = "0.13.3", features = ["toml"] } diff --git a/crates/cdk-mintd/src/bin/signatory.rs b/crates/cdk-mintd/src/bin/signatory.rs new file mode 100644 index 000000000..d43768a90 --- /dev/null +++ b/crates/cdk-mintd/src/bin/signatory.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; +use std::env; +use std::str::FromStr; + +use bip39::Mnemonic; +use cdk::nuts::CurrencyUnit; +use cdk_mintd::cli::CLIArgs; +use cdk_mintd::env_vars::ENV_WORK_DIR; +use cdk_mintd::{config, work_dir}; +use cdk_signatory::proto::server::grpc_server; +use cdk_signatory::MemorySignatory; +use clap::Parser; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = CLIArgs::parse(); + let work_dir = if let Some(work_dir) = args.work_dir { + tracing::info!("Using work dir from cmd arg"); + work_dir + } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) { + tracing::info!("Using work dir from env var"); + env_work_dir.into() + } else { + work_dir()? + }; + + let config_file_arg = match args.config { + Some(c) => c, + None => work_dir.join("config.toml"), + }; + + let settings = if config_file_arg.exists() { + config::Settings::new(Some(config_file_arg)) + } else { + tracing::info!("Config file does not exist. Attempting to read env vars"); + config::Settings::default() + }; + + // This check for any settings defined in ENV VARs + // ENV VARS will take **priority** over those in the config + let mut settings = settings.from_env()?; + let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?; + + let signatory = MemorySignatory::new( + settings.database.engine.clone().mint(&work_dir).await?, + &mnemonic.to_seed_normalized(""), + settings + .supported_units + .take() + .unwrap_or(vec![CurrencyUnit::default()]) + .into_iter() + .map(|u| (u, (0, 32))) + .collect::>(), + HashMap::new(), + ) + .await?; + + grpc_server(signatory, "[::1]:50051".parse().unwrap()).await?; + + Ok(()) +} diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index b14cfc79b..df9882e7c 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -1,8 +1,11 @@ use std::path::PathBuf; +use std::sync::Arc; use cdk::nuts::{CurrencyUnit, PublicKey}; -use cdk::Amount; +use cdk::{cdk_database, Amount}; use cdk_axum::cache; +use cdk_redb::MintRedbDatabase; +use cdk_sqlite::MintSqliteDatabase; use config::{Config, ConfigError, File}; use serde::{Deserialize, Serialize}; @@ -169,6 +172,30 @@ impl std::str::FromStr for DatabaseEngine { } } +impl DatabaseEngine { + /// Convert the database instance into a mint database + pub async fn mint>( + self, + work_dir: P, + ) -> Result< + Arc + Sync + Send + 'static>, + cdk_database::Error, + > { + match self { + DatabaseEngine::Sqlite => { + let sql_db_path = work_dir.into().join("cdk-mintd.sqlite"); + let db = MintSqliteDatabase::new(&sql_db_path).await?; + db.migrate().await; + Ok(Arc::new(db)) + } + DatabaseEngine::Redb => { + let redb_path = work_dir.into().join("cdk-mintd.redb"); + Ok(Arc::new(MintRedbDatabase::new(&redb_path)?)) + } + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Database { pub engine: DatabaseEngine, @@ -187,6 +214,8 @@ pub struct Settings { pub lnd: Option, pub fake_wallet: Option, pub database: Database, + pub supported_units: Option>, + pub remote_signatory: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/crates/cdk-mintd/src/env_vars.rs b/crates/cdk-mintd/src/env_vars.rs index 27b815b01..29c5287ab 100644 --- a/crates/cdk-mintd/src/env_vars.rs +++ b/crates/cdk-mintd/src/env_vars.rs @@ -72,15 +72,15 @@ pub const ENV_FAKE_WALLET_MIN_DELAY: &str = "CDK_MINTD_FAKE_WALLET_MIN_DELAY"; pub const ENV_FAKE_WALLET_MAX_DELAY: &str = "CDK_MINTD_FAKE_WALLET_MAX_DELAY"; impl Settings { - pub fn from_env(&mut self) -> Result { + pub fn from_env(mut self) -> Result { if let Ok(database) = env::var(DATABASE_ENV_VAR) { let engine = DatabaseEngine::from_str(&database).map_err(|err| anyhow!(err))?; self.database = Database { engine }; } - self.info = self.info.clone().from_env(); - self.mint_info = self.mint_info.clone().from_env(); - self.ln = self.ln.clone().from_env(); + self.info = self.info.from_env(); + self.mint_info = self.mint_info.from_env(); + self.ln = self.ln.from_env(); match self.ln.ln_backend { LnBackend::Cln => { @@ -104,7 +104,7 @@ impl Settings { LnBackend::None => bail!("Ln backend must be set"), } - Ok(self.clone()) + Ok(self) } } diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 1919dcf22..2464982ae 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; +use anyhow::anyhow; + pub mod cli; pub mod config; pub mod env_vars; @@ -22,6 +24,16 @@ fn expand_path(path: &str) -> Option { } } +/// Work dir +pub fn work_dir() -> anyhow::Result { + let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; + let dir = home_dir.join(".cdk-mintd"); + + std::fs::create_dir_all(&dir)?; + + Ok(dir) +} + #[cfg(test)] mod test { use std::env::current_dir; diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index f6d728e67..a0241a106 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -5,17 +5,15 @@ use std::collections::HashMap; use std::env; -use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; -use anyhow::{anyhow, bail, Result}; +use anyhow::bail; use axum::http::Request; use axum::middleware::Next; use axum::response::Response; use axum::{middleware, Router}; use bip39::Mnemonic; -use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning; use cdk::cdk_lightning::MintLightning; use cdk::mint::{MintBuilder, MintMeltLimits}; @@ -25,11 +23,10 @@ use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod}; use cdk::types::LnKey; use cdk_axum::cache::HttpCache; use cdk_mintd::cli::CLIArgs; -use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; +use cdk_mintd::config::{self, LnBackend}; use cdk_mintd::env_vars::ENV_WORK_DIR; use cdk_mintd::setup::LnBackendSetup; -use cdk_redb::MintRedbDatabase; -use cdk_sqlite::MintSqliteDatabase; +use cdk_mintd::work_dir; use clap::Parser; use tokio::sync::Notify; use tower_http::compression::CompressionLayer; @@ -76,7 +73,7 @@ async fn main() -> anyhow::Result<()> { let mut mint_builder = MintBuilder::new(); - let mut settings = if config_file_arg.exists() { + let settings = if config_file_arg.exists() { config::Settings::new(Some(config_file_arg)) } else { tracing::info!("Config file does not exist. Attempting to read env vars"); @@ -86,22 +83,7 @@ async fn main() -> anyhow::Result<()> { // This check for any settings defined in ENV VARs // ENV VARS will take **priority** over those in the config let settings = settings.from_env()?; - - let localstore: Arc + Send + Sync> = - match settings.database.engine { - DatabaseEngine::Sqlite => { - let sql_db_path = work_dir.join("cdk-mintd.sqlite"); - let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?; - - sqlite_db.migrate().await; - - Arc::new(sqlite_db) - } - DatabaseEngine::Redb => { - let redb_path = work_dir.join("cdk-mintd.redb"); - Arc::new(MintRedbDatabase::new(&redb_path)?) - } - }; + let localstore = settings.database.engine.clone().mint(&work_dir).await?; mint_builder = mint_builder.with_localstore(localstore); @@ -306,6 +288,12 @@ async fn main() -> anyhow::Result<()> { .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); + mint_builder = if let Some(remote_signatory) = settings.remote_signatory.clone() { + mint_builder.with_remote_signatory(remote_signatory) + } else { + mint_builder + }; + let cached_endpoints = vec![ CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11), CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11), @@ -411,12 +399,3 @@ async fn logging_middleware(req: Request, next: Next) -> Response { response } - -fn work_dir() -> Result { - let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; - let dir = home_dir.join(".cdk-mintd"); - - std::fs::create_dir_all(&dir)?; - - Ok(dir) -} diff --git a/crates/cdk-phoenixd/Cargo.toml b/crates/cdk-phoenixd/Cargo.toml index 30b640953..3f86b2b4a 100644 --- a/crates/cdk-phoenixd/Cargo.toml +++ b/crates/cdk-phoenixd/Cargo.toml @@ -6,7 +6,7 @@ authors = ["CDK Developers"] license = "MIT" homepage = "https://github.com/cashubtc/cdk" repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV +rust-version = "1.63.0" # MSRV description = "CDK ln backend for phoenixd" [dependencies] @@ -14,11 +14,16 @@ async-trait = "0.1" anyhow = "1" axum = "0.6.20" bitcoin = { version = "0.32.2", default-features = false } -cdk = { path = "../cdk", version = "0.6.0", default-features = false, features = ["mint"] } +cdk = { path = "../cdk", version = "0.6.0", default-features = false, features = [ + "mint", +] } futures = { version = "0.3.28", default-features = false } tokio = { version = "1", default-features = false } tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } thiserror = "1" phoenixd-rs = "0.4.0" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "=1.12.1", features = ["v4"] } diff --git a/crates/cdk-redb/Cargo.toml b/crates/cdk-redb/Cargo.toml index 06ed9a828..d37ea98c8 100644 --- a/crates/cdk-redb/Cargo.toml +++ b/crates/cdk-redb/Cargo.toml @@ -27,4 +27,4 @@ tracing = { version = "0.1", default-features = false, features = [ serde = { version = "1", default-features = false, features = ["derive"] } serde_json = "1" lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } -uuid = { version = "1", features = ["v4", "serde"] } +uuid = { version = "=1.12.1", features = ["v4", "serde"] } diff --git a/crates/cdk-signatory/Cargo.toml b/crates/cdk-signatory/Cargo.toml new file mode 100644 index 000000000..06f446363 --- /dev/null +++ b/crates/cdk-signatory/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "cdk-signatory" +version = "0.6.0" +edition = "2021" +description = "CDK signatory default implementation" + +[features] +default = [] +grpc = ["dep:tonic", "tokio/full", "dep:prost", "dep:tonic-build"] + +[dependencies] +async-trait = "0.1.83" +bitcoin = { version = "0.32.2", features = [ + "base64", + "serde", + "rand", + "rand-std", +] } +cdk-common = { path = "../cdk-common", default-features = false, features = [ + "mint", +] } +tracing = "0.1.41" +tokio = { version = "1", default-features = false, features = ["sync"] } +tonic = { version = "0.11.0", optional = true } +prost = { version = "0.12.6", optional = true } + +[build-dependencies] +tonic-build = { version = "0.11.0", features = ["prost"], optional = true } diff --git a/crates/cdk-signatory/build.rs b/crates/cdk-signatory/build.rs new file mode 100644 index 000000000..746b4345b --- /dev/null +++ b/crates/cdk-signatory/build.rs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(feature = "grpc")] + tonic_build::compile_protos("src/proto/signatory.proto").unwrap(); +} diff --git a/crates/cdk-signatory/src/lib.rs b/crates/cdk-signatory/src/lib.rs new file mode 100644 index 000000000..71e6c8c83 --- /dev/null +++ b/crates/cdk-signatory/src/lib.rs @@ -0,0 +1,541 @@ +//! In memory signatory +//! +//! Implements the Signatory trait from cdk-common to manage the key in-process, to be included +//! inside the mint to be executed as a single process. +//! +//! Even if it is embedded in the same process, the keys are not accessible from the outside of this +//! module, all communication is done through the Signatory trait and the signatory manager. +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; +use bitcoin::secp256k1::{self, Secp256k1}; +use cdk_common::amount::Amount; +use cdk_common::database::{self, MintDatabase}; +use cdk_common::dhke::{sign_message, verify_message}; +use cdk_common::error::Error; +use cdk_common::mint::MintKeySetInfo; +use cdk_common::nuts::nut01::MintKeyPair; +use cdk_common::nuts::{ + self, BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeySetInfo, KeysResponse, + KeysetResponse, Kind, MintKeySet, Proof, +}; +use cdk_common::secret; +use cdk_common::signatory::{KeysetIdentifier, Signatory}; +use cdk_common::util::unix_time; +use tokio::sync::RwLock; + +#[cfg(feature = "grpc")] +pub mod proto; + +#[cfg(feature = "grpc")] +pub use proto::client::RemoteSigner; + +/// Generate new [`MintKeySetInfo`] from path +#[tracing::instrument(skip_all)] +fn create_new_keyset( + secp: &secp256k1::Secp256k1, + xpriv: Xpriv, + derivation_path: DerivationPath, + derivation_path_index: Option, + unit: CurrencyUnit, + max_order: u8, + input_fee_ppk: u64, +) -> (MintKeySet, MintKeySetInfo) { + let keyset = MintKeySet::generate( + secp, + xpriv + .derive_priv(secp, &derivation_path) + .expect("RNG busted"), + unit, + max_order, + ); + let keyset_info = MintKeySetInfo { + id: keyset.id, + unit: keyset.unit.clone(), + active: true, + valid_from: unix_time(), + valid_to: None, + derivation_path, + derivation_path_index, + max_order, + input_fee_ppk, + }; + (keyset, keyset_info) +} + +fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option { + let unit_index = unit.derivation_index()?; + + Some(DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(0).expect("0 is a valid index"), + ChildNumber::from_hardened_idx(unit_index).expect("0 is a valid index"), + ChildNumber::from_hardened_idx(index).expect("0 is a valid index"), + ])) +} + +/// In-memory Signatory +/// +/// This is the default signatory implementation for the mint. +/// +/// The private keys and the all key-related data is stored in memory, in the same process, but it +/// is not accessible from the outside. +pub struct MemorySignatory { + keysets: RwLock>, + localstore: Arc + Send + Sync>, + secp_ctx: Secp256k1, + xpriv: Xpriv, +} + +impl MemorySignatory { + /// Creates a new MemorySignatory instance + pub async fn new( + localstore: Arc + Send + Sync>, + seed: &[u8], + supported_units: HashMap, + custom_paths: HashMap, + ) -> Result { + let secp_ctx = Secp256k1::new(); + let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); + + let mut active_keysets = HashMap::new(); + let keysets_infos = localstore.get_keyset_infos().await?; + let mut active_keyset_units = vec![]; + + if !keysets_infos.is_empty() { + tracing::debug!("Setting all saved keysets to inactive"); + for keyset in keysets_infos.clone() { + // Set all to in active + let mut keyset = keyset; + keyset.active = false; + localstore.add_keyset_info(keyset).await?; + } + + let keysets_by_unit: HashMap> = + keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| { + acc.entry(ks.unit.clone()).or_default().push(ks.clone()); + acc + }); + + for (unit, keysets) in keysets_by_unit { + let mut keysets = keysets; + keysets.sort_by(|a, b| b.derivation_path_index.cmp(&a.derivation_path_index)); + + let highest_index_keyset = keysets + .first() + .cloned() + .expect("unit will not be added to hashmap if empty"); + + let keysets: Vec = keysets + .into_iter() + .filter(|ks| ks.derivation_path_index.is_some()) + .collect(); + + if let Some((input_fee_ppk, max_order)) = supported_units.get(&unit) { + let derivation_path_index = if keysets.is_empty() { + 1 + } else if &highest_index_keyset.input_fee_ppk == input_fee_ppk + && &highest_index_keyset.max_order == max_order + { + let id = highest_index_keyset.id; + let keyset = MintKeySet::generate_from_xpriv( + &secp_ctx, + xpriv, + highest_index_keyset.max_order, + highest_index_keyset.unit.clone(), + highest_index_keyset.derivation_path.clone(), + ); + active_keysets.insert(id, keyset); + let mut keyset_info = highest_index_keyset; + keyset_info.active = true; + localstore.add_keyset_info(keyset_info).await?; + localstore.set_active_keyset(unit, id).await?; + continue; + } else { + highest_index_keyset.derivation_path_index.unwrap_or(0) + 1 + }; + + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit.clone(), derivation_path_index) + .ok_or(Error::UnsupportedUnit)?, + }; + + let (keyset, keyset_info) = create_new_keyset( + &secp_ctx, + xpriv, + derivation_path, + Some(derivation_path_index), + unit.clone(), + *max_order, + *input_fee_ppk, + ); + + let id = keyset_info.id; + localstore.add_keyset_info(keyset_info).await?; + localstore.set_active_keyset(unit.clone(), id).await?; + active_keysets.insert(id, keyset); + active_keyset_units.push(unit.clone()); + } + } + } + + for (unit, (fee, max_order)) in supported_units { + if !active_keyset_units.contains(&unit) { + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => { + derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)? + } + }; + + let (keyset, keyset_info) = create_new_keyset( + &secp_ctx, + xpriv, + derivation_path, + Some(0), + unit.clone(), + max_order, + fee, + ); + + let id = keyset_info.id; + localstore.add_keyset_info(keyset_info).await?; + localstore.set_active_keyset(unit, id).await?; + active_keysets.insert(id, keyset); + } + } + + Ok(Self { + keysets: RwLock::new(HashMap::new()), + secp_ctx, + localstore, + xpriv, + }) + } +} + +impl MemorySignatory { + fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet { + MintKeySet::generate_from_xpriv( + &self.secp_ctx, + self.xpriv, + keyset_info.max_order, + keyset_info.unit, + keyset_info.derivation_path, + ) + } + + async fn load_and_get_keyset(&self, id: &Id) -> Result { + let keysets = self.keysets.read().await; + let keyset_info = self + .localstore + .get_keyset_info(id) + .await? + .ok_or(Error::UnknownKeySet)?; + + if keysets.contains_key(id) { + return Ok(keyset_info); + } + drop(keysets); + + let id = keyset_info.id; + let mut keysets = self.keysets.write().await; + keysets.insert(id, self.generate_keyset(keyset_info.clone())); + Ok(keyset_info) + } + + #[tracing::instrument(skip(self))] + async fn get_keypair_for_amount( + &self, + keyset_id: &Id, + amount: &Amount, + ) -> Result { + let keyset_info = self.load_and_get_keyset(keyset_id).await?; + let active = self + .localstore + .get_active_keyset_id(&keyset_info.unit) + .await? + .ok_or(Error::InactiveKeyset)?; + + // Check that the keyset is active and should be used to sign + if keyset_info.id != active { + return Err(Error::InactiveKeyset); + } + + let keysets = self.keysets.read().await; + let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?; + + match keyset.keys.get(amount) { + Some(key_pair) => Ok(key_pair.clone()), + None => Err(Error::AmountKey), + } + } +} + +#[async_trait::async_trait] +impl Signatory for MemorySignatory { + async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result { + let BlindedMessage { + amount, + blinded_secret, + keyset_id, + .. + } = blinded_message; + let key_pair = self.get_keypair_for_amount(&keyset_id, &amount).await?; + let c = sign_message(&key_pair.secret_key, &blinded_secret)?; + + let blinded_signature = BlindSignature::new( + amount, + c, + keyset_id, + &blinded_message.blinded_secret, + key_pair.secret_key, + )?; + + Ok(blinded_signature) + } + + async fn verify_proof(&self, proof: Proof) -> Result<(), Error> { + // Check if secret is a nut10 secret with conditions + if let Ok(secret) = + <&crate::secret::Secret as TryInto>::try_into(&proof.secret) + { + // Checks and verifies known secret kinds. + // If it is an unknown secret kind it will be treated as a normal secret. + // Spending conditions will **not** be check. It is up to the wallet to ensure + // only supported secret kinds are used as there is no way for the mint to + // enforce only signing supported secrets as they are blinded at + // that point. + match secret.kind { + Kind::P2PK => { + proof.verify_p2pk()?; + } + Kind::HTLC => { + proof.verify_htlc()?; + } + } + } + + let key_pair = self + .get_keypair_for_amount(&proof.keyset_id, &proof.amount) + .await?; + + verify_message(&key_pair.secret_key, proof.c, proof.secret.as_bytes())?; + + Ok(()) + } + + async fn keyset(&self, keyset_id: Id) -> Result, Error> { + self.load_and_get_keyset(&keyset_id).await?; + Ok(self + .keysets + .read() + .await + .get(&keyset_id) + .map(|k| k.clone().into())) + } + + async fn keyset_pubkeys(&self, keyset_id: Id) -> Result { + self.load_and_get_keyset(&keyset_id).await?; + Ok(KeysResponse { + keysets: vec![self + .keysets + .read() + .await + .get(&keyset_id) + .ok_or(Error::UnknownKeySet)? + .clone() + .into()], + }) + } + + async fn pubkeys(&self) -> Result { + let active_keysets = self.localstore.get_active_keysets().await?; + let active_keysets: HashSet<&Id> = active_keysets.values().collect(); + for id in active_keysets.iter() { + let _ = self.load_and_get_keyset(id).await?; + } + let keysets = self.keysets.read().await; + Ok(KeysResponse { + keysets: keysets + .values() + .filter_map(|k| match active_keysets.contains(&k.id) { + true => Some(k.clone().into()), + false => None, + }) + .collect(), + }) + } + + async fn keysets(&self) -> Result { + let keysets = self.localstore.get_keyset_infos().await?; + let active_keysets: HashSet = self + .localstore + .get_active_keysets() + .await? + .values() + .cloned() + .collect(); + + Ok(KeysetResponse { + keysets: keysets + .into_iter() + .map(|k| KeySetInfo { + id: k.id, + unit: k.unit, + active: active_keysets.contains(&k.id), + input_fee_ppk: k.input_fee_ppk, + }) + .collect(), + }) + } + + /// Add current keyset to inactive keysets + /// Generate new keyset + #[tracing::instrument(skip(self))] + async fn rotate_keyset( + &self, + unit: CurrencyUnit, + derivation_path_index: u32, + max_order: u8, + input_fee_ppk: u64, + custom_paths: HashMap, + ) -> Result { + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit.clone(), derivation_path_index) + .ok_or(Error::UnsupportedUnit)?, + }; + + let (keyset, keyset_info) = create_new_keyset( + &self.secp_ctx, + self.xpriv, + derivation_path, + Some(derivation_path_index), + unit.clone(), + max_order, + input_fee_ppk, + ); + let id = keyset_info.id; + self.localstore.add_keyset_info(keyset_info.clone()).await?; + self.localstore.set_active_keyset(unit, id).await?; + + let mut keysets = self.keysets.write().await; + keysets.insert(id, keyset); + + Ok(keyset_info) + } + + async fn get_keyset_info(&self, keyset_id: KeysetIdentifier) -> Result { + let keyset_id = match keyset_id { + KeysetIdentifier::Id(id) => id, + KeysetIdentifier::Unit(unit) => self + .localstore + .get_active_keyset_id(&unit) + .await? + .ok_or(Error::UnsupportedUnit)?, + }; + + self.localstore + .get_keyset_info(&keyset_id) + .await? + .ok_or(Error::UnknownKeySet) + } +} + +#[cfg(test)] +mod test { + use bitcoin::key::Secp256k1; + use bitcoin::Network; + use cdk_common::MintKeySet; + use nuts::PublicKey; + + use super::*; + + #[test] + fn mint_mod_generate_keyset_from_seed() { + let seed = "test_seed".as_bytes(); + let keyset = MintKeySet::generate_from_seed( + &Secp256k1::new(), + seed, + 2, + CurrencyUnit::Sat, + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), + ); + + assert_eq!(keyset.unit, CurrencyUnit::Sat); + assert_eq!(keyset.keys.len(), 2); + + let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ + ( + Amount::from(1), + PublicKey::from_hex( + "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", + ) + .unwrap(), + ), + ( + Amount::from(2), + PublicKey::from_hex( + "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", + ) + .unwrap(), + ), + ] + .into_iter() + .collect(); + + let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset + .keys + .iter() + .map(|(amount, pair)| (*amount, pair.public_key)) + .collect(); + + assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); + } + + #[test] + fn mint_mod_generate_keyset_from_xpriv() { + let seed = "test_seed".as_bytes(); + let network = Network::Bitcoin; + let xpriv = Xpriv::new_master(network, seed).expect("Failed to create xpriv"); + let keyset = MintKeySet::generate_from_xpriv( + &Secp256k1::new(), + xpriv, + 2, + CurrencyUnit::Sat, + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), + ); + + assert_eq!(keyset.unit, CurrencyUnit::Sat); + assert_eq!(keyset.keys.len(), 2); + + let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ + ( + Amount::from(1), + PublicKey::from_hex( + "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", + ) + .unwrap(), + ), + ( + Amount::from(2), + PublicKey::from_hex( + "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", + ) + .unwrap(), + ), + ] + .into_iter() + .collect(); + + let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset + .keys + .iter() + .map(|(amount, pair)| (*amount, pair.public_key)) + .collect(); + + assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); + } +} diff --git a/crates/cdk-signatory/src/proto/client.rs b/crates/cdk-signatory/src/proto/client.rs new file mode 100644 index 000000000..5b1847b86 --- /dev/null +++ b/crates/cdk-signatory/src/proto/client.rs @@ -0,0 +1,72 @@ +use std::collections::HashMap; + +use bitcoin::bip32::DerivationPath; +use cdk_common::error::Error; +use cdk_common::mint::MintKeySetInfo; +use cdk_common::signatory::{KeysetIdentifier, Signatory}; +use cdk_common::{ + BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Proof, +}; + +use crate::proto::signatory_client::SignatoryClient; + +/// A client for the Signatory service. +pub struct RemoteSigner { + client: SignatoryClient, +} + +impl RemoteSigner { + /// Create a new RemoteSigner from a tonic transport channel. + pub async fn new(url: String) -> Result { + Ok(Self { + client: SignatoryClient::connect(url).await?, + }) + } +} + +#[async_trait::async_trait] +impl Signatory for RemoteSigner { + async fn blind_sign(&self, request: BlindedMessage) -> Result { + let req: super::BlindedMessage = request.into(); + self.client + .clone() + .blind_sign(req) + .await + .map(|response| response.into_inner().try_into()) + .map_err(|e| Error::Custom(e.to_string()))? + } + + async fn verify_proof(&self, _proof: Proof) -> Result<(), Error> { + todo!() + } + async fn keyset(&self, _keyset_id: Id) -> Result, Error> { + todo!() + } + + async fn keyset_pubkeys(&self, _keyset_id: Id) -> Result { + todo!() + } + + async fn pubkeys(&self) -> Result { + todo!() + } + + async fn keysets(&self) -> Result { + todo!() + } + + async fn get_keyset_info(&self, _keyset_id: KeysetIdentifier) -> Result { + todo!() + } + + async fn rotate_keyset( + &self, + _unit: CurrencyUnit, + _derivation_path_index: u32, + _max_order: u8, + _input_fee_ppk: u64, + _custom_paths: HashMap, + ) -> Result { + todo!() + } +} diff --git a/crates/cdk-signatory/src/proto/mod.rs b/crates/cdk-signatory/src/proto/mod.rs new file mode 100644 index 000000000..10ffd44f7 --- /dev/null +++ b/crates/cdk-signatory/src/proto/mod.rs @@ -0,0 +1,119 @@ +use cdk_common::{HTLCWitness, P2PKWitness}; +use tonic::Status; + +tonic::include_proto!("cdk_signatory"); + +pub mod client; +pub mod server; + +impl From for BlindedMessage { + fn from(value: cdk_common::BlindedMessage) -> Self { + BlindedMessage { + amount: value.amount.into(), + keyset_id: value.keyset_id.to_string(), + blinded_secret: value.blinded_secret.to_bytes().to_vec(), + witness: value.witness.map(|x| x.into()), + } + } +} + +impl TryInto for BlindedMessage { + type Error = Status; + fn try_into(self) -> Result { + Ok(cdk_common::BlindedMessage { + amount: self.amount.into(), + keyset_id: self + .keyset_id + .parse() + .map_err(|e| Status::from_error(Box::new(e)))?, + blinded_secret: cdk_common::PublicKey::from_slice(&self.blinded_secret) + .map_err(|e| Status::from_error(Box::new(e)))?, + witness: self.witness.map(|x| x.try_into()).transpose()?, + }) + } +} + +impl From for BlindSignatureDleq { + fn from(value: cdk_common::BlindSignatureDleq) -> Self { + BlindSignatureDleq { + e: value.e.as_secret_bytes().to_vec(), + s: value.s.as_secret_bytes().to_vec(), + } + } +} + +impl TryInto for BlindSignatureDleq { + type Error = cdk_common::error::Error; + fn try_into(self) -> Result { + Ok(cdk_common::BlindSignatureDleq { + e: cdk_common::SecretKey::from_slice(&self.e)?, + s: cdk_common::SecretKey::from_slice(&self.s)?, + }) + } +} + +impl From for BlindSignature { + fn from(value: cdk_common::BlindSignature) -> Self { + BlindSignature { + amount: value.amount.into(), + blinded_secret: value.c.to_bytes().to_vec(), + keyset_id: value.keyset_id.to_string(), + dleq: value.dleq.map(|x| x.into()), + } + } +} + +impl TryInto for BlindSignature { + type Error = cdk_common::error::Error; + + fn try_into(self) -> Result { + Ok(cdk_common::BlindSignature { + amount: self.amount.into(), + c: cdk_common::PublicKey::from_slice(&self.blinded_secret)?, + keyset_id: self.keyset_id.parse().expect("Invalid keyset id"), + dleq: self.dleq.map(|dleq| dleq.try_into()).transpose()?, + }) + } +} + +impl From for Witness { + fn from(value: cdk_common::Witness) -> Self { + match value { + cdk_common::Witness::P2PKWitness(P2PKWitness { signatures }) => Witness { + witness_type: Some(witness::WitnessType::P2pkWitness(P2pkWitness { + signatures, + })), + }, + cdk_common::Witness::HTLCWitness(HTLCWitness { + preimage, + signatures, + }) => Witness { + witness_type: Some(witness::WitnessType::HtlcWitness(HtlcWitness { + preimage, + signatures: signatures.unwrap_or_default(), + })), + }, + } + } +} + +impl TryInto for Witness { + type Error = Status; + fn try_into(self) -> Result { + match self.witness_type { + Some(witness::WitnessType::P2pkWitness(P2pkWitness { signatures })) => { + Ok(P2PKWitness { signatures }.into()) + } + Some(witness::WitnessType::HtlcWitness(hltc_witness)) => Ok(HTLCWitness { + preimage: hltc_witness.preimage, + signatures: if hltc_witness.signatures.is_empty() { + None + } else { + Some(hltc_witness.signatures) + }, + } + .into()), + None => Err(Status::invalid_argument("Witness type not set")), + } + } +} diff --git a/crates/cdk-signatory/src/proto/server.rs b/crates/cdk-signatory/src/proto/server.rs new file mode 100644 index 000000000..a594f4fe8 --- /dev/null +++ b/crates/cdk-signatory/src/proto/server.rs @@ -0,0 +1,38 @@ +use std::net::SocketAddr; + +use cdk_common::signatory::Signatory as _; +use tonic::transport::{Error, Server}; +use tonic::{Request, Response, Status}; + +use crate::proto::{self, signatory_server}; +use crate::MemorySignatory; + +struct CdkSignatory(MemorySignatory); + +#[tonic::async_trait] +impl signatory_server::Signatory for CdkSignatory { + async fn blind_sign( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request: {:?}", request); + let blind_signature = self + .0 + .blind_sign(request.into_inner().try_into()?) + .await + .map_err(|e| Status::from_error(Box::new(e)))?; + Ok(Response::new(blind_signature.into())) + } +} + +/// Runs the signatory server +pub async fn grpc_server(signatory: MemorySignatory, addr: SocketAddr) -> Result<(), Error> { + tracing::info!("grpc_server listening on {}", addr); + Server::builder() + .add_service(signatory_server::SignatoryServer::new(CdkSignatory( + signatory, + ))) + .serve(addr) + .await?; + Ok(()) +} diff --git a/crates/cdk-signatory/src/proto/signatory.proto b/crates/cdk-signatory/src/proto/signatory.proto new file mode 100644 index 000000000..6c6105b2d --- /dev/null +++ b/crates/cdk-signatory/src/proto/signatory.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package cdk_signatory; + +service Signatory { + rpc BlindSign (BlindedMessage) returns (BlindSignature); +} + + +message BlindSignature { + uint64 amount = 1; + string keyset_id = 2; + bytes blinded_secret = 3; + optional BlindSignatureDLEQ dleq = 4; +} + +message BlindSignatureDLEQ { + bytes e = 1; + bytes s = 2; +} + + +// Represents a blinded message +message BlindedMessage { + uint64 amount = 1; + string keyset_id = 2; + bytes blinded_secret = 3; + optional Witness witness = 4; // This field is optional by default in proto3 +} + +// Witness type +message Witness { + oneof witness_type { + P2PKWitness p2pk_witness = 1; + HTLCWitness htlc_witness = 2; + } +} + +// P2PKWitness type +message P2PKWitness { + // List of signatures + repeated string signatures = 1; +} + +// HTLCWitness type +message HTLCWitness { + // Preimage + string preimage = 1; + + // List of signatures + repeated string signatures = 2; +} diff --git a/crates/cdk-sqlite/Cargo.toml b/crates/cdk-sqlite/Cargo.toml index df0523d39..9eca26b49 100644 --- a/crates/cdk-sqlite/Cargo.toml +++ b/crates/cdk-sqlite/Cargo.toml @@ -34,4 +34,4 @@ tracing = { version = "0.1", default-features = false, features = [ ] } serde_json = "1" lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } -uuid = { version = "1", features = ["v4", "serde"] } +uuid = { version = "=1.12.1", features = ["v4", "serde"] } diff --git a/crates/cdk-strike/Cargo.toml b/crates/cdk-strike/Cargo.toml index 81a8575c5..b6aa6f968 100644 --- a/crates/cdk-strike/Cargo.toml +++ b/crates/cdk-strike/Cargo.toml @@ -6,7 +6,7 @@ authors = ["CDK Developers"] license = "MIT" homepage = "https://github.com/cashubtc/cdk" repository = "https://github.com/cashubtc/cdk.git" -rust-version = "1.63.0" # MSRV +rust-version = "1.63.0" # MSRV description = "CDK ln backend for Strike api" [dependencies] @@ -14,13 +14,18 @@ async-trait = "0.1" anyhow = "1" axum = "0.6.20" bitcoin = { version = "0.32.2", default-features = false } -cdk = { path = "../cdk", version = "0.6.0", default-features = false, features = ["mint"] } +cdk = { path = "../cdk", version = "0.6.0", default-features = false, features = [ + "mint", +] } futures = { version = "0.3.28", default-features = false } tokio = { version = "1", default-features = false } tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } thiserror = "1" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "=1.12.1", features = ["v4"] } strike-rs = "0.4.0" # strike-rs = { path = "../../../../strike-rs" } # strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "577ad9591" } diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index d72162c99..74f6cacc3 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -11,10 +11,12 @@ license = "MIT" [features] -default = ["mint", "wallet"] -mint = ["dep:futures", "cdk-common/mint"] +mint = ["dep:futures", "cdk-common/mint", "cdk-signatory"] # We do not commit to a MSRV with swagger enabled swagger = ["mint", "dep:utoipa", "cdk-common/swagger"] +# We do not commit to a MSRV with grpc enabled +grpc = ["mint", "cdk-signatory/grpc"] +# We do not commit to a MSRV with grpc enabled wallet = ["dep:reqwest", "cdk-common/wallet"] bench = [] http_subscription = [] @@ -22,6 +24,7 @@ http_subscription = [] [dependencies] cdk-common = { path = "../cdk-common", version = "0.6.1" } +cdk-signatory = { path = "../cdk-signatory", default-features = false, optional = true } cbor-diag = "0.1.12" async-trait = "0.1" anyhow = { version = "1.0.43", features = ["backtrace"] } @@ -62,6 +65,7 @@ uuid = { version = "=1.12.1", features = ["v4", "serde"] } # -Z minimal-versions sync_wrapper = "0.1.2" bech32 = "0.9.1" +paste = "1.0.15" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.21", features = [ @@ -105,3 +109,4 @@ criterion = "0.5.1" [[bench]] name = "dhke_benchmarks" harness = false +default = ["mint", "wallet"] diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index 5ab52befa..bcfc51bae 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -5,9 +5,11 @@ use std::sync::Arc; use anyhow::anyhow; use cdk_common::database::{self, MintDatabase}; +use cdk_common::signatory::Signatory; use super::nut17::SupportedMethods; use super::nut19::{self, CachedEndpoint}; +use super::signatory::SignatoryManager; use super::Nuts; use crate::amount::Amount; use crate::cdk_lightning::{self, MintLightning}; @@ -18,8 +20,14 @@ use crate::nuts::{ }; use crate::types::{LnKey, QuoteTTL}; +#[derive(Clone, Debug)] +pub enum SignatoryInfo { + Seed(Vec), + Remote(String), +} + /// Cashu Mint -#[derive(Default)] +#[derive(Default, Clone)] pub struct MintBuilder { /// Mint Info mint_info: MintInfo, @@ -27,9 +35,11 @@ pub struct MintBuilder { localstore: Option + Send + Sync>>, /// Ln backends for mint ln: Option + Send + Sync>>>, - seed: Option>, + signatory_info: Option, quote_ttl: Option, - supported_units: HashMap, + /// expose supported units + pub supported_units: HashMap, + signatory: Option>, } impl MintBuilder { @@ -52,6 +62,12 @@ impl MintBuilder { builder } + /// Set signatory service + pub fn with_signatory(mut self, signatory: Arc) -> Self { + self.signatory = Some(signatory); + self + } + /// Set localstore pub fn with_localstore( mut self, @@ -61,9 +77,15 @@ impl MintBuilder { self } - /// Set seed + /// Set seed to create a local signatory pub fn with_seed(mut self, seed: Vec) -> Self { - self.seed = Some(seed); + self.signatory_info = Some(SignatoryInfo::Seed(seed)); + self + } + + /// connect to a remote signatary instead of a creating a local one + pub fn with_remote_signatory(mut self, url: String) -> Self { + self.signatory_info = Some(SignatoryInfo::Remote(url)); self } @@ -217,22 +239,51 @@ impl MintBuilder { } /// Build mint - pub async fn build(&self) -> anyhow::Result { - let localstore = self - .localstore - .clone() - .ok_or(anyhow!("Localstore not set"))?; + pub async fn build(self) -> anyhow::Result { + let localstore = self.localstore.ok_or(anyhow!("Localstore not set"))?; localstore.set_mint_info(self.mint_info.clone()).await?; localstore .set_quote_ttl(self.quote_ttl.ok_or(anyhow!("Quote ttl not set"))?) .await?; + let signatory = if let Some(signatory) = self.signatory { + signatory + } else { + match self.signatory_info { + Some(SignatoryInfo::Seed(seed)) => Arc::new( + cdk_signatory::MemorySignatory::new( + localstore.clone(), + &seed, + self.supported_units, + HashMap::new(), + ) + .await?, + ) + as Arc, + #[cfg(feature = "grpc")] + Some(SignatoryInfo::Remote(url)) => Arc::new( + cdk_signatory::RemoteSigner::new(url) + .await + .map_err(|e| anyhow!("Remote signatory error: {}", e.to_string()))?, + ) + as Arc, + #[cfg(not(feature = "grpc"))] + Some(SignatoryInfo::Remote(url)) => panic!( + "CDK not compiled with grpc feature, therefore the remote signatory is disabled (url={})", url + ), + None => { + return Err(anyhow!("Signatory not set")); + } + } + }; + + let signatory_manager = Arc::new(SignatoryManager::new(signatory)); + Ok(Mint::new( - self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?, localstore, self.ln.clone().ok_or(anyhow!("Ln backends not set"))?, - self.supported_units.clone(), + signatory_manager, HashMap::new(), ) .await?) diff --git a/crates/cdk/src/mint/config.rs b/crates/cdk/src/mint/config.rs new file mode 100644 index 000000000..de745d135 --- /dev/null +++ b/crates/cdk/src/mint/config.rs @@ -0,0 +1,103 @@ +//! Active mint configuration +//! +//! This is the active configuration that can be updated at runtime. +use std::sync::Arc; + +use arc_swap::ArcSwap; + +use super::MintInfo; +use crate::mint_url::MintUrl; +use crate::types::QuoteTTL; + +/// Mint Inner configuration +pub struct Config { + /// Mint url + pub mint_info: MintInfo, + /// Mint config + pub mint_url: MintUrl, + /// Quotes ttl + pub quote_ttl: QuoteTTL, +} + +/// Mint configuration +/// +/// This struct is used to configure the mint, and it is wrapped inside a ArcSwap, so it can be +/// updated at runtime without locking the shared config nor without requiriming a mutable reference +/// to the config +/// +/// ArcSwap is used instead of a RwLock since the updates should be less frequent than the reads +#[derive(Clone)] +pub struct SwappableConfig { + config: Arc>, +} + +impl SwappableConfig { + /// Creates a new configuration instance + pub fn new(mint_url: MintUrl, quote_ttl: QuoteTTL, mint_info: MintInfo) -> Self { + let inner = Config { + quote_ttl, + mint_info, + mint_url, + }; + + Self { + config: Arc::new(ArcSwap::from_pointee(inner)), + } + } + + /// Gets an Arc of the current configuration + pub fn load(&self) -> Arc { + self.config.load().clone() + } + + /// Gets a copy of the mint url + pub fn mint_url(&self) -> MintUrl { + self.load().mint_url.clone() + } + + /// Replace the current mint url with a new one + pub fn set_mint_url(&self, mint_url: MintUrl) { + let current_inner = self.load(); + let new_inner = Config { + mint_url, + quote_ttl: current_inner.quote_ttl, + mint_info: current_inner.mint_info.clone(), + }; + + self.config.store(Arc::new(new_inner)); + } + + /// Gets a copy of the quote ttl + pub fn quote_ttl(&self) -> QuoteTTL { + self.load().quote_ttl + } + + /// Replaces the current quote ttl with a new one + pub fn set_quote_ttl(&self, quote_ttl: QuoteTTL) { + let current_inner = self.load(); + let new_inner = Config { + mint_info: current_inner.mint_info.clone(), + mint_url: current_inner.mint_url.clone(), + quote_ttl, + }; + + self.config.store(Arc::new(new_inner)); + } + + /// Gets a copy of the mint info + pub fn mint_info(&self) -> MintInfo { + self.load().mint_info.clone() + } + + /// Replaces the current mint info with a new one + pub fn set_mint_info(&self, mint_info: MintInfo) { + let current_inner = self.load(); + let new_inner = Config { + mint_info, + mint_url: current_inner.mint_url.clone(), + quote_ttl: current_inner.quote_ttl, + }; + + self.config.store(Arc::new(new_inner)); + } +} diff --git a/crates/cdk/src/mint/keysets.rs b/crates/cdk/src/mint/keysets.rs index 7dd96bee9..9ae057bed 100644 --- a/crates/cdk/src/mint/keysets.rs +++ b/crates/cdk/src/mint/keysets.rs @@ -1,12 +1,10 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use bitcoin::bip32::DerivationPath; +use cdk_common::mint::MintKeySetInfo; use tracing::instrument; -use super::{ - create_new_keyset, derivation_path_from_unit, CurrencyUnit, Id, KeySet, KeySetInfo, - KeysResponse, KeysetResponse, Mint, MintKeySet, MintKeySetInfo, -}; +use super::{CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Mint}; use crate::Error; impl Mint { @@ -14,77 +12,26 @@ impl Mint { /// clients #[instrument(skip(self))] pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result { - self.ensure_keyset_loaded(keyset_id).await?; - let keyset = self - .keysets - .read() - .await - .get(keyset_id) - .ok_or(Error::UnknownKeySet)? - .clone(); - Ok(KeysResponse { - keysets: vec![keyset.into()], - }) + self.signatory.keyset_pubkeys(keyset_id.to_owned()).await } /// Retrieve the public keys of the active keyset for distribution to wallet /// clients #[instrument(skip_all)] pub async fn pubkeys(&self) -> Result { - let active_keysets = self.localstore.get_active_keysets().await?; - - let active_keysets: HashSet<&Id> = active_keysets.values().collect(); - - for id in active_keysets.iter() { - self.ensure_keyset_loaded(id).await?; - } - - Ok(KeysResponse { - keysets: self - .keysets - .read() - .await - .values() - .filter_map(|k| match active_keysets.contains(&k.id) { - true => Some(k.clone().into()), - false => None, - }) - .collect(), - }) + self.signatory.pubkeys().await } /// Return a list of all supported keysets #[instrument(skip_all)] pub async fn keysets(&self) -> Result { - let keysets = self.localstore.get_keyset_infos().await?; - let active_keysets: HashSet = self - .localstore - .get_active_keysets() - .await? - .values() - .cloned() - .collect(); - - let keysets = keysets - .into_iter() - .map(|k| KeySetInfo { - id: k.id, - unit: k.unit, - active: active_keysets.contains(&k.id), - input_fee_ppk: k.input_fee_ppk, - }) - .collect(); - - Ok(KeysetResponse { keysets }) + self.signatory.keysets().await } /// Get keysets #[instrument(skip(self))] pub async fn keyset(&self, id: &Id) -> Result, Error> { - self.ensure_keyset_loaded(id).await?; - let keysets = self.keysets.read().await; - let keyset = keysets.get(id).map(|k| k.clone().into()); - Ok(keyset) + self.signatory.keyset(id.to_owned()).await } /// Add current keyset to inactive keysets @@ -98,29 +45,15 @@ impl Mint { input_fee_ppk: u64, custom_paths: &HashMap, ) -> Result { - let derivation_path = match custom_paths.get(&unit) { - Some(path) => path.clone(), - None => derivation_path_from_unit(unit.clone(), derivation_path_index) - .ok_or(Error::UnsupportedUnit)?, - }; - - let (keyset, keyset_info) = create_new_keyset( - &self.secp_ctx, - self.xpriv, - derivation_path, - Some(derivation_path_index), - unit.clone(), - max_order, - input_fee_ppk, - ); - let id = keyset_info.id; - self.localstore.add_keyset_info(keyset_info.clone()).await?; - self.localstore.set_active_keyset(unit, id).await?; - - let mut keysets = self.keysets.write().await; - keysets.insert(id, keyset); - - Ok(keyset_info) + self.signatory + .rotate_keyset( + unit, + derivation_path_index, + max_order, + input_fee_ppk, + custom_paths.to_owned(), + ) + .await } /// Rotate to next keyset for unit @@ -131,17 +64,7 @@ impl Mint { max_order: u8, input_fee_ppk: u64, ) -> Result { - let current_keyset_id = self - .localstore - .get_active_keyset_id(&unit) - .await? - .ok_or(Error::UnsupportedUnit)?; - - let keyset_info = self - .localstore - .get_keyset_info(¤t_keyset_id) - .await? - .ok_or(Error::UnknownKeySet)?; + let keyset_info = self.signatory.get_keyset_info(unit.clone().into()).await?; tracing::debug!( "Current active keyset {} path index {:?}", @@ -149,50 +72,14 @@ impl Mint { keyset_info.derivation_path_index ); - let keyset_info = self + self.signatory .rotate_keyset( unit, keyset_info.derivation_path_index.unwrap_or(1) + 1, max_order, input_fee_ppk, - &self.custom_paths, + self.custom_paths.to_owned(), ) - .await?; - - Ok(keyset_info) - } - - /// Ensure Keyset is loaded in mint - #[instrument(skip(self))] - pub async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> { - { - let keysets = self.keysets.read().await; - if keysets.contains_key(id) { - return Ok(()); - } - } - - let mut keysets = self.keysets.write().await; - let keyset_info = self - .localstore - .get_keyset_info(id) - .await? - .ok_or(Error::UnknownKeySet)?; - let id = keyset_info.id; - keysets.insert(id, self.generate_keyset(keyset_info)); - - Ok(()) - } - - /// Generate [`MintKeySet`] from [`MintKeySetInfo`] - #[instrument(skip_all)] - pub fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet { - MintKeySet::generate_from_xpriv( - &self.secp_ctx, - self.xpriv, - keyset_info.max_order, - keyset_info.unit, - keyset_info.derivation_path, - ) + .await } } diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 64417ba67..c1211eba7 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -3,25 +3,22 @@ use std::collections::HashMap; use std::sync::Arc; -use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; -use bitcoin::secp256k1::{self, Secp256k1}; +use bitcoin::bip32::DerivationPath; use cdk_common::common::LnKey; use cdk_common::database::{self, MintDatabase}; -use cdk_common::mint::MintKeySetInfo; use futures::StreamExt; use serde::{Deserialize, Serialize}; +use signatory::SignatoryManager; use subscription::PubSubManager; -use tokio::sync::{Notify, RwLock}; +use tokio::sync::Notify; use tokio::task::JoinSet; use tracing::instrument; use uuid::Uuid; use crate::cdk_lightning::{self, MintLightning}; -use crate::dhke::{sign_message, verify_message}; use crate::error::Error; use crate::fees::calculate_fee; use crate::nuts::*; -use crate::util::unix_time; use crate::Amount; mod builder; @@ -29,12 +26,17 @@ mod check_spendable; mod keysets; mod melt; mod mint_nut04; +pub mod signatory; mod start_up_check; pub mod subscription; mod swap; +/// re-export types pub use builder::{MintBuilder, MintMeltLimits}; pub use cdk_common::mint::{MeltQuote, MintQuote}; +#[cfg(feature = "grpc")] +pub use cdk_signatory::proto::client::RemoteSigner; +pub use cdk_signatory::MemorySignatory; /// Cashu Mint #[derive(Clone)] @@ -45,9 +47,9 @@ pub struct Mint { pub ln: HashMap + Send + Sync>>, /// Subscription manager pub pubsub_manager: Arc, - secp_ctx: Secp256k1, - xpriv: Xpriv, - keysets: Arc>>, + /// Signatory + pub signatory: Arc, + /// Custom paths to be used for keyset generation custom_paths: HashMap, } @@ -55,136 +57,16 @@ impl Mint { /// Create new [`Mint`] #[allow(clippy::too_many_arguments)] pub async fn new( - seed: &[u8], localstore: Arc + Send + Sync>, ln: HashMap + Send + Sync>>, - // Hashmap where the key is the unit and value is (input fee ppk, max_order) - supported_units: HashMap, + signatory: Arc, custom_paths: HashMap, ) -> Result { - let secp_ctx = Secp256k1::new(); - let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); - - let mut active_keysets = HashMap::new(); - let keysets_infos = localstore.get_keyset_infos().await?; - - let mut active_keyset_units = vec![]; - - if !keysets_infos.is_empty() { - tracing::debug!("Setting all saved keysets to inactive"); - for keyset in keysets_infos.clone() { - // Set all to in active - let mut keyset = keyset; - keyset.active = false; - localstore.add_keyset_info(keyset).await?; - } - - let keysets_by_unit: HashMap> = - keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| { - acc.entry(ks.unit.clone()).or_default().push(ks.clone()); - acc - }); - - for (unit, keysets) in keysets_by_unit { - let mut keysets = keysets; - keysets.sort_by(|a, b| b.derivation_path_index.cmp(&a.derivation_path_index)); - - let highest_index_keyset = keysets - .first() - .cloned() - .expect("unit will not be added to hashmap if empty"); - - let keysets: Vec = keysets - .into_iter() - .filter(|ks| ks.derivation_path_index.is_some()) - .collect(); - - if let Some((input_fee_ppk, max_order)) = supported_units.get(&unit) { - let derivation_path_index = if keysets.is_empty() { - 1 - } else if &highest_index_keyset.input_fee_ppk == input_fee_ppk - && &highest_index_keyset.max_order == max_order - { - 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?; - continue; - } else { - highest_index_keyset.derivation_path_index.unwrap_or(0) + 1 - }; - - let derivation_path = match custom_paths.get(&unit) { - Some(path) => path.clone(), - None => derivation_path_from_unit(unit.clone(), derivation_path_index) - .ok_or(Error::UnsupportedUnit)?, - }; - - let (keyset, keyset_info) = create_new_keyset( - &secp_ctx, - xpriv, - derivation_path, - Some(derivation_path_index), - unit.clone(), - *max_order, - *input_fee_ppk, - ); - - let id = keyset_info.id; - localstore.add_keyset_info(keyset_info).await?; - localstore.set_active_keyset(unit.clone(), id).await?; - active_keysets.insert(id, keyset); - active_keyset_units.push(unit.clone()); - } - } - } - - for (unit, (fee, max_order)) in supported_units { - if !active_keyset_units.contains(&unit) { - let derivation_path = match custom_paths.get(&unit) { - Some(path) => path.clone(), - None => { - derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)? - } - }; - - let (keyset, keyset_info) = create_new_keyset( - &secp_ctx, - xpriv, - derivation_path, - Some(0), - unit.clone(), - max_order, - fee, - ); - - let id = keyset_info.id; - localstore.add_keyset_info(keyset_info).await?; - localstore.set_active_keyset(unit, id).await?; - active_keysets.insert(id, keyset); - } - } - - let keysets = Arc::new(RwLock::new(active_keysets)); - Ok(Self { pubsub_manager: Arc::new(localstore.clone().into()), - secp_ctx, - xpriv, localstore, ln, - keysets, + signatory, custom_paths, }) } @@ -287,87 +169,13 @@ impl Mint { &self, blinded_message: &BlindedMessage, ) -> Result { - let BlindedMessage { - amount, - blinded_secret, - keyset_id, - .. - } = blinded_message; - self.ensure_keyset_loaded(keyset_id).await?; - - let keyset_info = self - .localstore - .get_keyset_info(keyset_id) - .await? - .ok_or(Error::UnknownKeySet)?; - - let active = self - .localstore - .get_active_keyset_id(&keyset_info.unit) - .await? - .ok_or(Error::InactiveKeyset)?; - - // Check that the keyset is active and should be used to sign - if keyset_info.id.ne(&active) { - return Err(Error::InactiveKeyset); - } - - let keysets = self.keysets.read().await; - let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?; - - let key_pair = match keyset.keys.get(amount) { - Some(key_pair) => key_pair, - None => return Err(Error::AmountKey), - }; - - let c = sign_message(&key_pair.secret_key, blinded_secret)?; - - let blinded_signature = BlindSignature::new( - *amount, - c, - keyset_info.id, - &blinded_message.blinded_secret, - key_pair.secret_key.clone(), - )?; - - Ok(blinded_signature) + self.signatory.blind_sign(blinded_message.to_owned()).await } /// Verify [`Proof`] meets conditions and is signed #[instrument(skip_all)] pub async fn verify_proof(&self, proof: &Proof) -> Result<(), Error> { - // Check if secret is a nut10 secret with conditions - if let Ok(secret) = - <&crate::secret::Secret as TryInto>::try_into(&proof.secret) - { - // Checks and verifies known secret kinds. - // If it is an unknown secret kind it will be treated as a normal secret. - // Spending conditions will **not** be check. It is up to the wallet to ensure - // only supported secret kinds are used as there is no way for the mint to - // enforce only signing supported secrets as they are blinded at - // that point. - match secret.kind { - Kind::P2PK => { - proof.verify_p2pk()?; - } - Kind::HTLC => { - proof.verify_htlc()?; - } - } - } - - self.ensure_keyset_loaded(&proof.keyset_id).await?; - let keysets = self.keysets.read().await; - let keyset = keysets.get(&proof.keyset_id).ok_or(Error::UnknownKeySet)?; - - let keypair = match keyset.keys.get(&proof.amount) { - Some(key_pair) => key_pair, - None => return Err(Error::AmountKey), - }; - - verify_message(&keypair.secret_key, proof.c, proof.secret.as_bytes())?; - - Ok(()) + self.signatory.verify_proof(proof.to_owned()).await } /// Verify melt request is valid @@ -513,148 +321,18 @@ pub struct FeeReserve { pub percent_fee_reserve: f32, } -/// Generate new [`MintKeySetInfo`] from path -#[instrument(skip_all)] -fn create_new_keyset( - secp: &secp256k1::Secp256k1, - xpriv: Xpriv, - derivation_path: DerivationPath, - derivation_path_index: Option, - unit: CurrencyUnit, - max_order: u8, - input_fee_ppk: u64, -) -> (MintKeySet, MintKeySetInfo) { - let keyset = MintKeySet::generate( - secp, - xpriv - .derive_priv(secp, &derivation_path) - .expect("RNG busted"), - unit, - max_order, - ); - let keyset_info = MintKeySetInfo { - id: keyset.id, - unit: keyset.unit.clone(), - active: true, - valid_from: unix_time(), - valid_to: None, - derivation_path, - derivation_path_index, - max_order, - input_fee_ppk, - }; - (keyset, keyset_info) -} - -fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option { - let unit_index = unit.derivation_index()?; - - Some(DerivationPath::from(vec![ - ChildNumber::from_hardened_idx(0).expect("0 is a valid index"), - ChildNumber::from_hardened_idx(unit_index).expect("0 is a valid index"), - ChildNumber::from_hardened_idx(index).expect("0 is a valid index"), - ])) -} - #[cfg(test)] mod tests { - use std::collections::HashSet; use std::str::FromStr; - use bitcoin::Network; use cdk_common::common::{LnKey, QuoteTTL}; - use secp256k1::Secp256k1; + use cdk_common::mint::MintKeySetInfo; + use cdk_signatory::MemorySignatory; use uuid::Uuid; use super::*; use crate::cdk_database::mint_memory::MintMemoryDatabase; - #[test] - fn mint_mod_generate_keyset_from_seed() { - let seed = "test_seed".as_bytes(); - let keyset = MintKeySet::generate_from_seed( - &Secp256k1::new(), - seed, - 2, - CurrencyUnit::Sat, - derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), - ); - - assert_eq!(keyset.unit, CurrencyUnit::Sat); - assert_eq!(keyset.keys.len(), 2); - - let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ - ( - Amount::from(1), - PublicKey::from_hex( - "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", - ) - .unwrap(), - ), - ( - Amount::from(2), - PublicKey::from_hex( - "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", - ) - .unwrap(), - ), - ] - .into_iter() - .collect(); - - let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset - .keys - .iter() - .map(|(amount, pair)| (*amount, pair.public_key)) - .collect(); - - assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); - } - - #[test] - fn mint_mod_generate_keyset_from_xpriv() { - let seed = "test_seed".as_bytes(); - let network = Network::Bitcoin; - let xpriv = Xpriv::new_master(network, seed).expect("Failed to create xpriv"); - let keyset = MintKeySet::generate_from_xpriv( - &Secp256k1::new(), - xpriv, - 2, - CurrencyUnit::Sat, - derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), - ); - - assert_eq!(keyset.unit, CurrencyUnit::Sat); - assert_eq!(keyset.keys.len(), 2); - - let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ - ( - Amount::from(1), - PublicKey::from_hex( - "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", - ) - .unwrap(), - ), - ( - Amount::from(2), - PublicKey::from_hex( - "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", - ) - .unwrap(), - ), - ] - .into_iter() - .collect(); - - let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset - .keys - .iter() - .map(|(amount, pair)| (*amount, pair.public_key)) - .collect(); - - assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); - } - #[derive(Default)] struct MintConfig<'a> { active_keysets: HashMap, @@ -692,11 +370,21 @@ mod tests { .unwrap(), ); + let signatory_manager = Arc::new(SignatoryManager::new(Arc::new( + MemorySignatory::new( + localstore.clone(), + config.seed, + config.supported_units, + HashMap::new(), + ) + .await + .expect("valid signatory"), + ))); + Mint::new( - config.seed, localstore, HashMap::new(), - config.supported_units, + signatory_manager, HashMap::new(), ) .await @@ -791,12 +479,27 @@ mod tests { mint.rotate_keyset(CurrencyUnit::default(), 0, 32, 1, &HashMap::new()) .await?; - let keys = mint.keysets.read().await.clone(); + let keys = mint + .signatory + .keyset_pubkeys("005f6e8c540c9e61".parse().expect("valid key")) + .await + .expect("keys"); - let expected_keys = r#"{"005f6e8c540c9e61":{"id":"005f6e8c540c9e61","unit":"sat","keys":{"1":{"public_key":"03e8aded7525acee36e3394e28f2dcbc012533ef2a2b085a55fc291d311afee3ef","secret_key":"32ee9fc0723772aed4c7b8ac0a02ffe390e54a4e0b037ec6035c2afa10ebd873"},"2":{"public_key":"02628c0919e5cb8ce9aed1f81ce313f40e1ab0b33439d5be2abc69d9bb574902e0","secret_key":"48384bf901bbe8f937d601001d067e73b28b435819c009589350c664f9ba872c"},"4":{"public_key":"039e7c7f274e1e8a90c61669e961c944944e6154c0794fccf8084af90252d2848f","secret_key":"1f039c1e54e9e65faae8ecf69492f810b4bb2292beb3734059f2bb4d564786d0"},"8":{"public_key":"02ca0e563ae941700aefcb16a7fb820afbb3258ae924ab520210cb730227a76ca3","secret_key":"ea3c2641d847c9b15c5f32c150b5c9c04d0666af0549e54f51f941cf584442be"},"16":{"public_key":"031dbab0e4f7fb4fb0030f0e1a1dc80668eadd0b1046df3337bb13a7b9c982d392","secret_key":"5b244f8552077e68b30b534e85bd0e8e29ae0108ff47f5cd92522aa524d3288f"},"32":{"public_key":"037241f7ad421374eb764a48e7769b5e2473582316844fda000d6eef28eea8ffb8","secret_key":"95608f61dd690aef34e6a2d4cbef3ad8fddb4537a14480a17512778058e4f5bd"},"64":{"public_key":"02bc9767b4abf88becdac47a59e67ee9a9a80b9864ef57d16084575273ac63c0e7","secret_key":"2e9cd067fafa342f3118bc1e62fbb8e53acdb0f96d51ce8a1e1037e43fad0dce"},"128":{"public_key":"0351e33a076f415c2cadc945bc9bcb75bf4a774b28df8a0605dea1557e5897fed8","secret_key":"7014f27be5e2b77e4951a81c18ae3585d0b037899d8a37b774970427b13d8f65"},"256":{"public_key":"0314b9f4300367c7e64fa85770da90839d2fc2f57d63660f08bb3ebbf90ed76840","secret_key":"1a545bd9c40fc6cf2ab281710e279967e9f4b86cd07761c741da94bc8042c8fb"},"512":{"public_key":"030d95abc7e881d173f4207a3349f4ee442b9e51cc461602d3eb9665b9237e8db3","secret_key":"622984ef16d1cb28e9adc7a7cfea1808d85b4bdabd015977f0320c9f573858b4"},"1024":{"public_key":"0351a68a667c5fc21d66c187baecefa1d65529d06b7ae13112d432b6bca16b0e8c","secret_key":"6a8badfa26129499b60edb96cda4cbcf08f8007589eb558a9d0307bdc56e0ff6"},"2048":{"public_key":"0376166d8dcf97d8b0e9f11867ff0dafd439c90255b36a25be01e37e14741b9c6a","secret_key":"48fe41181636716ce202b3a3303c2475e6d511991930868d907441e1bcbf8566"},"4096":{"public_key":"03d40f47b4e5c4d72f2a977fab5c66b54d945b2836eb888049b1dd9334d1d70304","secret_key":"66a25bf144a3b40c015dd1f630aa4ba81d2242f5aee845e4f378246777b21676"},"8192":{"public_key":"03be18afaf35a29d7bcd5dfd1936d82c1c14691a63f8aa6ece258e16b0c043049b","secret_key":"4ddac662e82f6028888c11bdefd07229d7c1b56987395f106cc9ea5b301695f6"},"16384":{"public_key":"028e9c6ce70f34cd29aad48656bf8345bb5ba2cb4f31fdd978686c37c93f0ab411","secret_key":"83676bd7d047655476baecad2864519f0ffd8e60f779956d2faebcc727caa7bd"},"32768":{"public_key":"0253e34bab4eec93e235c33994e01bf851d5caca4559f07d37b5a5c266de7cf840","secret_key":"d5be522906223f5d92975e2a77f7e166aa121bf93d5fe442d6d132bf67166b04"},"65536":{"public_key":"02684ede207f9ace309b796b5259fc81ef0d4492b4fb5d66cf866b0b4a6f27bec9","secret_key":"20d859b7052d768e007bf285ee11dc0b98a4abfe272a551852b0cce9fb6d5ad4"},"131072":{"public_key":"027cdf7be8b20a49ac7f2f065f7c53764c8926799877858c6b00b888a8aa6741a5","secret_key":"f6eef28183344b32fc0a1fba00cd6cf967614e51d1c990f0bfce8f67c6d9746a"},"262144":{"public_key":"026939b8f766c3ebaf26408e7e54fc833805563e2ef14c8ee4d0435808b005ec4c","secret_key":"690f23e4eaa250c652afeac24d4efb583095a66abf6b87a7f3d17b1f42c5f896"},"524288":{"public_key":"03772542057493a46eed6513b40386e766eedada16560ffde2f776b65794e9f004","secret_key":"fe36e61bea74665f8796b4b62f9501ae6e0d5b16733d2c05c146cd39f89475a0"},"1048576":{"public_key":"02b016346e5a322d371c6e6164b28b31b4d93a51572351ca2f26cdc12e916d9ac3","secret_key":"b9269779e057ce715964caa6d6b5b65672f255e86746e994b6b8c4780cb9d728"},"2097152":{"public_key":"028f25283e36a11df7713934a5287267381f8304aca3c1eb1b89fddce973ef1436","secret_key":"41aec998b9624ddcff97eb7341daa6385b2a8714ed3f12969ef39649f4d641ab"},"4194304":{"public_key":"03e5841d310819a49ec42dfb24839c61f68bbfc93ac68f6dad37fd5b2d204cc535","secret_key":"e5aef2509c56236f004e2df4343beab6406816fb187c3532d4340a9674857c64"},"8388608":{"public_key":"0307ebfeb87b7bca9baa03fad00499e5cc999fa5179ef0b7ad4f555568bcb946f5","secret_key":"369e8dcabcc69a2eabb7363beb66178cafc29e53b02c46cd15374028c3110541"},"16777216":{"public_key":"02f2508e7df981c32f7b0008a273e2a1f19c23bb60a1561dba6b2a95ed1251eb90","secret_key":"f93965b96ed5428bcacd684eff2f43a9777d03adfde867fa0c6efb39c46a7550"},"33554432":{"public_key":"0381883a1517f8c9979a84fcd5f18437b1a2b0020376ecdd2e515dc8d5a157a318","secret_key":"7f5e77c7ed04dff952a7c15564ab551c769243eb65423adfebf46bf54360cd64"},"67108864":{"public_key":"02aa648d39c9a725ef5927db15af6895f0d43c17f0a31faff4406314fc80180086","secret_key":"d34eda86679bf872dfb6faa6449285741bba6c6d582cd9fe5a9152d5752596cc"},"134217728":{"public_key":"0380658e5163fcf274e1ace6c696d1feef4c6068e0d03083d676dc5ef21804f22d","secret_key":"3ad22e92d497309c5b08b2dc01cb5180de3e00d3d703229914906bc847183987"},"268435456":{"public_key":"031526f03de945c638acccb879de837ac3fabff8590057cfb8552ebcf51215f3aa","secret_key":"3a740771e29119b171ab8e79e97499771439e0ab6a082ec96e43baf06a546372"},"536870912":{"public_key":"035eb3e7262e126c5503e1b402db05f87de6556773ae709cb7aa1c3b0986b87566","secret_key":"9b77ee8cd879128c0ea6952dd188e63617fbaa9e66a3bca0244bcceb9b1f7f48"},"1073741824":{"public_key":"03f12e6a0903ed0db87485a296b1dca9d953a8a6919ff88732238fbc672d6bd125","secret_key":"f3947bca4df0f024eade569c81c5c53e167476e074eb81fa6b289e5e10dd4e42"},"2147483648":{"public_key":"02cece3fb38a54581e0646db4b29242b6d78e49313dda46764094f9d128c1059c1","secret_key":"582d54a894cd41441157849e0d16750e5349bd9310776306e7313b255866950b"}}}}"#; + let expected_keys = r#"{"keysets":[{"id":"005f6e8c540c9e61","unit":"sat","keys":{"1":"03e8aded7525acee36e3394e28f2dcbc012533ef2a2b085a55fc291d311afee3ef","2":"02628c0919e5cb8ce9aed1f81ce313f40e1ab0b33439d5be2abc69d9bb574902e0","4":"039e7c7f274e1e8a90c61669e961c944944e6154c0794fccf8084af90252d2848f","8":"02ca0e563ae941700aefcb16a7fb820afbb3258ae924ab520210cb730227a76ca3","16":"031dbab0e4f7fb4fb0030f0e1a1dc80668eadd0b1046df3337bb13a7b9c982d392","32":"037241f7ad421374eb764a48e7769b5e2473582316844fda000d6eef28eea8ffb8","64":"02bc9767b4abf88becdac47a59e67ee9a9a80b9864ef57d16084575273ac63c0e7","128":"0351e33a076f415c2cadc945bc9bcb75bf4a774b28df8a0605dea1557e5897fed8","256":"0314b9f4300367c7e64fa85770da90839d2fc2f57d63660f08bb3ebbf90ed76840","512":"030d95abc7e881d173f4207a3349f4ee442b9e51cc461602d3eb9665b9237e8db3","1024":"0351a68a667c5fc21d66c187baecefa1d65529d06b7ae13112d432b6bca16b0e8c","2048":"0376166d8dcf97d8b0e9f11867ff0dafd439c90255b36a25be01e37e14741b9c6a","4096":"03d40f47b4e5c4d72f2a977fab5c66b54d945b2836eb888049b1dd9334d1d70304","8192":"03be18afaf35a29d7bcd5dfd1936d82c1c14691a63f8aa6ece258e16b0c043049b","16384":"028e9c6ce70f34cd29aad48656bf8345bb5ba2cb4f31fdd978686c37c93f0ab411","32768":"0253e34bab4eec93e235c33994e01bf851d5caca4559f07d37b5a5c266de7cf840","65536":"02684ede207f9ace309b796b5259fc81ef0d4492b4fb5d66cf866b0b4a6f27bec9","131072":"027cdf7be8b20a49ac7f2f065f7c53764c8926799877858c6b00b888a8aa6741a5","262144":"026939b8f766c3ebaf26408e7e54fc833805563e2ef14c8ee4d0435808b005ec4c","524288":"03772542057493a46eed6513b40386e766eedada16560ffde2f776b65794e9f004","1048576":"02b016346e5a322d371c6e6164b28b31b4d93a51572351ca2f26cdc12e916d9ac3","2097152":"028f25283e36a11df7713934a5287267381f8304aca3c1eb1b89fddce973ef1436","4194304":"03e5841d310819a49ec42dfb24839c61f68bbfc93ac68f6dad37fd5b2d204cc535","8388608":"0307ebfeb87b7bca9baa03fad00499e5cc999fa5179ef0b7ad4f555568bcb946f5","16777216":"02f2508e7df981c32f7b0008a273e2a1f19c23bb60a1561dba6b2a95ed1251eb90","33554432":"0381883a1517f8c9979a84fcd5f18437b1a2b0020376ecdd2e515dc8d5a157a318","67108864":"02aa648d39c9a725ef5927db15af6895f0d43c17f0a31faff4406314fc80180086","134217728":"0380658e5163fcf274e1ace6c696d1feef4c6068e0d03083d676dc5ef21804f22d","268435456":"031526f03de945c638acccb879de837ac3fabff8590057cfb8552ebcf51215f3aa","536870912":"035eb3e7262e126c5503e1b402db05f87de6556773ae709cb7aa1c3b0986b87566","1073741824":"03f12e6a0903ed0db87485a296b1dca9d953a8a6919ff88732238fbc672d6bd125","2147483648":"02cece3fb38a54581e0646db4b29242b6d78e49313dda46764094f9d128c1059c1"}}]}"#; assert_eq!(expected_keys, serde_json::to_string(&keys.clone()).unwrap()); + mint.rotate_keyset(CurrencyUnit::default(), 1, 32, 2, &HashMap::new()) + .await?; + + let keys = mint + .signatory + .keyset_pubkeys("00c919b6c4fa90c6".parse().expect("valid key")) + .await + .expect("keys"); + + assert_ne!(expected_keys, serde_json::to_string(&keys.clone()).unwrap()); + Ok(()) } } diff --git a/crates/cdk/src/mint/signatory.rs b/crates/cdk/src/mint/signatory.rs new file mode 100644 index 000000000..8d6365dfe --- /dev/null +++ b/crates/cdk/src/mint/signatory.rs @@ -0,0 +1,134 @@ +//! Signatory manager for handling signatory requests. +use std::collections::HashMap; +use std::sync::Arc; + +use bitcoin::bip32::DerivationPath; +use cdk_common::error::Error; +use cdk_common::mint::MintKeySetInfo; +use cdk_common::signatory::{KeysetIdentifier, Signatory}; +use cdk_common::{ + BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Proof, +}; +use tokio::sync::{mpsc, oneshot}; +use tokio::task::JoinHandle; + +macro_rules! signatory_manager { + ( + $( + $variant:ident($($input:ty),*) -> $output:ty, + )* $(,)? + ) => { + paste::paste! { + #[allow(unused_parens)] + enum Request { + $( + /// Asynchronous method to handle the `[<$variant:camel>]` request. + [<$variant:camel>]((($($input),*), oneshot::Sender>)), + )* + } + + /// Manager for handling signatory requests. + pub struct SignatoryManager { + inner: Arc, + pipeline: mpsc::Sender, + runner: JoinHandle<()>, + } + + impl ::std::ops::Deref for SignatoryManager { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + return &self.inner; + } + } + + #[allow(unused_parens)] + impl SignatoryManager { + /// Creates a new SignatoryManager with the given signatory. + /// + /// # Arguments + /// * `signatory` - An `Arc` of a signatory object implementing the required trait. + pub fn new(signatory: Arc) -> Self { + let (sender, receiver) = mpsc::channel(10_000); + let signatory_for_inner = signatory.clone(); + let runner = tokio::spawn(async move { + let mut receiver = receiver; + loop { + let request = if let Some(request) = receiver.recv().await { + request + } else { + continue; + }; + let signatory = signatory.clone(); + tokio::spawn(async move { + match request { + $( + Request::[<$variant:camel>]((( $([<$input:snake>]),* ), response)) => { + let output = signatory.[<$variant:lower>]($([<$input:snake>]),*).await; + if let Err(err) = response.send(output) { + tracing::error!("Error sending response: {:?}", err); + } + } + )* + } + }); + } + }); + + Self { + pipeline: sender, + inner: signatory_for_inner, + runner, + } + } + + $( + /// Asynchronous method to handle the `$variant` request. + /// + /// # Arguments + /// * $($input: $input),* - The inputs required for the `$variant` request. + /// + /// # Returns + /// * `Result<$output, Error>` - The result of processing the request. + pub async fn [<$variant:lower>](&self, $([<$input:snake>]: $input),*) -> Result<$output, Error> { + let (sender, receiver) = oneshot::channel(); + + self.pipeline + .try_send(Request::[<$variant:camel>]((($([<$input:snake>]),*), sender))) + .map_err(|e| Error::SendError(e.to_string()))?; + + receiver + .await + .map_err(|e| Error::RecvError(e.to_string()))? + } + )* + } + + impl Drop for SignatoryManager { + fn drop(&mut self) { + self.runner.abort(); + } + } + + impl From for SignatoryManager { + fn from(signatory: T) -> Self { + Self::new(Arc::new(signatory)) + } + } + + } + }; +} + +type Map = HashMap; + +signatory_manager! { + blind_sign(BlindedMessage) -> BlindSignature, + verify_proof(Proof) -> (), + keyset(Id) -> Option, + keysets() -> KeysetResponse, + keyset_pubkeys(Id) -> KeysResponse, + pubkeys() -> KeysResponse, + rotate_keyset(CurrencyUnit, u32, u8, u64, Map) -> MintKeySetInfo, + get_keyset_info(KeysetIdentifier) -> MintKeySetInfo, +} diff --git a/misc/itests.sh b/misc/itests.sh index cb1276062..c1fdbab43 100755 --- a/misc/itests.sh +++ b/misc/itests.sh @@ -45,8 +45,8 @@ fi echo "Temp directory created: $cdk_itests" export MINT_DATABASE="$1"; -cargo build -p cdk-integration-tests -cargo build --bin regtest_mint +cargo build -p cdk-integration-tests +cargo build --bin regtest_mint # cargo run --bin regtest_mint > "$cdk_itests/mint.log" 2>&1 & cargo run --bin regtest_mint & @@ -60,7 +60,7 @@ START_TIME=$(date +%s) while true; do # Get the current time CURRENT_TIME=$(date +%s) - + # Calculate the elapsed time ELAPSED_TIME=$((CURRENT_TIME - START_TIME)) @@ -83,6 +83,7 @@ while true; do fi done +export RUST_BACKTRACE=1 # Run cargo test cargo test -p cdk-integration-tests --test regtest