From 7fe6e2b4186f16cc0a2bf462670bb0df1a604c48 Mon Sep 17 00:00:00 2001 From: Cesar Rodas Date: Mon, 13 Jan 2025 20:46:45 -0300 Subject: [PATCH 1/3] 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 | 4 +- 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 +- crates/cdk-integration-tests/tests/mint.rs | 24 +- crates/cdk-mintd/Cargo.toml | 19 +- 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 | 68 ++- crates/cdk/src/mint/config.rs | 103 ++++ crates/cdk/src/mint/keysets.rs | 265 +-------- crates/cdk/src/mint/mod.rs | 317 ++-------- crates/cdk/src/mint/signatory.rs | 134 +++++ 32 files changed, 1512 insertions(+), 591 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 f21a72c77..0a7f35730 100644 --- a/crates/cashu/src/nuts/nut00/mod.rs +++ b/crates/cashu/src/nuts/nut00/mod.rs @@ -202,6 +202,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 5862b8e9d..a9d430dff 100644 --- a/crates/cashu/src/nuts/nut01/mod.rs +++ b/crates/cashu/src/nuts/nut01/mod.rs @@ -91,6 +91,14 @@ impl<'de> Deserialize<'de> for Keys { } } +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 0240bafe5..c07399726 100644 --- a/crates/cdk-axum/Cargo.toml +++ b/crates/cdk-axum/Cargo.toml @@ -28,8 +28,8 @@ futures = { version = "0.3.28", default-features = false } moka = { version = "0.11.1", features = ["future"] } serde_json = "1" paste = "1.0.15" -serde = { version = "1", features = ["derive"] } -uuid = { version = "1", features = ["v4", "serde"] } +serde = { version = "1.0.210", features = ["derive"] } +uuid = { version = "=1.12.1", features = ["v4", "serde"] } sha2 = "0.10.8" redis = { version = "0.23.3", features = [ "tokio-rustls-comp", diff --git a/crates/cdk-cln/Cargo.toml b/crates/cdk-cln/Cargo.toml index 99574973b..50113d71e 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.7.1", default-features = false, features = ["mint"] } +cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = [ + "mint", +] } cln-rpc = "0.3.0" futures = { version = "0.3.28", default-features = false } tokio = { version = "1", default-features = false } tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } thiserror = "1" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "=1.12.1", features = ["v4"] } diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index aec61fbef..3f2a4698d 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 ce66f6754..4ef95bc2f 100644 --- a/crates/cdk-integration-tests/Cargo.toml +++ b/crates/cdk-integration-tests/Cargo.toml @@ -32,7 +32,7 @@ futures = { version = "0.3.28", default-features = false, features = [ "executor", ] } once_cell = "1.19.0" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "=1.12.1", features = ["v4"] } serde = "1" serde_json = "1" # ln-regtest-rs = { path = "../../../../ln-regtest-rs" } diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 11680a1a8..7ab7f25b7 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; @@ -9,7 +10,8 @@ use bip39::Mnemonic; use cdk::amount::{Amount, SplitTarget}; 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, @@ -48,11 +50,18 @@ 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 @@ -467,7 +476,7 @@ async fn test_correct_keyset() -> Result<()> { .with_description("regtest mint".to_string()) .with_seed(mnemonic.to_seed_normalized("").to_vec()); - let mint = mint_builder.build().await?; + let mint = mint_builder.clone().build().await?; localstore .set_mint_info(mint_builder.mint_info.clone()) @@ -492,7 +501,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 8a847c651..2cd4fca08 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -26,14 +26,17 @@ cdk-redb = { path = "../cdk-redb", version = "0.7.1", default-features = false, cdk-sqlite = { path = "../cdk-sqlite", version = "0.7.1", default-features = false, features = [ "mint", ] } -cdk-cln = { path = "../cdk-cln", version = "0.7.1", default-features = false } -cdk-lnbits = { path = "../cdk-lnbits", version = "0.7.1", default-features = false } -cdk-phoenixd = { path = "../cdk-phoenixd", version = "0.7.1", default-features = false } -cdk-lnd = { path = "../cdk-lnd", version = "0.7.1", default-features = false } -cdk-fake-wallet = { path = "../cdk-fake-wallet", version = "0.7.1", default-features = false } -cdk-strike = { path = "../cdk-strike", version = "0.7.1" } -cdk-axum = { path = "../cdk-axum", version = "0.7.1", default-features = false } -cdk-mint-rpc = { path = "../cdk-mint-rpc", version = "0.7.1", default-features = false, optional = true } +cdk-cln = { path = "../cdk-cln", version = "0.7.0", default-features = false } +cdk-lnbits = { path = "../cdk-lnbits", version = "0.7.0", default-features = false } +cdk-phoenixd = { path = "../cdk-phoenixd", version = "0.7.0", default-features = false } +cdk-lnd = { path = "../cdk-lnd", version = "0.7.0", default-features = false } +cdk-fake-wallet = { path = "../cdk-fake-wallet", version = "0.7.0", default-features = false } +cdk-strike = { path = "../cdk-strike", version = "0.7.0" } +cdk-axum = { path = "../cdk-axum", version = "0.7.0", default-features = false } +cdk-mint-rpc = { path = "../cdk-mint-rpc", version = "0.7.0", default-features = false, optional = true } +cdk-signatory = { path = "../cdk-signatory", default-features = false, features = [ + "grpc", +] } config = { version = "0.13.3", features = ["toml"] } clap = { version = "~4.0.32", features = ["derive"] } bitcoin = { version = "0.32.2", features = [ 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 d9c1178bb..51bfed081 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -1,9 +1,12 @@ use std::path::PathBuf; +use std::sync::Arc; use bitcoin::hashes::{sha256, Hash}; use cdk::nuts::{CurrencyUnit, PublicKey}; -use cdk::Amount; +use cdk::{cdk_database, Amount}; use cdk_axum::cache; +use cdk_redb::MintRedbDatabase; +use cdk_sqlite::MintSqliteDatabase; use config::{Config, ConfigError, File}; use serde::{Deserialize, Serialize}; @@ -187,6 +190,30 @@ impl std::str::FromStr for DatabaseEngine { } } +impl DatabaseEngine { + /// Convert the database instance into a mint database + pub async fn mint>( + 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, @@ -207,6 +234,8 @@ pub struct Settings { pub database: Database, #[cfg(feature = "management-rpc")] pub mint_management_rpc: Option, + 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 8c5e874fa..f24f3e67a 100644 --- a/crates/cdk-mintd/src/env_vars.rs +++ b/crates/cdk-mintd/src/env_vars.rs @@ -83,15 +83,15 @@ pub const ENV_MINT_MANAGEMENT_PORT: &str = "CDK_MINTD_MANAGEMENT_PORT"; pub const ENV_MINT_MANAGEMENT_TLS_DIR_PATH: &str = "CDK_MINTD_MANAGEMENT_TLS_DIR_PATH"; impl Settings { - pub fn from_env(&mut self) -> Result { + 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(); #[cfg(feature = "management-rpc")] { @@ -125,7 +125,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 d5d606d21..dc2428440 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}; @@ -27,11 +25,10 @@ use cdk_axum::cache::HttpCache; #[cfg(feature = "management-rpc")] use cdk_mint_rpc::MintRPCServer; use cdk_mintd::cli::CLIArgs; -use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; +use cdk_mintd::config::{self, LnBackend}; use cdk_mintd::env_vars::ENV_WORK_DIR; use cdk_mintd::setup::LnBackendSetup; -use cdk_redb::MintRedbDatabase; -use cdk_sqlite::MintSqliteDatabase; +use cdk_mintd::work_dir; use clap::Parser; use tokio::sync::Notify; use tower_http::compression::CompressionLayer; @@ -79,7 +76,7 @@ async fn main() -> anyhow::Result<()> { let mut mint_builder = MintBuilder::new(); - let mut settings = if config_file_arg.exists() { + let settings = if config_file_arg.exists() { config::Settings::new(Some(config_file_arg)) } else { tracing::info!("Config file does not exist. Attempting to read env vars"); @@ -89,22 +86,7 @@ async fn main() -> anyhow::Result<()> { // This check for any settings defined in ENV VARs // ENV VARS will take **priority** over those in the config let settings = settings.from_env()?; - - let localstore: Arc + Send + Sync> = - match settings.database.engine { - DatabaseEngine::Sqlite => { - let sql_db_path = work_dir.join("cdk-mintd.sqlite"); - let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?; - - sqlite_db.migrate().await; - - Arc::new(sqlite_db) - } - DatabaseEngine::Redb => { - let redb_path = work_dir.join("cdk-mintd.redb"); - Arc::new(MintRedbDatabase::new(&redb_path)?) - } - }; + let localstore = settings.database.engine.clone().mint(&work_dir).await?; mint_builder = mint_builder.with_localstore(localstore); @@ -308,6 +290,12 @@ async fn main() -> anyhow::Result<()> { .with_description(settings.mint_info.description) .with_seed(mnemonic.to_seed_normalized("").to_vec()); + mint_builder = if let Some(remote_signatory) = settings.remote_signatory.clone() { + mint_builder.with_remote_signatory(remote_signatory) + } else { + mint_builder + }; + let cached_endpoints = vec![ CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11), CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11), @@ -460,15 +448,6 @@ 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) -} - async fn shutdown_signal() { tokio::signal::ctrl_c() .await diff --git a/crates/cdk-phoenixd/Cargo.toml b/crates/cdk-phoenixd/Cargo.toml index ddb4351c4..2d3bba30d 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.7.1", default-features = false, features = ["mint"] } +cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = [ + "mint", +] } futures = { version = "0.3.28", default-features = false } tokio = { version = "1", default-features = false } tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } thiserror = "1" phoenixd-rs = "0.4.0" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "=1.12.1", features = ["v4"] } diff --git a/crates/cdk-redb/Cargo.toml b/crates/cdk-redb/Cargo.toml index ce39c59d9..11501f142 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 4eab96ff9..8594dc87a 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 525fdf714..dd59db895 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.7.1", default-features = false, features = ["mint"] } +cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = [ + "mint", +] } futures = { version = "0.3.28", default-features = false } tokio = { version = "1", default-features = false } tokio-util = { version = "0.7.11", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } thiserror = "1" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "=1.12.1", features = ["v4"] } strike-rs = "0.4.0" # strike-rs = { path = "../../../../strike-rs" } # strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "577ad9591" } diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index b239c9a94..d8cc6dff9 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.7.1" } +cdk-signatory = { path = "../cdk-signatory", default-features = false, optional = true } cbor-diag = "0.1.12" async-trait = "0.1" anyhow = { version = "1.0.43", features = ["backtrace"] } @@ -62,6 +65,7 @@ uuid = { version = "=1.12.1", features = ["v4", "serde"] } # -Z minimal-versions sync_wrapper = "0.1.2" bech32 = "0.9.1" +paste = "1.0.15" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.21", features = [ @@ -106,3 +110,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 9ef1deef0..a7d082238 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -6,9 +6,11 @@ use std::sync::Arc; use anyhow::anyhow; use bitcoin::bip32::DerivationPath; use cdk_common::database::{self, MintDatabase}; +use cdk_common::signatory::Signatory; use super::nut17::SupportedMethods; use super::nut19::{self, CachedEndpoint}; +use super::signatory::SignatoryManager; use super::Nuts; use crate::amount::Amount; use crate::cdk_lightning::{self, MintLightning}; @@ -19,8 +21,14 @@ use crate::nuts::{ }; use crate::types::LnKey; +#[derive(Clone, Debug)] +pub enum SignatoryInfo { + Seed(Vec), + Remote(String), +} + /// Cashu Mint -#[derive(Default)] +#[derive(Default, Clone)] pub struct MintBuilder { /// Mint Info pub mint_info: MintInfo, @@ -28,8 +36,10 @@ pub struct MintBuilder { localstore: Option + Send + Sync>>, /// Ln backends for mint ln: Option + Send + Sync>>>, - seed: Option>, - supported_units: HashMap, + signatory_info: Option, + /// expose supported units + pub supported_units: HashMap, + signatory: Option>, custom_paths: HashMap, } @@ -53,6 +63,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, @@ -62,9 +78,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 } @@ -224,11 +246,43 @@ impl MintBuilder { .clone() .ok_or(anyhow!("Localstore not set"))?; + let signatory = if let Some(signatory) = self.signatory.as_ref() { + signatory.clone() + } else { + match self.signatory_info.as_ref() { + Some(SignatoryInfo::Seed(seed)) => Arc::new( + cdk_signatory::MemorySignatory::new( + localstore.clone(), + seed, + self.supported_units.clone(), + HashMap::new(), + ) + .await?, + ) + as Arc, + #[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, self.custom_paths.clone(), ) .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 b8ed6ac94..d20a5573d 100644 --- a/crates/cdk/src/mint/keysets.rs +++ b/crates/cdk/src/mint/keysets.rs @@ -1,197 +1,38 @@ -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; +use std::collections::HashMap; -use bitcoin::bip32::{DerivationPath, Xpriv}; -use bitcoin::key::Secp256k1; -use bitcoin::secp256k1::All; -use cdk_common::database::{self, MintDatabase}; +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 { - /// Initialize keysets and returns a [`Result`] with a tuple of the following: - /// * a [`HashMap`] mapping each active keyset `Id` to `MintKeySet` - /// * a [`Vec`] of `CurrencyUnit` containing active keysets units - pub async fn init_keysets( - xpriv: Xpriv, - secp_ctx: &Secp256k1, - localstore: &Arc + Send + Sync>, - supported_units: &HashMap, - custom_paths: &HashMap, - ) -> Result<(HashMap, Vec), Error> { - let mut active_keysets: HashMap = HashMap::new(); - let mut active_keyset_units: Vec = vec![]; - - // Get keysets info from DB - let keysets_infos = localstore.get_keyset_infos().await?; - - if !keysets_infos.is_empty() { - tracing::debug!("Setting all saved keysets to inactive"); - for keyset in keysets_infos.clone() { - // Set all to in active - let mut keyset = keyset; - keyset.active = false; - localstore.add_keyset_info(keyset).await?; - } - - let keysets_by_unit: HashMap> = - keysets_infos.iter().fold(HashMap::new(), |mut acc, ks| { - acc.entry(ks.unit.clone()).or_default().push(ks.clone()); - acc - }); - - for (unit, keysets) in keysets_by_unit { - let mut keysets = keysets; - keysets.sort_by(|a, b| b.derivation_path_index.cmp(&a.derivation_path_index)); - - // Get the keyset with the highest counter - let highest_index_keyset = keysets - .first() - .cloned() - .expect("unit will not be added to hashmap if empty"); - - let keysets: Vec = keysets - .into_iter() - .filter(|ks| ks.derivation_path_index.is_some()) - .collect(); - - if let Some((input_fee_ppk, max_order)) = supported_units.get(&unit) { - if !keysets.is_empty() - && &highest_index_keyset.input_fee_ppk == input_fee_ppk - && &highest_index_keyset.max_order == max_order - { - tracing::debug!("Current highest index keyset matches expect fee and max order. Setting active"); - let id = highest_index_keyset.id; - let keyset = MintKeySet::generate_from_xpriv( - secp_ctx, - xpriv, - highest_index_keyset.max_order, - highest_index_keyset.unit.clone(), - highest_index_keyset.derivation_path.clone(), - ); - active_keysets.insert(id, keyset); - let mut keyset_info = highest_index_keyset; - keyset_info.active = true; - localstore.add_keyset_info(keyset_info).await?; - active_keyset_units.push(unit.clone()); - localstore.set_active_keyset(unit, id).await?; - } else { - // Check to see if there are not keysets by this unit - let derivation_path_index = if keysets.is_empty() { - 1 - } else { - highest_index_keyset.derivation_path_index.unwrap_or(0) + 1 - }; - - let derivation_path = match custom_paths.get(&unit) { - Some(path) => path.clone(), - None => derivation_path_from_unit(unit.clone(), derivation_path_index) - .ok_or(Error::UnsupportedUnit)?, - }; - - let (keyset, keyset_info) = create_new_keyset( - secp_ctx, - xpriv, - derivation_path, - Some(derivation_path_index), - unit.clone(), - *max_order, - *input_fee_ppk, - ); - - let id = keyset_info.id; - localstore.add_keyset_info(keyset_info).await?; - localstore.set_active_keyset(unit.clone(), id).await?; - active_keysets.insert(id, keyset); - active_keyset_units.push(unit.clone()); - }; - } - } - } - - Ok((active_keysets, active_keyset_units)) - } - /// Retrieve the public keys of the active keyset for distribution to wallet /// clients #[instrument(skip(self))] pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result { - 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 @@ -205,31 +46,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.clone(), id).await?; - - let mut keysets = self.keysets.write().await; - keysets.insert(id, keyset); - - tracing::info!("Rotated to new keyset {} for {}", id, unit); - - Ok(keyset_info) + self.signatory + .rotate_keyset( + unit, + derivation_path_index, + max_order, + input_fee_ppk, + custom_paths.to_owned(), + ) + .await } /// Rotate to next keyset for unit @@ -240,17 +65,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 {:?}", @@ -258,50 +73,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 caee81d0b..7586f800d 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, QuoteTTL}; 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; @@ -30,13 +27,18 @@ mod keysets; mod ln; mod melt; mod mint_nut04; +pub mod signatory; mod start_up_check; pub mod subscription; mod swap; mod verification; +/// 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)] @@ -47,9 +49,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, } @@ -57,61 +59,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, active_keyset_units) = Mint::init_keysets( - xpriv, - &secp_ctx, - &localstore, - &supported_units, - &custom_paths, - ) - .await?; - - // Create new keysets for supported units that aren't covered by the current keysets - for (unit, (fee, max_order)) in supported_units { - if !active_keyset_units.contains(&unit) { - let derivation_path = match custom_paths.get(&unit) { - Some(path) => path.clone(), - None => { - derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)? - } - }; - - let (keyset, keyset_info) = create_new_keyset( - &secp_ctx, - xpriv, - derivation_path, - Some(0), - unit.clone(), - max_order, - fee, - ); - - let id = keyset_info.id; - localstore.add_keyset_info(keyset_info).await?; - localstore.set_active_keyset(unit, id).await?; - active_keysets.insert(id, keyset); - } - } - - let 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, }) } @@ -228,87 +185,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 @@ -454,148 +337,17 @@ 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; - use cdk_sqlite::mint::memory::new_with_state; - use secp256k1::Secp256k1; + use cdk_common::common::{LnKey, QuoteTTL}; + use cdk_common::mint::MintKeySetInfo; + use cdk_signatory::MemorySignatory; use uuid::Uuid; 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); - } - #[derive(Default)] struct MintConfig<'a> { active_keysets: HashMap, @@ -626,11 +378,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 @@ -725,12 +487,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, +} From 84c5b6d57f1e3c14462d5bdf025b93cbaaa076b9 Mon Sep 17 00:00:00 2001 From: Cesar Rodas Date: Wed, 5 Feb 2025 18:20:27 -0300 Subject: [PATCH 2/3] Remove the shared Mint instance between tests Since the SignatoryManager spawns a worker (with `tokio::spawn`) to execute the Signatory (which implements the `Signatory` trait), a mpsc channel is used to exchange messages. In side Tokio tests, each test has its own Tokio runtime. When the first test is done, since `instantiate` shares all the Mint instances, their Tokio runtime is dropped, and the tokio::spawn's task gets killed, breaking the channel. Since the SignatoryManager has no way to respawn, understandably so, since the task runs in an infinite loop, receiving messages and spawning new tasks to non-blocking. There are two choices: either create one Mint per test (which is what this PR does) or compile a modified version of the SignatoryManager for tests, which performs the calls to the Signatory trait in place instead of converting it to a message and passing to the signatory Tokio task. The first option is more straightforward and fixes other instances of the code where `tokio::spawn` is used, such as subscriptions. A third option could be explored, which would imply spawning a Tokio runtime in another OS-level thread, creating the Mint in that context, and sharing it with everything else; perhaps this should explored further if the idea is to test Mint this way. --- crates/cdk-integration-tests/tests/mint.rs | 18 ++++++------------ crates/cdk/src/mint/mod.rs | 3 ++- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 7ab7f25b7..7876aaccf 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -50,7 +50,7 @@ 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 localstore = Arc::new(memory::empty().await.expect("valid db instance")); let seed = mnemonic.to_seed_normalized(""); let signatory_manager = Arc::new(SignatoryManager::new(Arc::new( MemorySignatory::new(localstore.clone(), &seed, supported_units, HashMap::new()) @@ -68,10 +68,6 @@ async fn new_mint(fee: u64) -> Mint { .unwrap() } -async fn initialize() -> Mint { - new_mint(0).await -} - async fn mint_proofs( mint: &Mint, amount: Amount, @@ -116,7 +112,7 @@ async fn mint_proofs( #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_mint_double_spend() -> Result<()> { - let mint = initialize().await; + let mint = new_mint(0).await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); @@ -127,9 +123,7 @@ async fn test_mint_double_spend() -> Result<()> { let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - let swap = mint.process_swap_request(swap_request).await; - - assert!(swap.is_ok()); + mint.process_swap_request(swap_request).await?; let preswap_two = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?; @@ -150,7 +144,7 @@ async fn test_mint_double_spend() -> Result<()> { /// This will work if the mint does not check for outputs amounts overflowing #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_attempt_to_swap_by_overflowing() -> Result<()> { - let mint = initialize().await; + let mint = new_mint(0).await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); @@ -189,7 +183,7 @@ async fn test_attempt_to_swap_by_overflowing() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] pub async fn test_p2pk_swap() -> Result<()> { - let mint = initialize().await; + let mint = new_mint(0).await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); @@ -307,7 +301,7 @@ pub async fn test_p2pk_swap() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_swap_unbalanced() -> Result<()> { - let mint = initialize().await; + let mint = new_mint(0).await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 7586f800d..e5b18988b 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -341,9 +341,10 @@ pub struct FeeReserve { mod tests { use std::str::FromStr; - use cdk_common::common::{LnKey, QuoteTTL}; + use cdk_common::common::LnKey; use cdk_common::mint::MintKeySetInfo; use cdk_signatory::MemorySignatory; + use cdk_sqlite::mint::memory::new_with_state; use uuid::Uuid; use super::*; From 45d551146a56753146d67003d606a598f53ec8de Mon Sep 17 00:00:00 2001 From: Cesar Rodas Date: Wed, 12 Feb 2025 00:18:37 -0300 Subject: [PATCH 3/3] Add more commands --- crates/cashu/src/secret.rs | 6 ++ crates/cdk-signatory/src/proto/client.rs | 21 ++++++- crates/cdk-signatory/src/proto/mod.rs | 57 +++++++++++++++++++ crates/cdk-signatory/src/proto/server.rs | 17 ++++++ .../cdk-signatory/src/proto/signatory.proto | 20 +++++++ crates/cdk/src/mint/keysets.rs | 1 - crates/cdk/src/mint/mod.rs | 2 +- 7 files changed, 119 insertions(+), 5 deletions(-) diff --git a/crates/cashu/src/secret.rs b/crates/cashu/src/secret.rs index ef388393b..88e44a7ee 100644 --- a/crates/cashu/src/secret.rs +++ b/crates/cashu/src/secret.rs @@ -44,6 +44,12 @@ impl Secret { Self(secret.into()) } + /// Creates a new [`Secret`] from bytes + pub fn from_bytes(bytes: Vec) -> Self { + let secret = hex::encode(bytes); + Self(secret) + } + /// Create secret value /// Generate a new random secret as the recommended 32 byte hex pub fn generate() -> Self { diff --git a/crates/cdk-signatory/src/proto/client.rs b/crates/cdk-signatory/src/proto/client.rs index 5b1847b86..111034e66 100644 --- a/crates/cdk-signatory/src/proto/client.rs +++ b/crates/cdk-signatory/src/proto/client.rs @@ -5,7 +5,8 @@ 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, + dhke, BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, + Proof, }; use crate::proto::signatory_client::SignatoryClient; @@ -36,9 +37,23 @@ impl Signatory for RemoteSigner { .map_err(|e| Error::Custom(e.to_string()))? } - async fn verify_proof(&self, _proof: Proof) -> Result<(), Error> { - todo!() + async fn verify_proof(&self, request: Proof) -> Result<(), Error> { + let req: super::Proof = request.into(); + let result: super::Success = self + .client + .clone() + .verify_proof(req) + .await + .map(|response| response.into_inner()) + .map_err(|e| Error::Custom(e.to_string()))?; + + if result.success { + Ok(()) + } else { + Err(dhke::Error::TokenNotVerified)? + } } + async fn keyset(&self, _keyset_id: Id) -> Result, Error> { todo!() } diff --git a/crates/cdk-signatory/src/proto/mod.rs b/crates/cdk-signatory/src/proto/mod.rs index 10ffd44f7..ea53fb34a 100644 --- a/crates/cdk-signatory/src/proto/mod.rs +++ b/crates/cdk-signatory/src/proto/mod.rs @@ -1,3 +1,4 @@ +use cdk_common::secret::Secret; use cdk_common::{HTLCWitness, P2PKWitness}; use tonic::Status; @@ -6,6 +7,62 @@ tonic::include_proto!("cdk_signatory"); pub mod client; pub mod server; +impl From for ProofDleq { + fn from(value: cdk_common::ProofDleq) -> Self { + ProofDleq { + e: value.e.as_secret_bytes().to_vec(), + s: value.s.as_secret_bytes().to_vec(), + r: value.r.as_secret_bytes().to_vec(), + } + } +} + +impl TryInto for ProofDleq { + type Error = Status; + + fn try_into(self) -> Result { + Ok(cdk_common::ProofDleq { + e: cdk_common::SecretKey::from_slice(&self.e) + .map_err(|e| Status::from_error(Box::new(e)))?, + s: cdk_common::SecretKey::from_slice(&self.s) + .map_err(|e| Status::from_error(Box::new(e)))?, + r: cdk_common::SecretKey::from_slice(&self.r) + .map_err(|e| Status::from_error(Box::new(e)))?, + }) + } +} + +impl From for Proof { + fn from(value: cdk_common::Proof) -> Self { + Proof { + amount: value.amount.into(), + keyset_id: value.keyset_id.to_string(), + secret: value.secret.to_bytes(), + c: value.c.to_bytes().to_vec(), + witness: value.witness.map(|w| w.into()), + dleq: value.dleq.map(|dleq| dleq.into()), + } + } +} + +impl TryInto for Proof { + type Error = Status; + fn try_into(self) -> Result { + Ok(cdk_common::Proof { + amount: self.amount.into(), + keyset_id: self + .keyset_id + .parse() + .map_err(|e| Status::from_error(Box::new(e)))?, + secret: Secret::from_bytes(self.secret), + c: cdk_common::PublicKey::from_slice(&self.c) + .map_err(|e| Status::from_error(Box::new(e)))?, + witness: self.witness.map(|w| w.try_into()).transpose()?, + dleq: self.dleq.map(|x| x.try_into()).transpose()?, + }) + } +} + impl From for BlindedMessage { fn from(value: cdk_common::BlindedMessage) -> Self { BlindedMessage { diff --git a/crates/cdk-signatory/src/proto/server.rs b/crates/cdk-signatory/src/proto/server.rs index a594f4fe8..abe22ac92 100644 --- a/crates/cdk-signatory/src/proto/server.rs +++ b/crates/cdk-signatory/src/proto/server.rs @@ -1,5 +1,6 @@ use std::net::SocketAddr; +use cdk_common::dhke; use cdk_common::signatory::Signatory as _; use tonic::transport::{Error, Server}; use tonic::{Request, Response, Status}; @@ -23,6 +24,22 @@ impl signatory_server::Signatory for CdkSignatory { .map_err(|e| Status::from_error(Box::new(e)))?; Ok(Response::new(blind_signature.into())) } + + async fn verify_proof( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request: {:?}", request); + let result = match self.0.verify_proof(request.into_inner().try_into()?).await { + Ok(()) => proto::Success { success: true }, + Err(cdk_common::error::Error::DHKE(dhke::Error::TokenNotVerified)) => { + proto::Success { success: false } + } + Err(err) => return Err(Status::from_error(Box::new(err))), + }; + + Ok(Response::new(result)) + } } /// Runs the signatory server diff --git a/crates/cdk-signatory/src/proto/signatory.proto b/crates/cdk-signatory/src/proto/signatory.proto index 6c6105b2d..61f63da69 100644 --- a/crates/cdk-signatory/src/proto/signatory.proto +++ b/crates/cdk-signatory/src/proto/signatory.proto @@ -4,9 +4,29 @@ package cdk_signatory; service Signatory { rpc BlindSign (BlindedMessage) returns (BlindSignature); + rpc VerifyProof (Proof) returns (Success); } +message Success { + bool success = 1; +} + +message Proof { + uint64 amount = 1; + string keyset_id = 2; + bytes secret = 3; + bytes C = 4; + optional Witness witness = 5; + optional ProofDLEQ dleq = 6; +} + +message ProofDLEQ { + bytes e = 1; + bytes s = 2; + bytes r = 3; +} + message BlindSignature { uint64 amount = 1; string keyset_id = 2; diff --git a/crates/cdk/src/mint/keysets.rs b/crates/cdk/src/mint/keysets.rs index d20a5573d..2a57f4d0f 100644 --- a/crates/cdk/src/mint/keysets.rs +++ b/crates/cdk/src/mint/keysets.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use bitcoin::bip32::DerivationPath; - use cdk_common::mint::MintKeySetInfo; use tracing::instrument; diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index e5b18988b..e54b586d3 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -494,7 +494,7 @@ mod tests { .await .expect("keys"); - 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"}}]}"#; + let expected_keys = r#"{"keysets":[{"id":"005f6e8c540c9e61","unit":"sat","keys":{"1":"03e8aded7525acee36e3394e28f2dcbc012533ef2a2b085a55fc291d311afee3ef","1024":"0351a68a667c5fc21d66c187baecefa1d65529d06b7ae13112d432b6bca16b0e8c","1048576":"02b016346e5a322d371c6e6164b28b31b4d93a51572351ca2f26cdc12e916d9ac3","1073741824":"03f12e6a0903ed0db87485a296b1dca9d953a8a6919ff88732238fbc672d6bd125","128":"0351e33a076f415c2cadc945bc9bcb75bf4a774b28df8a0605dea1557e5897fed8","131072":"027cdf7be8b20a49ac7f2f065f7c53764c8926799877858c6b00b888a8aa6741a5","134217728":"0380658e5163fcf274e1ace6c696d1feef4c6068e0d03083d676dc5ef21804f22d","16":"031dbab0e4f7fb4fb0030f0e1a1dc80668eadd0b1046df3337bb13a7b9c982d392","16384":"028e9c6ce70f34cd29aad48656bf8345bb5ba2cb4f31fdd978686c37c93f0ab411","16777216":"02f2508e7df981c32f7b0008a273e2a1f19c23bb60a1561dba6b2a95ed1251eb90","2":"02628c0919e5cb8ce9aed1f81ce313f40e1ab0b33439d5be2abc69d9bb574902e0","2048":"0376166d8dcf97d8b0e9f11867ff0dafd439c90255b36a25be01e37e14741b9c6a","2097152":"028f25283e36a11df7713934a5287267381f8304aca3c1eb1b89fddce973ef1436","2147483648":"02cece3fb38a54581e0646db4b29242b6d78e49313dda46764094f9d128c1059c1","256":"0314b9f4300367c7e64fa85770da90839d2fc2f57d63660f08bb3ebbf90ed76840","262144":"026939b8f766c3ebaf26408e7e54fc833805563e2ef14c8ee4d0435808b005ec4c","268435456":"031526f03de945c638acccb879de837ac3fabff8590057cfb8552ebcf51215f3aa","32":"037241f7ad421374eb764a48e7769b5e2473582316844fda000d6eef28eea8ffb8","32768":"0253e34bab4eec93e235c33994e01bf851d5caca4559f07d37b5a5c266de7cf840","33554432":"0381883a1517f8c9979a84fcd5f18437b1a2b0020376ecdd2e515dc8d5a157a318","4":"039e7c7f274e1e8a90c61669e961c944944e6154c0794fccf8084af90252d2848f","4096":"03d40f47b4e5c4d72f2a977fab5c66b54d945b2836eb888049b1dd9334d1d70304","4194304":"03e5841d310819a49ec42dfb24839c61f68bbfc93ac68f6dad37fd5b2d204cc535","512":"030d95abc7e881d173f4207a3349f4ee442b9e51cc461602d3eb9665b9237e8db3","524288":"03772542057493a46eed6513b40386e766eedada16560ffde2f776b65794e9f004","536870912":"035eb3e7262e126c5503e1b402db05f87de6556773ae709cb7aa1c3b0986b87566","64":"02bc9767b4abf88becdac47a59e67ee9a9a80b9864ef57d16084575273ac63c0e7","65536":"02684ede207f9ace309b796b5259fc81ef0d4492b4fb5d66cf866b0b4a6f27bec9","67108864":"02aa648d39c9a725ef5927db15af6895f0d43c17f0a31faff4406314fc80180086","8":"02ca0e563ae941700aefcb16a7fb820afbb3258ae924ab520210cb730227a76ca3","8192":"03be18afaf35a29d7bcd5dfd1936d82c1c14691a63f8aa6ece258e16b0c043049b","8388608":"0307ebfeb87b7bca9baa03fad00499e5cc999fa5179ef0b7ad4f555568bcb946f5"}}]}"#; assert_eq!(expected_keys, serde_json::to_string(&keys.clone()).unwrap());