diff --git a/Cargo.toml b/Cargo.toml index c595181c..68829179 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-featu cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.9.2" } cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.9.2" } cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.9.2" } +cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.9.2", default-features = false } clap = { version = "4.5.31", features = ["derive"] } ciborium = { version = "0.2.2", default-features = false, features = ["std"] } cbor-diag = "0.1.12" diff --git a/crates/cashu/src/nuts/nut00/mod.rs b/crates/cashu/src/nuts/nut00/mod.rs index 58b68fe8..99b35101 100644 --- a/crates/cashu/src/nuts/nut00/mod.rs +++ b/crates/cashu/src/nuts/nut00/mod.rs @@ -283,6 +283,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 5862b8e9..a9d430df 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/src/auth.rs b/crates/cdk-axum/src/auth.rs index d73337a1..2d9c74e8 100644 --- a/crates/cdk-axum/src/auth.rs +++ b/crates/cdk-axum/src/auth.rs @@ -103,12 +103,7 @@ where pub async fn get_auth_keysets( State(state): State, ) -> Result, Response> { - let keysets = state.mint.auth_keysets().await.map_err(|err| { - tracing::error!("Could not get keysets: {}", err); - into_response(err) - })?; - - Ok(Json(keysets)) + Ok(Json(state.mint.auth_keysets())) } #[cfg_attr(feature = "swagger", utoipa::path( @@ -125,7 +120,7 @@ pub async fn get_auth_keysets( pub async fn get_blind_auth_keys( State(state): State, ) -> Result, Response> { - let pubkeys = state.mint.auth_pubkeys().await.map_err(|err| { + let pubkeys = state.mint.auth_pubkeys().map_err(|err| { tracing::error!("Could not get keys: {}", err); into_response(err) })?; diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 681e08c6..73055134 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -82,12 +82,7 @@ post_cache_wrapper!( pub(crate) async fn get_keys( State(state): State, ) -> Result, Response> { - let pubkeys = state.mint.pubkeys().await.map_err(|err| { - tracing::error!("Could not get keys: {}", err); - into_response(err) - })?; - - Ok(Json(pubkeys)) + Ok(Json(state.mint.pubkeys())) } #[cfg_attr(feature = "swagger", utoipa::path( @@ -110,7 +105,7 @@ pub(crate) async fn get_keyset_pubkeys( State(state): State, Path(keyset_id): Path, ) -> Result, Response> { - let pubkeys = state.mint.keyset_pubkeys(&keyset_id).await.map_err(|err| { + let pubkeys = state.mint.keyset_pubkeys(&keyset_id).map_err(|err| { tracing::error!("Could not get keyset pubkeys: {}", err); into_response(err) })?; @@ -134,12 +129,7 @@ pub(crate) async fn get_keyset_pubkeys( pub(crate) async fn get_keysets( State(state): State, ) -> Result, Response> { - let keysets = state.mint.keysets().await.map_err(|err| { - tracing::error!("Could not get keysets: {}", err); - into_response(err) - })?; - - Ok(Json(keysets)) + Ok(Json(state.mint.keysets())) } #[cfg_attr(feature = "swagger", utoipa::path( diff --git a/crates/cdk-cli/Cargo.toml b/crates/cdk-cli/Cargo.toml index 01004994..6ec4c8b2 100644 --- a/crates/cdk-cli/Cargo.toml +++ b/crates/cdk-cli/Cargo.toml @@ -37,4 +37,3 @@ nostr-sdk = { version = "0.41.0", default-features = false, features = [ reqwest.workspace = true url.workspace = true serde_with.workspace = true - diff --git a/crates/cdk-common/src/database/mint/mod.rs b/crates/cdk-common/src/database/mint/mod.rs index c2d9b206..41326f35 100644 --- a/crates/cdk-common/src/database/mint/mod.rs +++ b/crates/cdk-common/src/database/mint/mod.rs @@ -42,6 +42,7 @@ pub trait KeysDatabase { /// Get [`MintKeySetInfo`]s async fn get_keyset_infos(&self) -> Result, Self::Err>; } + /// Mint Quote Database trait #[async_trait] pub trait QuotesDatabase { @@ -172,10 +173,7 @@ pub trait SignaturesDatabase { /// Mint Database trait #[async_trait] pub trait Database: - KeysDatabase - + QuotesDatabase - + ProofsDatabase - + SignaturesDatabase + QuotesDatabase + ProofsDatabase + SignaturesDatabase { /// Set [`MintInfo`] async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error>; diff --git a/crates/cdk-common/src/database/mint/test.rs b/crates/cdk-common/src/database/mint/test.rs index f5177712..4f285af2 100644 --- a/crates/cdk-common/src/database/mint/test.rs +++ b/crates/cdk-common/src/database/mint/test.rs @@ -12,7 +12,7 @@ use super::*; use crate::mint::MintKeySetInfo; #[inline] -async fn setup_keyset>(db: &DB) -> Id { +async fn setup_keyset + KeysDatabase>(db: &DB) -> Id { let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap(); let keyset_info = MintKeySetInfo { id: keyset_id, @@ -30,7 +30,7 @@ async fn setup_keyset>(db: &DB) -> Id { } /// State transition test -pub async fn state_transition>(db: DB) { +pub async fn state_transition + KeysDatabase>(db: DB) { let keyset_id = setup_keyset(&db).await; let proofs = vec![ diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index aea4ec3f..c40bad0f 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -92,6 +92,14 @@ pub enum Error { #[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")] AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod), + /// Internal Error - Send error + #[error("Internal send error: {0}")] + SendError(String), + + /// Internal Error - Recv error + #[error("Internal receive error: {0}")] + RecvError(String), + // Mint Errors /// Minting is disabled #[error("Minting is disabled")] diff --git a/crates/cdk-integration-tests/src/init_auth_mint.rs b/crates/cdk-integration-tests/src/init_auth_mint.rs index eabbf649..49c62320 100644 --- a/crates/cdk-integration-tests/src/init_auth_mint.rs +++ b/crates/cdk-integration-tests/src/init_auth_mint.rs @@ -4,23 +4,25 @@ use std::sync::Arc; use anyhow::Result; use bip39::Mnemonic; use cashu::{AuthRequired, Method, ProtectedEndpoint, RoutePath}; -use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase}; +use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase, MintKeysDatabase}; use cdk::mint::{MintBuilder, MintMeltLimits}; use cdk::nuts::{CurrencyUnit, PaymentMethod}; use cdk::types::FeeReserve; use cdk::wallet::AuthWallet; use cdk_fake_wallet::FakeWallet; -pub async fn start_fake_mint_with_auth( +pub async fn start_fake_mint_with_auth( _addr: &str, _port: u16, openid_discovery: String, database: D, auth_database: A, + key_store: K, ) -> Result<()> where D: MintDatabase + Send + Sync + 'static, A: MintAuthDatabase + Send + Sync + 'static, + K: MintKeysDatabase + Send + Sync + 'static, { let fee_reserve = FeeReserve { min_fee_reserve: 1.into(), @@ -31,7 +33,9 @@ where let mut mint_builder = MintBuilder::new(); - mint_builder = mint_builder.with_localstore(Arc::new(database)); + mint_builder = mint_builder + .with_localstore(Arc::new(database)) + .with_keystore(Arc::new(key_store)); mint_builder = mint_builder .add_ln_backend( diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index f9ed35a7..d0c5fa58 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/crates/cdk-integration-tests/src/init_pure_tests.rs @@ -9,7 +9,7 @@ use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use bip39::Mnemonic; use cdk::amount::SplitTarget; -use cdk::cdk_database::{self, MintDatabase, WalletDatabase}; +use cdk::cdk_database::{self, WalletDatabase}; use cdk::mint::{MintBuilder, MintMeltLimits}; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ @@ -56,18 +56,15 @@ impl Debug for DirectMintConnection { #[async_trait] impl MintConnector for DirectMintConnection { async fn get_mint_keys(&self) -> Result, Error> { - self.mint.pubkeys().await.map(|pks| pks.keysets) + Ok(self.mint.pubkeys().keysets) } async fn get_mint_keyset(&self, keyset_id: Id) -> Result { - self.mint - .keyset(&keyset_id) - .await - .and_then(|res| res.ok_or(Error::UnknownKeySet)) + self.mint.keyset(&keyset_id).ok_or(Error::UnknownKeySet) } async fn get_mint_keysets(&self) -> Result { - self.mint.keysets().await + Ok(self.mint.keysets()) } async fn post_mint_quote( @@ -173,40 +170,42 @@ pub fn setup_tracing() { } pub async fn create_and_start_test_mint() -> Result { - let mut mint_builder = MintBuilder::new(); - // Read environment variable to determine database type let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set"); - let localstore: Arc + Send + Sync> = - match db_type.to_lowercase().as_str() { - "sqlite" => { - // Create a temporary directory for SQLite database - let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?; - let path = temp_dir.join("mint.db").to_str().unwrap().to_string(); - let database = cdk_sqlite::MintSqliteDatabase::new(&path) + let mut mint_builder = match db_type.to_lowercase().as_str() { + "sqlite" => { + // Create a temporary directory for SQLite database + let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?; + let path = temp_dir.join("mint.db").to_str().unwrap().to_string(); + let database = Arc::new( + cdk_sqlite::MintSqliteDatabase::new(&path) .await - .expect("Could not create sqlite db"); - Arc::new(database) - } - "redb" => { - // Create a temporary directory for ReDB database - let temp_dir = create_temp_dir("cdk-test-redb-mint")?; - let path = temp_dir.join("mint.redb"); - let database = cdk_redb::MintRedbDatabase::new(&path) - .expect("Could not create redb mint database"); - Arc::new(database) - } - "memory" => { - let database = cdk_sqlite::mint::memory::empty().await?; - Arc::new(database) - } - _ => { - bail!("Db type not set") - } - }; - - mint_builder = mint_builder.with_localstore(localstore.clone()); + .expect("Could not create sqlite db"), + ); + MintBuilder::new() + .with_localstore(database.clone()) + .with_keystore(database) + } + "redb" => { + // Create a temporary directory for ReDB database + let temp_dir = create_temp_dir("cdk-test-redb-mint")?; + let path = temp_dir.join("mint.redb"); + let database = Arc::new( + cdk_redb::MintRedbDatabase::new(&path) + .expect("Could not create redb mint database"), + ); + MintBuilder::new() + .with_localstore(database.clone()) + .with_keystore(database) + } + "memory" => MintBuilder::new() + .with_localstore(Arc::new(cdk_sqlite::mint::memory::empty().await?)) + .with_keystore(Arc::new(cdk_sqlite::mint::memory::empty().await?)), + _ => { + bail!("Db type not set") + } + }; let fee_reserve = FeeReserve { min_fee_reserve: 1.into(), @@ -237,6 +236,12 @@ pub async fn create_and_start_test_mint() -> Result { .with_urls(vec!["https://aaa".to_string()]) .with_seed(mnemonic.to_seed_normalized("").to_vec()); + let localstore = mint_builder + .localstore + .as_ref() + .map(|x| x.clone()) + .expect("localstore"); + localstore .set_mint_info(mint_builder.mint_info.clone()) .await?; diff --git a/crates/cdk-integration-tests/tests/integration_tests_pure.rs b/crates/cdk-integration-tests/tests/integration_tests_pure.rs index ca10f257..34425caf 100644 --- a/crates/cdk-integration-tests/tests/integration_tests_pure.rs +++ b/crates/cdk-integration-tests/tests/integration_tests_pure.rs @@ -8,6 +8,7 @@ use std::assert_eq; use std::collections::{HashMap, HashSet}; use std::hash::RandomState; use std::str::FromStr; +use std::time::Duration; use cashu::amount::SplitTarget; use cashu::dhke::construct_proofs; @@ -24,6 +25,7 @@ use cdk::wallet::{ReceiveOptions, SendMemo, SendOptions}; use cdk::Amount; use cdk_fake_wallet::create_fake_invoice; use cdk_integration_tests::init_pure_tests::*; +use tokio::time::sleep; /// Tests the token swap and send functionality: /// 1. Alice gets funded with 64 sats @@ -235,15 +237,7 @@ async fn test_mint_double_spend() { .await .expect("Could not get proofs"); - let keys = mint_bob - .pubkeys() - .await - .unwrap() - .keysets - .first() - .unwrap() - .clone() - .keys; + let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let preswap = PreMintSecrets::random( @@ -300,15 +294,7 @@ async fn test_attempt_to_swap_by_overflowing() { let amount = 2_u64.pow(63); - let keys = mint_bob - .pubkeys() - .await - .unwrap() - .keysets - .first() - .unwrap() - .clone() - .keys; + let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let pre_mint_amount = @@ -429,15 +415,7 @@ pub async fn test_p2pk_swap() { let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages()); - let keys = mint_bob - .pubkeys() - .await - .unwrap() - .keysets - .first() - .cloned() - .unwrap() - .keys; + let keys = mint_bob.pubkeys().keysets.first().cloned().unwrap().keys; let post_swap = mint_bob.process_swap_request(swap_request).await.unwrap(); @@ -496,6 +474,8 @@ pub async fn test_p2pk_swap() { assert!(attempt_swap.is_ok()); + sleep(Duration::from_secs(1)).await; + let mut msgs = HashMap::new(); while let Ok((sub_id, msg)) = listener.try_recv() { assert_eq!(sub_id, "test".into()); @@ -509,10 +489,16 @@ pub async fn test_p2pk_swap() { } } - for keys in public_keys_to_listen { - let statuses = msgs.remove(&keys).expect("some events"); + for (i, key) in public_keys_to_listen.into_iter().enumerate() { + let statuses = msgs.remove(&key).expect("some events"); // Every input pk receives two state updates, as there are only two state transitions - assert_eq!(statuses, vec![State::Pending, State::Spent]); + assert_eq!( + statuses, + vec![State::Pending, State::Spent], + "failed to test key {:?} (pos {})", + key, + i, + ); } assert!(listener.try_recv().is_err(), "no other event is happening"); @@ -527,7 +513,7 @@ async fn test_swap_overpay_underpay_fee() { .expect("Failed to create test mint"); mint_bob - .rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new()) + .rotate_keyset(CurrencyUnit::Sat, 32, 1) .await .unwrap(); @@ -545,15 +531,7 @@ async fn test_swap_overpay_underpay_fee() { .await .expect("Could not get proofs"); - let keys = mint_bob - .pubkeys() - .await - .unwrap() - .keysets - .first() - .unwrap() - .clone() - .keys; + let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap(); @@ -597,7 +575,7 @@ async fn test_mint_enforce_fee() { .expect("Failed to create test mint"); mint_bob - .rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new()) + .rotate_keyset(CurrencyUnit::Sat, 32, 1) .await .unwrap(); @@ -619,15 +597,7 @@ async fn test_mint_enforce_fee() { .await .expect("Could not get proofs"); - let keys = mint_bob - .pubkeys() - .await - .unwrap() - .keysets - .first() - .unwrap() - .clone() - .keys; + let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let five_proofs: Vec<_> = proofs.drain(..5).collect(); @@ -689,7 +659,7 @@ async fn test_mint_change_with_fee_melt() { .expect("Failed to create test mint"); mint_bob - .rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new()) + .rotate_keyset(CurrencyUnit::Sat, 32, 1) .await .unwrap(); @@ -914,14 +884,6 @@ async fn test_concurrent_double_spend_melt() { } async fn get_keyset_id(mint: &Mint) -> Id { - let keys = mint - .pubkeys() - .await - .unwrap() - .keysets - .first() - .unwrap() - .clone() - .keys; + let keys = mint.pubkeys().keysets.first().unwrap().clone().keys; Id::from(&keys) } diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index a7b4956a..662ae5af 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -31,7 +31,9 @@ async fn test_correct_keyset() { let mut mint_builder = MintBuilder::new(); let localstore = Arc::new(database); - mint_builder = mint_builder.with_localstore(localstore.clone()); + mint_builder = mint_builder + .with_localstore(localstore.clone()) + .with_keystore(localstore.clone()); mint_builder = mint_builder .add_ln_backend( @@ -57,42 +59,35 @@ async fn test_correct_keyset() { let quote_ttl = QuoteTTL::new(10000, 10000); localstore.set_quote_ttl(quote_ttl).await.unwrap(); - mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0) - .await - .unwrap(); - mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0) - .await - .unwrap(); + let active = mint.get_active_keysets(); - let active = mint.localstore.get_active_keysets().await.unwrap(); + let active = active + .get(&CurrencyUnit::Sat) + .expect("There is a keyset for unit"); + let old_keyset_info = mint.get_keyset_info(active).expect("There is keyset"); + + mint.rotate_keyset(CurrencyUnit::Sat, 32, 0).await.unwrap(); + + let active = mint.get_active_keysets(); let active = active .get(&CurrencyUnit::Sat) .expect("There is a keyset for unit"); - let keyset_info = mint - .localstore - .get_keyset_info(active) - .await - .unwrap() - .expect("There is keyset"); + let keyset_info = mint.get_keyset_info(active).expect("There is keyset"); - assert!(keyset_info.derivation_path_index == Some(2)); + assert_ne!(keyset_info.id, old_keyset_info.id); + mint.rotate_keyset(CurrencyUnit::Sat, 32, 0).await.unwrap(); let mint = mint_builder.build().await.unwrap(); - let active = mint.localstore.get_active_keysets().await.unwrap(); + let active = mint.get_active_keysets(); let active = active .get(&CurrencyUnit::Sat) .expect("There is a keyset for unit"); - let keyset_info = mint - .localstore - .get_keyset_info(active) - .await - .unwrap() - .expect("There is keyset"); + let new_keyset_info = mint.get_keyset_info(active).expect("There is keyset"); - assert!(keyset_info.derivation_path_index == Some(2)); + assert_ne!(new_keyset_info.id, keyset_info.id); } diff --git a/crates/cdk-mint-rpc/src/proto/server.rs b/crates/cdk-mint-rpc/src/proto/server.rs index 1f874660..5d25afd0 100644 --- a/crates/cdk-mint-rpc/src/proto/server.rs +++ b/crates/cdk-mint-rpc/src/proto/server.rs @@ -688,7 +688,7 @@ impl CdkMint for MintRPCServer { let keyset_info = self .mint - .rotate_next_keyset( + .rotate_keyset( unit, request.max_order.map(|a| a as u8).unwrap_or(32), request.input_fee_ppk.unwrap_or(0), diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index 5c56c0bc..be6bf3a2 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -18,7 +18,7 @@ cln = ["dep:cdk-cln"] lnd = ["dep:cdk-lnd"] lnbits = ["dep:cdk-lnbits"] fakewallet = ["dep:cdk-fake-wallet"] -grpc-processor = ["dep:cdk-payment-processor"] +grpc-processor = ["dep:cdk-payment-processor", "cdk-signatory/grpc"] sqlcipher = ["cdk-sqlite/sqlcipher"] # MSRV is not committed to with redb enabled redb = ["dep:cdk-redb"] @@ -45,6 +45,7 @@ cdk-lnbits = { workspace = true, optional = true } cdk-lnd = { workspace = true, optional = true } cdk-fake-wallet = { workspace = true, optional = true } cdk-axum.workspace = true +cdk-signatory.workspace = true cdk-mint-rpc = { workspace = true, optional = true } cdk-payment-processor = { workspace = true, optional = true } config = { version = "0.15.11", features = ["toml"] } @@ -63,3 +64,8 @@ home.workspace = true url.workspace = true utoipa = { workspace = true, optional = true } utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true } + +[build-dependencies] +# Dep of utopia 2.5.0 breaks so keeping here for now +zip = "=2.4.2" +time = "=0.3.39" diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 8733a884..212f4e01 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -12,7 +12,9 @@ pub struct Info { pub url: String, pub listen_host: String, pub listen_port: u16, - pub mnemonic: String, + pub mnemonic: Option, + pub signatory_url: Option, + pub signatory_certs: Option, pub input_fee_ppk: Option, pub http_cache: cache::Config, @@ -28,8 +30,12 @@ impl std::fmt::Debug for Info { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Use a fallback approach that won't panic let mnemonic_display = { - let hash = sha256::Hash::hash(self.mnemonic.clone().into_bytes().as_ref()); - format!("") + if let Some(mnemonic) = self.mnemonic.as_ref() { + let hash = sha256::Hash::hash(mnemonic.as_bytes()); + format!("") + } else { + format!("", self.signatory_url.clone().unwrap_or_default()) + } }; f.debug_struct("Info") @@ -377,7 +383,7 @@ mod tests { url: "http://example.com".to_string(), listen_host: "127.0.0.1".to_string(), listen_port: 8080, - mnemonic: "test secret mnemonic phrase".to_string(), + mnemonic: Some("test secret mnemonic phrase".to_string()), input_fee_ppk: Some(100), ..Default::default() }; @@ -404,7 +410,7 @@ mod tests { url: "http://example.com".to_string(), listen_host: "127.0.0.1".to_string(), listen_port: 8080, - mnemonic: "".to_string(), // Empty mnemonic + mnemonic: Some("".to_string()), // Empty mnemonic enable_swagger_ui: Some(false), ..Default::default() }; @@ -423,7 +429,7 @@ mod tests { url: "http://example.com".to_string(), listen_host: "127.0.0.1".to_string(), listen_port: 8080, - mnemonic: "特殊字符 !@#$%^&*()".to_string(), // Special characters + mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), // Special characters ..Default::default() }; diff --git a/crates/cdk-mintd/src/env_vars/common.rs b/crates/cdk-mintd/src/env_vars/common.rs index 44e5b0e9..27b1cee9 100644 --- a/crates/cdk-mintd/src/env_vars/common.rs +++ b/crates/cdk-mintd/src/env_vars/common.rs @@ -6,6 +6,8 @@ pub const ENV_URL: &str = "CDK_MINTD_URL"; pub const ENV_LISTEN_HOST: &str = "CDK_MINTD_LISTEN_HOST"; pub const ENV_LISTEN_PORT: &str = "CDK_MINTD_LISTEN_PORT"; pub const ENV_MNEMONIC: &str = "CDK_MINTD_MNEMONIC"; +pub const ENV_SIGNATORY_URL: &str = "CDK_MINTD_SIGNATORY_URL"; +pub const ENV_SIGNATORY_CERTS: &str = "CDK_MINTD_SIGNATORY_CERTS"; pub const ENV_SECONDS_QUOTE_VALID: &str = "CDK_MINTD_SECONDS_QUOTE_VALID"; pub const ENV_CACHE_SECONDS: &str = "CDK_MINTD_CACHE_SECONDS"; pub const ENV_EXTEND_CACHE_SECONDS: &str = "CDK_MINTD_EXTEND_CACHE_SECONDS"; diff --git a/crates/cdk-mintd/src/env_vars/info.rs b/crates/cdk-mintd/src/env_vars/info.rs index 2bb6ed57..086e5a4f 100644 --- a/crates/cdk-mintd/src/env_vars/info.rs +++ b/crates/cdk-mintd/src/env_vars/info.rs @@ -22,8 +22,16 @@ impl Info { } } + if let Ok(signatory_url) = env::var(ENV_SIGNATORY_URL) { + self.signatory_url = Some(signatory_url); + } + + if let Ok(signatory_certs) = env::var(ENV_SIGNATORY_CERTS) { + self.signatory_certs = Some(signatory_certs); + } + if let Ok(mnemonic) = env::var(ENV_MNEMONIC) { - self.mnemonic = mnemonic; + self.mnemonic = Some(mnemonic); } if let Ok(cache_seconds_str) = env::var(ENV_CACHE_SECONDS) { diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 40e44bd2..9b73c6dd 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use axum::Router; use bip39::Mnemonic; -use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase}; +use cdk::cdk_database::{self, MintAuthDatabase}; use cdk::mint::{MintBuilder, MintMeltLimits}; // Feature-gated imports #[cfg(any( @@ -107,8 +107,6 @@ async fn main() -> anyhow::Result<()> { None => work_dir.join("config.toml"), }; - let mut mint_builder = MintBuilder::new(); - let mut settings = if config_file_arg.exists() { config::Settings::new(Some(config_file_arg)) } else { @@ -120,25 +118,28 @@ async fn main() -> anyhow::Result<()> { // ENV VARS will take **priority** over those in the config let settings = settings.from_env()?; - let localstore: Arc + Send + Sync> = - match settings.database.engine { - DatabaseEngine::Sqlite => { - let sql_db_path = work_dir.join("cdk-mintd.sqlite"); - #[cfg(not(feature = "sqlcipher"))] - let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?; - #[cfg(feature = "sqlcipher")] - let sqlite_db = MintSqliteDatabase::new(&sql_db_path, args.password).await?; + let mut mint_builder = match settings.database.engine { + DatabaseEngine::Sqlite => { + let sql_db_path = work_dir.join("cdk-mintd.sqlite"); + #[cfg(not(feature = "sqlcipher"))] + let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?; + #[cfg(feature = "sqlcipher")] + let sqlite_db = MintSqliteDatabase::new(&sql_db_path, args.password).await?; - Arc::new(sqlite_db) - } - #[cfg(feature = "redb")] - DatabaseEngine::Redb => { - let redb_path = work_dir.join("cdk-mintd.redb"); - Arc::new(MintRedbDatabase::new(&redb_path)?) - } - }; - - mint_builder = mint_builder.with_localstore(localstore); + let db = Arc::new(sqlite_db); + MintBuilder::new() + .with_localstore(db.clone()) + .with_keystore(db) + } + #[cfg(feature = "redb")] + DatabaseEngine::Redb => { + let redb_path = work_dir.join("cdk-mintd.redb"); + let db = Arc::new(MintRedbDatabase::new(&redb_path)?); + MintBuilder::new() + .with_localstore(db.clone()) + .with_keystore(db) + } + }; let mut contact_info: Option> = None; @@ -361,13 +362,31 @@ async fn main() -> anyhow::Result<()> { mint_builder = mint_builder.with_tos_url(tos_url.to_string()); } - let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?; - mint_builder = mint_builder .with_name(settings.mint_info.name) .with_version(mint_version) - .with_description(settings.mint_info.description) - .with_seed(mnemonic.to_seed_normalized("").to_vec()); + .with_description(settings.mint_info.description); + + mint_builder = if let Some(signatory_url) = settings.info.signatory_url { + tracing::info!( + "Connecting to remote signatory to {} with certs {:?}", + signatory_url, + settings.info.signatory_certs + ); + mint_builder.with_signatory(Arc::new( + cdk_signatory::SignatoryRpcClient::new(signatory_url, settings.info.signatory_certs) + .await?, + )) + } else if let Some(mnemonic) = settings + .info + .mnemonic + .map(|s| Mnemonic::from_str(&s)) + .transpose()? + { + mint_builder.with_seed(mnemonic.to_seed_normalized("").to_vec()) + } else { + bail!("No seed nor remote signatory set"); + }; let cached_endpoints = vec![ CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11), diff --git a/crates/cdk-signatory/Cargo.toml b/crates/cdk-signatory/Cargo.toml new file mode 100644 index 00000000..5653e6ca --- /dev/null +++ b/crates/cdk-signatory/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "cdk-signatory" +version = "0.9.2" +edition = "2021" +description = "CDK signatory default implementation" + +[features] +default = ["grpc"] +sqlcipher = ["cdk-sqlite/sqlcipher"] +redb = ["dep:cdk-redb"] +grpc = ["dep:tonic", "tokio/full", "dep:prost", "dep:tonic-build"] + +[dependencies] +async-trait.workspace = true +bitcoin.workspace = true +cdk-common = { workspace = true, default-features=false, features = [ + "mint", "auth", +] } +tonic = { workspace = true, optional = true } +prost = { workspace = true, optional = true } +tracing.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +# main.rs dependencies +anyhow.workspace = true +cdk-sqlite = { workspace = true, features = ["mint", "auth"] } +cdk-redb = { workspace = true, features = ["mint", "auth"], optional = true } +clap = { workspace = true } +bip39.workspace = true +home.workspace = true +thiserror.workspace = true +tracing-subscriber.workspace = true +tokio = { workspace = true, features = ["full"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] } +getrandom = { version = "0.2", features = ["js"] } + +[build-dependencies] +tonic-build = { workspace = true, features = ["prost"], optional = true } diff --git a/crates/cdk-signatory/build.rs b/crates/cdk-signatory/build.rs new file mode 100644 index 00000000..7cba4c69 --- /dev/null +++ b/crates/cdk-signatory/build.rs @@ -0,0 +1,6 @@ +fn main() { + println!("cargo:rerun-if-changed=src/proto/signatory.proto"); + + #[cfg(feature = "grpc")] + tonic_build::compile_protos("proto/signatory.proto").unwrap(); +} diff --git a/crates/cdk-signatory/generate_certs.sh b/crates/cdk-signatory/generate_certs.sh new file mode 100755 index 00000000..7a41ab97 --- /dev/null +++ b/crates/cdk-signatory/generate_certs.sh @@ -0,0 +1,51 @@ +if [ $# -eq 1 ]; then + cd "$1" || { echo "Failed to cd into '$1'"; exit 1; } +fi + +# Generate private key for Certificate Authority (CA) +openssl genrsa -out ca.key 4096 + +# Generate CA certificate +openssl req -new -x509 -days 365 -key ca.key -out ca.pem -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=MyCA" + +# Generate private key for Server +openssl genrsa -out server.key 4096 + +# Generate Certificate Signing Request (CSR) for Server +openssl req -new -key server.key -out server.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=localhost" + +# Generate Server certificate +openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -extfile <(printf "subjectAltName=DNS:localhost,DNS:my-server,IP:127.0.0.1") + +# Generate private key for Client +openssl genrsa -out client.key 4096 + +# Generate CSR for Client +openssl req -new -key client.key -out client.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=client" + +# Generate Client certificate +openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem + +# Verify the certificates +echo "Verifying Server Certificate:" +openssl verify -CAfile ca.pem server.pem + +echo "Verifying Client Certificate:" +openssl verify -CAfile ca.pem client.pem + +# Clean up CSR files (optional) +rm server.csr client.csr + +# Display certificate information +echo "Server Certificate Info:" +openssl x509 -in server.pem -text -noout | grep "Subject:\|Issuer:\|DNS:\|IP Address:" + +echo "Client Certificate Info:" +openssl x509 -in client.pem -text -noout | grep "Subject:\|Issuer:" + +# Final files you'll need: +# - ca.pem (Certificate Authority certificate) +# - server.key (Server private key) +# - server.pem (Server certificate) +# - client.key (Client private key) +# - client.pem (Client certificate) diff --git a/crates/cdk-signatory/proto/signatory.proto b/crates/cdk-signatory/proto/signatory.proto new file mode 100644 index 00000000..07576896 --- /dev/null +++ b/crates/cdk-signatory/proto/signatory.proto @@ -0,0 +1,173 @@ +syntax = "proto3"; + +package signatory; + +service Signatory { + rpc BlindSign(BlindedMessages) returns (BlindSignResponse); + rpc VerifyProofs(Proofs) returns (BooleanResponse); + // returns all the keysets for the mint + rpc Keysets(EmptyRequest) returns (KeysResponse); + // rotates the keysets + rpc RotateKeyset(RotationRequest) returns (KeyRotationResponse); +} + +enum Operation { + OPERATION_UNSPECIFIED = 0; + OPERATION_MINT = 1; + OPERATION_MELT = 2; + OPERATION_SWAP = 3; +} + +message BlindSignResponse { + Error error = 1; + BlindSignatures sigs = 2; +} + +message BlindedMessages { + repeated BlindedMessage blinded_messages = 1; + Operation operation = 2; + string correlation_id = 3; +} + +// Represents a blinded message +message BlindedMessage { + uint64 amount = 1; + string keyset_id = 2; + bytes blinded_secret = 3; +} + +message BooleanResponse { + Error error = 1; + bool success = 2; +} + +message KeyRotationResponse { + Error error = 1; + KeySet keyset = 2; +} + +message KeysResponse { + Error error = 1; + SignatoryKeysets keysets = 2; +} + +message SignatoryKeysets { + bytes pubkey = 1; + repeated KeySet keysets = 2; +} + +message KeySet { + string id = 1; + CurrencyUnit unit = 2; + bool active = 3; + uint64 input_fee_ppk = 4; + Keys keys = 5; +} + +message Keys { + map keys = 1; +} + +message RotationRequest { + CurrencyUnit unit = 1; + uint64 input_fee_ppk = 2; + uint32 max_order = 3; +} + +enum CurrencyUnitType { + CURRENCY_UNIT_TYPE_UNSPECIFIED = 0; + CURRENCY_UNIT_TYPE_SAT = 1; + CURRENCY_UNIT_TYPE_MSAT = 2; + CURRENCY_UNIT_TYPE_USD = 3; + CURRENCY_UNIT_TYPE_EUR = 4; + CURRENCY_UNIT_TYPE_AUTH = 5; +} + +message CurrencyUnit { + oneof currency_unit { + CurrencyUnitType unit = 1; + string custom_unit = 2; + } +} + +message Proofs { + repeated Proof proof = 1; + Operation operation = 3; + string correlation_id = 4; +} + +message Proof { + uint64 amount = 1; + string keyset_id = 2; + bytes secret = 3; + bytes c = 4; +} + +message ProofDLEQ { + bytes e = 1; + bytes s = 2; + bytes r = 3; +} + +message SigningResponse { + Error error = 1; + BlindSignatures blind_signatures = 2; +} +message BlindSignatures { + repeated BlindSignature blind_signatures = 1; +} + +message BlindSignature { + uint64 amount = 1; + string keyset_id = 2; + bytes blinded_secret = 3; + optional BlindSignatureDLEQ dleq = 4; +} + +message BlindSignatureDLEQ { + bytes e = 1; + bytes s = 2; +} + +// Witness type +message Witness { + oneof witness_type { + P2PKWitness p2pk_witness = 1; + HTLCWitness htlc_witness = 2; + } +} + +// P2PKWitness type +message P2PKWitness { + // List of signatures + repeated string signatures = 1; +} + +// HTLCWitness type +message HTLCWitness { + // Preimage + string preimage = 1; + // List of signatures + repeated string signatures = 2; +} + +enum ErrorCode { + ERROR_CODE_UNSPECIFIED = 0; + ERROR_CODE_AMOUNT_OUTSIDE_LIMIT = 1; + ERROR_CODE_DUPLICATE_INPUTS_PROVIDED = 2; + ERROR_CODE_DUPLICATE_OUTPUTS_PROVIDED = 3; + ERROR_CODE_KEYSET_NOT_KNOWN = 4; + ERROR_CODE_KEYSET_INACTIVE = 5; + ERROR_CODE_MINTING_DISABLED = 6; + ERROR_CODE_COULD_NOT_ROTATE_KEYSET = 7; + ERROR_CODE_INVALID_PROOF = 8; + ERROR_CODE_INVALID_BLIND_MESSAGE = 9; + ERROR_CODE_UNIT_NOT_SUPPORTED = 10; +} + +message Error { + ErrorCode code = 1; + string detail = 2; +} + +message EmptyRequest {} diff --git a/crates/cdk-signatory/src/bin/cli/mod.rs b/crates/cdk-signatory/src/bin/cli/mod.rs new file mode 100644 index 00000000..b6177ac9 --- /dev/null +++ b/crates/cdk-signatory/src/bin/cli/mod.rs @@ -0,0 +1,165 @@ +//! Signatory CLI main logic +//! +//! This logic is in this file to be excluded for wasm +use std::collections::HashMap; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; +use std::{env, fs}; + +use anyhow::{bail, Result}; +use bip39::rand::{thread_rng, Rng}; +use bip39::Mnemonic; +use cdk_common::database::MintKeysDatabase; +use cdk_common::CurrencyUnit; +#[cfg(feature = "redb")] +use cdk_redb::MintRedbDatabase; +use cdk_signatory::{db_signatory, grpc_server}; +use cdk_sqlite::MintSqliteDatabase; +use clap::Parser; +use tracing::Level; +use tracing_subscriber::EnvFilter; + +const DEFAULT_WORK_DIR: &str = ".cdk-signatory"; +const ENV_MNEMONIC: &str = "CDK_MINTD_MNEMONIC"; + +/// Simple CLI application to interact with cashu +#[derive(Parser)] +#[command(name = "cashu-signatory")] +#[command(author = "thesimplekid ")] +#[command(version = "0.1.0")] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Database engine to use (sqlite/redb) + #[arg(short, long, default_value = "sqlite")] + engine: String, + /// Database password for sqlcipher + #[arg(long)] + password: Option, + /// Path to working dir + #[arg(short, long)] + work_dir: Option, + /// Logging level + #[arg(short, long, default_value = "error")] + log_level: Level, + #[arg(long, default_value = "127.0.0.1")] + listen_addr: String, + #[arg(long, default_value = "15060")] + listen_port: u32, + #[arg(long, short)] + certs: Option, + /// Supported units with the format of name,fee and max_order + #[arg(long, short, default_value = "sat,0,32")] + units: Vec, +} + +/// Main function for the signatory standalone binary +pub async fn cli_main() -> Result<()> { + let args: Cli = Cli::parse(); + let default_filter = args.log_level; + let supported_units = args + .units + .into_iter() + .map(|unit| { + let mut parts = unit.split(",").collect::>(); + parts.reverse(); + let unit: CurrencyUnit = parts.pop().unwrap_or_default().parse()?; + let fee = parts + .pop() + .map(|x| x.parse()) + .transpose()? + .unwrap_or_default(); + let max_order = parts.pop().map(|x| x.parse()).transpose()?.unwrap_or(32); + Ok::<(_, (_, _)), anyhow::Error>((unit, (fee, max_order))) + }) + .collect::, _>>()?; + + let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn"; + + let env_filter = EnvFilter::new(format!("{default_filter},{sqlx_filter}")); + + // Parse input + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + + let work_dir = match &args.work_dir { + Some(work_dir) => work_dir.clone(), + None => { + let home_dir = home::home_dir().unwrap(); + home_dir.join(DEFAULT_WORK_DIR) + } + }; + + let certs = Some( + args.certs + .map(|x| x.into()) + .unwrap_or_else(|| work_dir.clone()), + ); + + fs::create_dir_all(&work_dir)?; + + let localstore: Arc + Send + Sync> = + match args.engine.as_str() { + "sqlite" => { + let sql_path = work_dir.join("cdk-cli.sqlite"); + #[cfg(not(feature = "sqlcipher"))] + let db = MintSqliteDatabase::new(&sql_path).await?; + #[cfg(feature = "sqlcipher")] + let db = { + match args.password { + Some(pass) => MintSqliteDatabase::new(&sql_path, pass).await?, + None => bail!("Missing database password"), + } + }; + + Arc::new(db) + } + "redb" => { + #[cfg(feature = "redb")] + { + let redb_path = work_dir.join("cdk-cli.redb"); + Arc::new(MintRedbDatabase::new(&redb_path)?) + } + #[cfg(not(feature = "redb"))] + { + bail!("redb feature not enabled"); + } + } + _ => bail!("Unknown DB engine"), + }; + + let seed_path = work_dir.join("seed"); + + let mnemonic = if let Ok(mnemonic) = env::var(ENV_MNEMONIC) { + Mnemonic::from_str(&mnemonic)? + } else { + match fs::metadata(seed_path.clone()) { + Ok(_) => { + let contents = fs::read_to_string(seed_path.clone())?; + Mnemonic::from_str(&contents)? + } + Err(_e) => { + let mut rng = thread_rng(); + let random_bytes: [u8; 32] = rng.gen(); + + let mnemonic = Mnemonic::from_entropy(&random_bytes)?; + tracing::info!("Creating new seed"); + + fs::write(seed_path, mnemonic.to_string())?; + + mnemonic + } + } + }; + let seed = mnemonic.to_seed_normalized(""); + + let signatory = + db_signatory::DbSignatory::new(localstore, &seed, supported_units, Default::default()) + .await?; + + let socket_addr = SocketAddr::from_str(&format!("{}:{}", args.listen_addr, args.listen_port))?; + + grpc_server(signatory, socket_addr, certs).await?; + + Ok(()) +} diff --git a/crates/cdk-signatory/src/bin/signatory.rs b/crates/cdk-signatory/src/bin/signatory.rs new file mode 100644 index 00000000..b5eab1f0 --- /dev/null +++ b/crates/cdk-signatory/src/bin/signatory.rs @@ -0,0 +1,15 @@ +#[cfg(not(target_arch = "wasm32"))] +mod cli; + +fn main() { + #[cfg(target_arch = "wasm32")] + println!("Not supported in wasm32"); + #[cfg(not(target_arch = "wasm32"))] + { + use tokio::runtime::Runtime; + let rt = Runtime::new().unwrap(); + rt.block_on(async { + cli::cli_main().await.unwrap(); + }); + } +} diff --git a/crates/cdk-signatory/src/common.rs b/crates/cdk-signatory/src/common.rs new file mode 100644 index 00000000..8ca945c5 --- /dev/null +++ b/crates/cdk-signatory/src/common.rs @@ -0,0 +1,156 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; +use bitcoin::secp256k1::{self, All, Secp256k1}; +use cdk_common::database; +use cdk_common::error::Error; +use cdk_common::mint::MintKeySetInfo; +use cdk_common::nuts::{CurrencyUnit, Id, MintKeySet}; +use cdk_common::util::unix_time; + +/// Initialize keysets and returns a [`Result`] with a tuple of the following: +/// * a [`HashMap`] mapping each active keyset `Id` to `MintKeySet` +/// * a [`Vec`] of `CurrencyUnit` containing active keysets units +pub async fn init_keysets( + xpriv: Xpriv, + secp_ctx: &Secp256k1, + 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)) +} + +/// Generate new [`MintKeySetInfo`] from path +#[tracing::instrument(skip_all)] +pub 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) +} + +pub 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"), + ])) +} diff --git a/crates/cdk-signatory/src/db_signatory.rs b/crates/cdk-signatory/src/db_signatory.rs new file mode 100644 index 00000000..b7f87901 --- /dev/null +++ b/crates/cdk-signatory/src/db_signatory.rs @@ -0,0 +1,345 @@ +//! Main Signatory implementation +//! +//! It is named db_signatory because it uses a database to maintain state. +use std::collections::HashMap; +use std::sync::Arc; + +use bitcoin::bip32::{DerivationPath, Xpriv}; +use bitcoin::secp256k1::{self, Secp256k1}; +use cdk_common::dhke::{sign_message, verify_message}; +use cdk_common::mint::MintKeySetInfo; +use cdk_common::nuts::{BlindSignature, BlindedMessage, CurrencyUnit, Id, MintKeySet, Proof}; +use cdk_common::{database, Error, PublicKey}; +use tokio::sync::RwLock; +use tracing::instrument; + +use crate::common::{create_new_keyset, derivation_path_from_unit, init_keysets}; +use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet, SignatoryKeysets}; + +/// In-memory Signatory +/// +/// This is the default signatory implementation for the mint. +/// +/// The private keys and the all key-related data is stored in memory, in the same process, but it +/// is not accessible from the outside. +pub struct DbSignatory { + keysets: RwLock>, + active_keysets: RwLock>, + localstore: Arc + Send + Sync>, + secp_ctx: Secp256k1, + custom_paths: HashMap, + xpriv: Xpriv, + xpub: PublicKey, +} + +impl DbSignatory { + /// Creates a new MemorySignatory instance + pub async fn new( + localstore: Arc + Send + Sync>, + seed: &[u8], + mut 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, active_keyset_units) = init_keysets( + xpriv, + &secp_ctx, + &localstore, + &supported_units, + &custom_paths, + ) + .await?; + + supported_units.entry(CurrencyUnit::Auth).or_insert((0, 1)); + + // Create new keysets for supported units that aren't covered by the current keysets + for (unit, (fee, max_order)) in supported_units { + if !active_keyset_units.contains(&unit) { + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => { + derivation_path_from_unit(unit.clone(), 0).ok_or(Error::UnsupportedUnit)? + } + }; + + let (keyset, keyset_info) = create_new_keyset( + &secp_ctx, + xpriv, + derivation_path, + Some(0), + unit.clone(), + max_order, + fee, + ); + + let id = keyset_info.id; + localstore.add_keyset_info(keyset_info).await?; + localstore.set_active_keyset(unit, id).await?; + active_keysets.insert(id, keyset); + } + } + + let keys = Self { + keysets: Default::default(), + active_keysets: Default::default(), + localstore, + custom_paths, + xpub: xpriv.to_keypair(&secp_ctx).public_key().into(), + secp_ctx, + xpriv, + }; + keys.reload_keys_from_db().await?; + + Ok(keys) + } + + /// Load all the keysets from the database, even if they are not active. + /// + /// Since the database is owned by this process, we can load all the keysets in memory, and use + /// it as the primary source, and the database as the persistence layer. + /// + /// Any operation performed with keysets, are done through this trait and never to the database + /// directly. + async fn reload_keys_from_db(&self) -> Result<(), Error> { + let mut keysets = self.keysets.write().await; + let mut active_keysets = self.active_keysets.write().await; + keysets.clear(); + active_keysets.clear(); + + let db_active_keysets = self.localstore.get_active_keysets().await?; + + for mut info in self.localstore.get_keyset_infos().await? { + let id = info.id; + let keyset = self.generate_keyset(&info); + info.active = db_active_keysets.get(&info.unit) == Some(&info.id); + if info.active { + active_keysets.insert(info.unit.clone(), id); + } + keysets.insert(id, (info, keyset)); + } + + Ok(()) + } + + fn generate_keyset(&self, keyset_info: &MintKeySetInfo) -> MintKeySet { + MintKeySet::generate_from_xpriv( + &self.secp_ctx, + self.xpriv, + keyset_info.max_order, + keyset_info.unit.clone(), + keyset_info.derivation_path.clone(), + ) + } +} + +#[async_trait::async_trait] +impl Signatory for DbSignatory { + fn name(&self) -> String { + format!("Signatory {}", env!("CARGO_PKG_VERSION")) + } + + #[instrument(skip_all)] + async fn blind_sign( + &self, + blinded_messages: Vec, + ) -> Result, Error> { + let keysets = self.keysets.read().await; + + blinded_messages + .into_iter() + .map(|blinded_message| { + let BlindedMessage { + amount, + blinded_secret, + keyset_id, + .. + } = blinded_message; + + let (info, key) = keysets.get(&keyset_id).ok_or(Error::UnknownKeySet)?; + if !info.active { + return Err(Error::InactiveKeyset); + } + + let key_pair = key.keys.get(&amount).ok_or(Error::UnknownKeySet)?; + let c = sign_message(&key_pair.secret_key, &blinded_secret)?; + + let blinded_signature = BlindSignature::new( + amount, + c, + keyset_id, + &blinded_message.blinded_secret, + key_pair.secret_key.clone(), + )?; + + Ok(blinded_signature) + }) + .collect::, _>>() + } + + #[tracing::instrument(skip_all)] + async fn verify_proofs(&self, proofs: Vec) -> Result<(), Error> { + let keysets = self.keysets.read().await; + + proofs.into_iter().try_for_each(|proof| { + let (_, key) = keysets.get(&proof.keyset_id).ok_or(Error::UnknownKeySet)?; + let key_pair = key.keys.get(&proof.amount).ok_or(Error::UnknownKeySet)?; + verify_message(&key_pair.secret_key, proof.c, proof.secret.as_bytes())?; + Ok(()) + }) + } + + #[tracing::instrument(skip_all)] + async fn keysets(&self) -> Result { + Ok(SignatoryKeysets { + pubkey: self.xpub, + keysets: self + .keysets + .read() + .await + .values() + .map(|k| k.into()) + .collect::>(), + }) + } + + /// Add current keyset to inactive keysets + /// Generate new keyset + #[tracing::instrument(skip(self))] + async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result { + let path_index = if let Some(current_keyset_id) = + self.localstore.get_active_keyset_id(&args.unit).await? + { + let keyset_info = self + .localstore + .get_keyset_info(¤t_keyset_id) + .await? + .ok_or(Error::UnknownKeySet)?; + + keyset_info.derivation_path_index.unwrap_or(1) + 1 + } else { + 1 + }; + + let derivation_path = match self.custom_paths.get(&args.unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(args.unit.clone(), path_index) + .ok_or(Error::UnsupportedUnit)?, + }; + + let (keyset, info) = create_new_keyset( + &self.secp_ctx, + self.xpriv, + derivation_path, + Some(path_index), + args.unit.clone(), + args.max_order, + args.input_fee_ppk, + ); + let id = info.id; + self.localstore.add_keyset_info(info.clone()).await?; + self.localstore.set_active_keyset(args.unit, id).await?; + + self.reload_keys_from_db().await?; + + Ok((&(info, keyset)).into()) + } +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + + use bitcoin::key::Secp256k1; + use bitcoin::Network; + use cdk_common::{Amount, MintKeySet, PublicKey}; + + use super::*; + + #[test] + fn mint_mod_generate_keyset_from_seed() { + let seed = "test_seed".as_bytes(); + let keyset = MintKeySet::generate_from_seed( + &Secp256k1::new(), + seed, + 2, + CurrencyUnit::Sat, + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), + ); + + assert_eq!(keyset.unit, CurrencyUnit::Sat); + assert_eq!(keyset.keys.len(), 2); + + let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ + ( + Amount::from(1), + PublicKey::from_hex( + "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", + ) + .unwrap(), + ), + ( + Amount::from(2), + PublicKey::from_hex( + "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", + ) + .unwrap(), + ), + ] + .into_iter() + .collect(); + + let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset + .keys + .iter() + .map(|(amount, pair)| (*amount, pair.public_key)) + .collect(); + + assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); + } + + #[test] + fn mint_mod_generate_keyset_from_xpriv() { + let seed = "test_seed".as_bytes(); + let network = Network::Bitcoin; + let xpriv = Xpriv::new_master(network, seed).expect("Failed to create xpriv"); + let keyset = MintKeySet::generate_from_xpriv( + &Secp256k1::new(), + xpriv, + 2, + CurrencyUnit::Sat, + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), + ); + + assert_eq!(keyset.unit, CurrencyUnit::Sat); + assert_eq!(keyset.keys.len(), 2); + + let expected_amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = vec![ + ( + Amount::from(1), + PublicKey::from_hex( + "0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e", + ) + .unwrap(), + ), + ( + Amount::from(2), + PublicKey::from_hex( + "03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e", + ) + .unwrap(), + ), + ] + .into_iter() + .collect(); + + let amounts_and_pubkeys: HashSet<(Amount, PublicKey)> = keyset + .keys + .iter() + .map(|(amount, pair)| (*amount, pair.public_key)) + .collect(); + + assert_eq!(amounts_and_pubkeys, expected_amounts_and_pubkeys); + } +} diff --git a/crates/cdk-signatory/src/embedded.rs b/crates/cdk-signatory/src/embedded.rs new file mode 100644 index 00000000..72924c32 --- /dev/null +++ b/crates/cdk-signatory/src/embedded.rs @@ -0,0 +1,147 @@ +//! Run a Signatory in a embedded environment, inside a CDK instance, but this wrapper makes sure to +//! run the Signatory in another thread, isolated form the main CDK, communicating through messages +use std::sync::Arc; + +use cdk_common::{BlindSignature, BlindedMessage, Error, Proof}; +use tokio::sync::{mpsc, oneshot}; +use tokio::task::JoinHandle; + +use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet, SignatoryKeysets}; + +enum Request { + BlindSign( + ( + Vec, + oneshot::Sender, Error>>, + ), + ), + VerifyProof((Vec, oneshot::Sender>)), + Keysets(oneshot::Sender>), + RotateKeyset( + ( + RotateKeyArguments, + oneshot::Sender>, + ), + ), +} + +/// Creates a service-like to wrap an implementation of the Signatory +/// +/// This implements the actor model, ensuring the Signatory and their private key is moved from the +/// main thread to their own tokio task, and communicates with the main program by passing messages, +/// an extra layer of security to move the keys to another layer. +pub struct Service { + pipeline: mpsc::Sender, + runner: Option>, +} + +impl Drop for Service { + fn drop(&mut self) { + if let Some(runner) = self.runner.take() { + runner.abort(); + } + } +} + +impl Service { + /// Takes a signatory and spawns it into a Tokio task, isolating its implementation with the + /// main thread, communicating with it through messages + pub fn new(handler: Arc) -> Self { + let (tx, rx) = mpsc::channel(10_000); + let runner = Some(tokio::spawn(Self::runner(rx, handler))); + + Self { + pipeline: tx, + runner, + } + } + + #[tracing::instrument(skip_all)] + async fn runner( + mut receiver: mpsc::Receiver, + handler: Arc, + ) { + while let Some(request) = receiver.recv().await { + match request { + Request::BlindSign((blinded_message, response)) => { + let output = handler.blind_sign(blinded_message).await; + if let Err(err) = response.send(output) { + tracing::error!("Error sending response: {:?}", err); + } + } + Request::VerifyProof((proof, response)) => { + let output = handler.verify_proofs(proof).await; + if let Err(err) = response.send(output) { + tracing::error!("Error sending response: {:?}", err); + } + } + Request::Keysets(response) => { + let output = handler.keysets().await; + if let Err(err) = response.send(output) { + tracing::error!("Error sending response: {:?}", err); + } + } + Request::RotateKeyset((args, response)) => { + let output = handler.rotate_keyset(args).await; + if let Err(err) = response.send(output) { + tracing::error!("Error sending response: {:?}", err); + } + } + } + } + } +} + +#[async_trait::async_trait] +impl Signatory for Service { + fn name(&self) -> String { + "Embedded".to_owned() + } + + #[tracing::instrument(skip_all)] + async fn blind_sign( + &self, + blinded_messages: Vec, + ) -> Result, Error> { + let (tx, rx) = oneshot::channel(); + self.pipeline + .send(Request::BlindSign((blinded_messages, tx))) + .await + .map_err(|e| Error::SendError(e.to_string()))?; + + rx.await.map_err(|e| Error::RecvError(e.to_string()))? + } + + #[tracing::instrument(skip_all)] + async fn verify_proofs(&self, proofs: Vec) -> Result<(), Error> { + let (tx, rx) = oneshot::channel(); + self.pipeline + .send(Request::VerifyProof((proofs, tx))) + .await + .map_err(|e| Error::SendError(e.to_string()))?; + + rx.await.map_err(|e| Error::RecvError(e.to_string()))? + } + + #[tracing::instrument(skip_all)] + async fn keysets(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.pipeline + .send(Request::Keysets(tx)) + .await + .map_err(|e| Error::SendError(e.to_string()))?; + + rx.await.map_err(|e| Error::RecvError(e.to_string()))? + } + + #[tracing::instrument(skip(self))] + async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result { + let (tx, rx) = oneshot::channel(); + self.pipeline + .send(Request::RotateKeyset((args, tx))) + .await + .map_err(|e| Error::SendError(e.to_string()))?; + + rx.await.map_err(|e| Error::RecvError(e.to_string()))? + } +} diff --git a/crates/cdk-signatory/src/lib.rs b/crates/cdk-signatory/src/lib.rs new file mode 100644 index 00000000..6485b62d --- /dev/null +++ b/crates/cdk-signatory/src/lib.rs @@ -0,0 +1,21 @@ +//! In memory signatory +//! +//! Implements the Signatory trait from cdk-common to manage the key in-process, to be included +//! inside the mint to be executed as a single process. +//! +//! Even if it is embedded in the same process, the keys are not accessible from the outside of this +//! module, all communication is done through the Signatory trait and the signatory manager. +#![deny(missing_docs)] +#![deny(warnings)] + +#[cfg(feature = "grpc")] +mod proto; + +#[cfg(feature = "grpc")] +pub use proto::{client::SignatoryRpcClient, server::grpc_server}; + +mod common; + +pub mod db_signatory; +pub mod embedded; +pub mod signatory; diff --git a/crates/cdk-signatory/src/proto/client.rs b/crates/cdk-signatory/src/proto/client.rs new file mode 100644 index 00000000..0abaaf30 --- /dev/null +++ b/crates/cdk-signatory/src/proto/client.rs @@ -0,0 +1,158 @@ +use std::path::Path; + +use cdk_common::error::Error; +use cdk_common::{BlindSignature, BlindedMessage, Proof}; +use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; + +use crate::proto::signatory_client::SignatoryClient; +use crate::signatory::{RotateKeyArguments, Signatory, SignatoryKeySet, SignatoryKeysets}; + +/// A client for the Signatory service. +pub struct SignatoryRpcClient { + client: SignatoryClient, + url: String, +} + +#[derive(thiserror::Error, Debug)] +/// Client Signatory Error +pub enum ClientError { + /// Transport error + #[error(transparent)] + Transport(#[from] tonic::transport::Error), + + /// IO-related errors + #[error(transparent)] + Io(#[from] std::io::Error), + + /// Signatory Error + #[error(transparent)] + Signatory(#[from] cdk_common::error::Error), + + /// Invalid URL + #[error("Invalid URL")] + InvalidUrl, +} + +impl SignatoryRpcClient { + /// Create a new RemoteSigner from a tonic transport channel. + pub async fn new>(url: String, tls_dir: Option) -> Result { + let channel = if let Some(tls_dir) = tls_dir { + let tls_dir = tls_dir.as_ref(); + let server_root_ca_cert = std::fs::read_to_string(tls_dir.join("ca.pem"))?; + let server_root_ca_cert = Certificate::from_pem(server_root_ca_cert); + let client_cert = std::fs::read_to_string(tls_dir.join("client.pem"))?; + let client_key = std::fs::read_to_string(tls_dir.join("client.key"))?; + let client_identity = Identity::from_pem(client_cert, client_key); + let tls = ClientTlsConfig::new() + .ca_certificate(server_root_ca_cert) + .identity(client_identity); + + Channel::from_shared(url.clone()) + .map_err(|_| ClientError::InvalidUrl)? + .tls_config(tls)? + .connect() + .await? + } else { + Channel::from_shared(url.clone()) + .map_err(|_| ClientError::InvalidUrl)? + .connect() + .await? + }; + + Ok(Self { + client: SignatoryClient::new(channel), + url, + }) + } +} + +macro_rules! handle_error { + ($x:expr, $y:ident, scalar) => {{ + let mut obj = $x.into_inner(); + if let Some(err) = obj.error.take() { + return Err(err.into()); + } + + obj.$y + }}; + ($x:expr, $y:ident) => {{ + let mut obj = $x.into_inner(); + if let Some(err) = obj.error.take() { + return Err(err.into()); + } + + obj.$y + .take() + .ok_or(Error::Custom("Internal error".to_owned()))? + }}; +} + +#[async_trait::async_trait] +impl Signatory for SignatoryRpcClient { + fn name(&self) -> String { + format!("Rpc Signatory {}", self.url) + } + + #[tracing::instrument(skip_all)] + async fn blind_sign(&self, request: Vec) -> Result, Error> { + let req = super::BlindedMessages { + blinded_messages: request + .into_iter() + .map(|blind_message| blind_message.into()) + .collect(), + operation: super::Operation::Unspecified.into(), + correlation_id: "".to_owned(), + }; + + self.client + .clone() + .blind_sign(req) + .await + .map(|response| { + handle_error!(response, sigs) + .blind_signatures + .into_iter() + .map(|blinded_signature| blinded_signature.try_into()) + .collect() + }) + .map_err(|e| Error::Custom(e.to_string()))? + } + + #[tracing::instrument(skip_all)] + async fn verify_proofs(&self, proofs: Vec) -> Result<(), Error> { + let req: super::Proofs = proofs.into(); + self.client + .clone() + .verify_proofs(req) + .await + .map(|response| { + if handle_error!(response, success, scalar) { + Ok(()) + } else { + Err(Error::SignatureMissingOrInvalid) + } + }) + .map_err(|e| Error::Custom(e.to_string()))? + } + + #[tracing::instrument(skip_all)] + async fn keysets(&self) -> Result { + self.client + .clone() + .keysets(super::EmptyRequest {}) + .await + .map(|response| handle_error!(response, keysets).try_into()) + .map_err(|e| Error::Custom(e.to_string()))? + } + + #[tracing::instrument(skip(self))] + async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result { + let req: super::RotationRequest = args.into(); + self.client + .clone() + .rotate_keyset(req) + .await + .map(|response| handle_error!(response, keyset).try_into()) + .map_err(|e| Error::Custom(e.to_string()))? + } +} diff --git a/crates/cdk-signatory/src/proto/convert.rs b/crates/cdk-signatory/src/proto/convert.rs new file mode 100644 index 00000000..e2842cdb --- /dev/null +++ b/crates/cdk-signatory/src/proto/convert.rs @@ -0,0 +1,455 @@ +//! Type conversions between Rust types and the generated protobuf types. +use std::collections::BTreeMap; + +use cdk_common::secret::Secret; +use cdk_common::util::hex; +use cdk_common::{Amount, HTLCWitness, P2PKWitness, PublicKey}; +use tonic::Status; + +use super::*; + +const INTERNAL_ERROR: &str = "Missing property"; + +impl From for SignatoryKeysets { + fn from(keyset: crate::signatory::SignatoryKeysets) -> Self { + Self { + pubkey: keyset.pubkey.to_bytes().to_vec(), + keysets: keyset + .keysets + .into_iter() + .map(|keyset| keyset.into()) + .collect(), + } + } +} + +impl TryInto for SignatoryKeysets { + /// TODO: Make sure that all type Error here are cdk_common::Error + type Error = cdk_common::Error; + + fn try_into(self) -> Result { + Ok(crate::signatory::SignatoryKeysets { + pubkey: PublicKey::from_slice(&self.pubkey)?, + keysets: self + .keysets + .into_iter() + .map(|keyset| keyset.try_into()) + .collect::, _>>()?, + }) + } +} + +impl TryInto for KeySet { + type Error = cdk_common::Error; + + fn try_into(self) -> Result { + Ok(crate::signatory::SignatoryKeySet { + id: self.id.parse()?, + unit: self + .unit + .ok_or(cdk_common::Error::Custom(INTERNAL_ERROR.to_owned()))? + .try_into() + .map_err(|_| cdk_common::Error::Custom("Invalid currency unit".to_owned()))?, + active: self.active, + input_fee_ppk: self.input_fee_ppk, + keys: cdk_common::Keys::new( + self.keys + .ok_or(cdk_common::Error::Custom(INTERNAL_ERROR.to_owned()))? + .keys + .into_iter() + .map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk))) + .collect::, _>>()?, + ), + }) + } +} + +impl From for KeySet { + fn from(keyset: crate::signatory::SignatoryKeySet) -> Self { + Self { + id: keyset.id.to_string(), + unit: Some(keyset.unit.into()), + active: keyset.active, + input_fee_ppk: keyset.input_fee_ppk, + keys: Some(Keys { + keys: keyset + .keys + .iter() + .map(|(key, value)| ((*key).into(), value.to_bytes().to_vec())) + .collect(), + }), + } + } +} + +impl From for Error { + fn from(err: cdk_common::Error) -> Self { + let code = match err { + cdk_common::Error::AmountError(_) => ErrorCode::AmountOutsideLimit, + cdk_common::Error::DuplicateInputs => ErrorCode::DuplicateInputsProvided, + cdk_common::Error::DuplicateOutputs => ErrorCode::DuplicateInputsProvided, + cdk_common::Error::UnknownKeySet => ErrorCode::KeysetNotKnown, + cdk_common::Error::InactiveKeyset => ErrorCode::KeysetInactive, + _ => ErrorCode::Unspecified, + }; + + Error { + code: code.into(), + detail: err.to_string(), + } + } +} + +impl From for cdk_common::Error { + fn from(val: Error) -> Self { + match val.code.try_into().expect("valid code") { + ErrorCode::AmountOutsideLimit => { + cdk_common::Error::AmountError(cdk_common::amount::Error::AmountOverflow) + } + ErrorCode::DuplicateInputsProvided => cdk_common::Error::DuplicateInputs, + ErrorCode::KeysetNotKnown => cdk_common::Error::UnknownKeySet, + ErrorCode::KeysetInactive => cdk_common::Error::InactiveKeyset, + ErrorCode::Unspecified => cdk_common::Error::Custom(val.detail), + _ => todo!(), + } + } +} + +impl From 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 From> for Proofs { + fn from(value: Vec) -> Self { + Proofs { + proof: value.into_iter().map(|x| x.into()).collect(), + operation: Operation::Unspecified.into(), + correlation_id: "".to_owned(), + } + } +} + +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(), + } + } +} + +impl TryInto for Proof { + type Error = Status; + fn try_into(self) -> Result { + let secret = if let Ok(str) = String::from_utf8(self.secret.clone()) { + str + } else { + hex::encode(&self.secret) + }; + + Ok(cdk_common::Proof { + amount: self.amount.into(), + keyset_id: self + .keyset_id + .parse() + .map_err(|e| Status::from_error(Box::new(e)))?, + secret: Secret::new(secret), + c: cdk_common::PublicKey::from_slice(&self.c) + .map_err(|e| Status::from_error(Box::new(e)))?, + witness: None, + dleq: None, + }) + } +} + +impl From 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 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 BlindedMessage { + fn from(value: cdk_common::BlindedMessage) -> Self { + BlindedMessage { + amount: value.amount.into(), + keyset_id: value.keyset_id.to_string(), + blinded_secret: value.blinded_secret.to_bytes().to_vec(), + } + } +} + +impl TryInto 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: None, + }) + } +} + +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")), + } + } +} + +impl From<()> for EmptyRequest { + fn from(_: ()) -> Self { + EmptyRequest {} + } +} + +impl TryInto<()> for EmptyRequest { + type Error = cdk_common::error::Error; + + fn try_into(self) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl From for CurrencyUnit { + fn from(value: cdk_common::CurrencyUnit) -> Self { + match value { + cdk_common::CurrencyUnit::Sat => CurrencyUnit { + currency_unit: Some(currency_unit::CurrencyUnit::Unit( + CurrencyUnitType::Sat.into(), + )), + }, + cdk_common::CurrencyUnit::Msat => CurrencyUnit { + currency_unit: Some(currency_unit::CurrencyUnit::Unit( + CurrencyUnitType::Msat.into(), + )), + }, + cdk_common::CurrencyUnit::Usd => CurrencyUnit { + currency_unit: Some(currency_unit::CurrencyUnit::Unit( + CurrencyUnitType::Usd.into(), + )), + }, + cdk_common::CurrencyUnit::Eur => CurrencyUnit { + currency_unit: Some(currency_unit::CurrencyUnit::Unit( + CurrencyUnitType::Eur.into(), + )), + }, + cdk_common::CurrencyUnit::Auth => CurrencyUnit { + currency_unit: Some(currency_unit::CurrencyUnit::Unit( + CurrencyUnitType::Auth.into(), + )), + }, + cdk_common::CurrencyUnit::Custom(name) => CurrencyUnit { + currency_unit: Some(currency_unit::CurrencyUnit::CustomUnit(name)), + }, + _ => unreachable!(), + } + } +} + +impl TryInto for CurrencyUnit { + type Error = Status; + + fn try_into(self) -> Result { + match self.currency_unit { + Some(currency_unit::CurrencyUnit::Unit(u)) => match u + .try_into() + .map_err(|_| Status::invalid_argument("Invalid currency unit"))? + { + CurrencyUnitType::Sat => Ok(cdk_common::CurrencyUnit::Sat), + CurrencyUnitType::Msat => Ok(cdk_common::CurrencyUnit::Msat), + CurrencyUnitType::Usd => Ok(cdk_common::CurrencyUnit::Usd), + CurrencyUnitType::Eur => Ok(cdk_common::CurrencyUnit::Eur), + CurrencyUnitType::Auth => Ok(cdk_common::CurrencyUnit::Auth), + CurrencyUnitType::Unspecified => { + Err(Status::invalid_argument("Current unit is not specified")) + } + }, + Some(currency_unit::CurrencyUnit::CustomUnit(name)) => { + Ok(cdk_common::CurrencyUnit::Custom(name)) + } + None => Err(Status::invalid_argument("Currency unit not set")), + } + } +} + +impl TryInto for KeySet { + type Error = cdk_common::error::Error; + fn try_into(self) -> Result { + Ok(cdk_common::KeySet { + id: self + .id + .parse() + .map_err(|_| cdk_common::error::Error::Custom("Invalid ID".to_owned()))?, + unit: self + .unit + .ok_or(cdk_common::error::Error::Custom(INTERNAL_ERROR.to_owned()))? + .try_into() + .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?, + keys: cdk_common::Keys::new( + self.keys + .ok_or(cdk_common::error::Error::Custom(INTERNAL_ERROR.to_owned()))? + .keys + .into_iter() + .map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk))) + .collect::, _>>()?, + ), + }) + } +} + +impl From for RotationRequest { + fn from(value: crate::signatory::RotateKeyArguments) -> Self { + Self { + unit: Some(value.unit.into()), + max_order: value.max_order.into(), + input_fee_ppk: value.input_fee_ppk, + } + } +} + +impl TryInto for RotationRequest { + type Error = Status; + + fn try_into(self) -> Result { + Ok(crate::signatory::RotateKeyArguments { + unit: self + .unit + .ok_or(Status::invalid_argument("unit not set"))? + .try_into()?, + max_order: self + .max_order + .try_into() + .map_err(|_| Status::invalid_argument("Invalid max_order"))?, + input_fee_ppk: self.input_fee_ppk, + }) + } +} + +impl From for KeySet { + fn from(value: cdk_common::KeySetInfo) -> Self { + Self { + id: value.id.into(), + unit: Some(value.unit.into()), + active: value.active, + input_fee_ppk: value.input_fee_ppk, + keys: Default::default(), + } + } +} + +impl TryInto for KeySet { + type Error = cdk_common::Error; + + fn try_into(self) -> Result { + Ok(cdk_common::KeySetInfo { + id: self.id.try_into()?, + unit: self + .unit + .ok_or(cdk_common::Error::Custom(INTERNAL_ERROR.to_owned()))? + .try_into() + .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?, + active: self.active, + input_fee_ppk: self.input_fee_ppk, + }) + } +} diff --git a/crates/cdk-signatory/src/proto/mod.rs b/crates/cdk-signatory/src/proto/mod.rs new file mode 100644 index 00000000..c72eb5f6 --- /dev/null +++ b/crates/cdk-signatory/src/proto/mod.rs @@ -0,0 +1,6 @@ +mod convert; + +tonic::include_proto!("signatory"); + +pub mod client; +pub mod server; diff --git a/crates/cdk-signatory/src/proto/server.rs b/crates/cdk-signatory/src/proto/server.rs new file mode 100644 index 00000000..4fa6f95a --- /dev/null +++ b/crates/cdk-signatory/src/proto/server.rs @@ -0,0 +1,222 @@ +use std::net::SocketAddr; +use std::path::Path; + +use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig}; +use tonic::{Request, Response, Status}; + +use crate::proto::{self, signatory_server}; +use crate::signatory::Signatory; + +pub struct CdkSignatoryServer +where + T: Signatory + Send + Sync + 'static, +{ + inner: T, +} + +#[tonic::async_trait] +impl signatory_server::Signatory for CdkSignatoryServer +where + T: Signatory + Send + Sync + 'static, +{ + #[tracing::instrument(skip_all)] + async fn blind_sign( + &self, + request: Request, + ) -> Result, Status> { + let result = match self + .inner + .blind_sign( + request + .into_inner() + .blinded_messages + .into_iter() + .map(|blind_message| blind_message.try_into()) + .collect::, _>>()?, + ) + .await + { + Ok(blind_signatures) => proto::BlindSignResponse { + sigs: Some(proto::BlindSignatures { + blind_signatures: blind_signatures + .into_iter() + .map(|blind_sign| blind_sign.into()) + .collect(), + }), + ..Default::default() + }, + Err(err) => proto::BlindSignResponse { + error: Some(err.into()), + ..Default::default() + }, + }; + + Ok(Response::new(result)) + } + + #[tracing::instrument(skip_all)] + async fn verify_proofs( + &self, + request: Request, + ) -> Result, Status> { + let result = match self + .inner + .verify_proofs( + request + .into_inner() + .proof + .into_iter() + .map(|x| x.try_into()) + .collect::, _>>()?, + ) + .await + { + Ok(()) => proto::BooleanResponse { + success: true, + ..Default::default() + }, + + Err(cdk_common::Error::DHKE(_)) => proto::BooleanResponse { + success: false, + ..Default::default() + }, + Err(err) => proto::BooleanResponse { + error: Some(err.into()), + ..Default::default() + }, + }; + + Ok(Response::new(result)) + } + + async fn keysets( + &self, + _request: Request, + ) -> Result, Status> { + let result = match self.inner.keysets().await { + Ok(result) => proto::KeysResponse { + keysets: Some(result.into()), + ..Default::default() + }, + Err(err) => proto::KeysResponse { + error: Some(err.into()), + ..Default::default() + }, + }; + + Ok(Response::new(result)) + } + + async fn rotate_keyset( + &self, + request: Request, + ) -> Result, Status> { + let mint_keyset_info = match self + .inner + .rotate_keyset(request.into_inner().try_into()?) + .await + { + Ok(result) => proto::KeyRotationResponse { + keyset: Some(result.into()), + ..Default::default() + }, + Err(err) => proto::KeyRotationResponse { + error: Some(err.into()), + ..Default::default() + }, + }; + + Ok(Response::new(mint_keyset_info)) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Transport error + #[error(transparent)] + Transport(#[from] tonic::transport::Error), + /// Io error + #[error(transparent)] + Io(#[from] std::io::Error), +} + +/// Runs the signatory server +pub async fn grpc_server>( + signatory: T, + addr: SocketAddr, + tls_dir: Option, +) -> Result<(), Error> +where + T: Signatory + Send + Sync + 'static, +{ + tracing::info!("Starting RPC server {}", addr); + + let mut server = match tls_dir { + Some(tls_dir) => { + tracing::info!("TLS configuration found, starting secure server"); + let tls_dir = tls_dir.as_ref(); + let server_pem_path = tls_dir.join("server.pem"); + let server_key_path = tls_dir.join("server.key"); + let ca_pem_path = tls_dir.join("ca.pem"); + + if !server_pem_path.exists() { + tracing::error!( + "Server certificate file does not exist: {}", + server_pem_path.display() + ); + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!( + "Server certificate file not found: {}", + server_pem_path.display() + ), + ))); + } + + if !server_key_path.exists() { + tracing::error!( + "Server key file does not exist: {}", + server_key_path.display() + ); + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Server key file not found: {}", server_key_path.display()), + ))); + } + + if !ca_pem_path.exists() { + tracing::error!( + "CA certificate file does not exist: {}", + ca_pem_path.display() + ); + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("CA certificate file not found: {}", ca_pem_path.display()), + ))); + } + + let cert = std::fs::read_to_string(&server_pem_path)?; + let key = std::fs::read_to_string(&server_key_path)?; + let client_ca_cert = std::fs::read_to_string(&ca_pem_path)?; + let client_ca_cert = Certificate::from_pem(client_ca_cert); + let server_identity = Identity::from_pem(cert, key); + let tls_config = ServerTlsConfig::new() + .identity(server_identity) + .client_ca_root(client_ca_cert); + + Server::builder().tls_config(tls_config)? + } + None => { + tracing::warn!("No valid TLS configuration found, starting insecure server"); + Server::builder() + } + }; + + server + .add_service(signatory_server::SignatoryServer::new(CdkSignatoryServer { + inner: signatory, + })) + .serve(addr) + .await?; + Ok(()) +} diff --git a/crates/cdk-signatory/src/signatory.rs b/crates/cdk-signatory/src/signatory.rs new file mode 100644 index 00000000..88de075b --- /dev/null +++ b/crates/cdk-signatory/src/signatory.rs @@ -0,0 +1,152 @@ +//! Signatory mod +//! +//! This module abstract all the key related operations, defining an interface for the necessary +//! operations, to be implemented by the different signatory implementations. +//! +//! There is an in memory implementation, when the keys are stored in memory, in the same process, +//! but it is isolated from the rest of the application, and they communicate through a channel with +//! the defined API. +use cdk_common::error::Error; +use cdk_common::mint::MintKeySetInfo; +use cdk_common::{ + BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, Keys, MintKeySet, Proof, PublicKey, +}; + +#[derive(Debug)] +/// Type alias to make the keyset info API more useful, queryable by unit and Id +pub enum KeysetIdentifier { + /// Mint Keyset by unit + Unit(CurrencyUnit), + /// Mint Keyset by Id + Id(Id), +} + +impl From for KeysetIdentifier { + fn from(id: Id) -> Self { + Self::Id(id) + } +} + +impl From for KeysetIdentifier { + fn from(unit: CurrencyUnit) -> Self { + Self::Unit(unit) + } +} + +/// RotateKeyArguments +/// +/// This struct is used to pass the arguments to the rotate_keyset function +/// +/// TODO: Change argument to accept a vector of Amount instead of max_order. +#[derive(Debug, Clone)] +pub struct RotateKeyArguments { + /// Unit + pub unit: CurrencyUnit, + /// Max order + pub max_order: u8, + /// Input fee + pub input_fee_ppk: u64, +} + +#[derive(Debug, Clone)] +/// Signatory keysets +pub struct SignatoryKeysets { + /// The public key + pub pubkey: PublicKey, + /// The list of keysets + pub keysets: Vec, +} + +#[derive(Debug, Clone)] +/// SignatoryKeySet +/// +/// This struct is used to represent a keyset and its info, pretty much all the information but the +/// private key, that will never leave the signatory +pub struct SignatoryKeySet { + /// The keyset Id + pub id: Id, + /// The Currency Unit + pub unit: CurrencyUnit, + /// Whether to set it as active or not + pub active: bool, + /// The list of public keys + pub keys: Keys, + /// Information about the fee per public key + pub input_fee_ppk: u64, +} + +impl From<&SignatoryKeySet> for KeySet { + fn from(val: &SignatoryKeySet) -> Self { + val.to_owned().into() + } +} + +impl From for KeySet { + fn from(val: SignatoryKeySet) -> Self { + KeySet { + id: val.id, + unit: val.unit, + keys: val.keys, + } + } +} + +impl From<&SignatoryKeySet> for MintKeySetInfo { + fn from(val: &SignatoryKeySet) -> Self { + val.to_owned().into() + } +} + +impl From for MintKeySetInfo { + fn from(val: SignatoryKeySet) -> Self { + MintKeySetInfo { + id: val.id, + unit: val.unit, + active: val.active, + input_fee_ppk: val.input_fee_ppk, + derivation_path: Default::default(), + derivation_path_index: Default::default(), + max_order: 0, + valid_to: None, + valid_from: 0, + } + } +} + +impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet { + fn from((info, key): &(MintKeySetInfo, MintKeySet)) -> Self { + Self { + id: info.id, + unit: key.unit.clone(), + active: info.active, + input_fee_ppk: info.input_fee_ppk, + keys: key.keys.clone().into(), + } + } +} + +#[async_trait::async_trait] +/// Signatory trait +pub trait Signatory { + /// The Signatory implementation name. This may be exposed, so being as discreet as possible is + /// advised. + fn name(&self) -> String; + + /// Blind sign a message. + /// + /// The message can be for a coin or an auth token. + async fn blind_sign( + &self, + blinded_messages: Vec, + ) -> Result, Error>; + + /// Verify [`Proof`] meets conditions and is signed + async fn verify_proofs(&self, proofs: Vec) -> Result<(), Error>; + + /// Retrieve the list of all mint keysets + async fn keysets(&self) -> Result; + + /// Add current keyset to inactive keysets + /// Generate new keyset + async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result; +} diff --git a/crates/cdk-sqlite/src/mint/migrations/20250415093121_drop_keystore_foreign.sql b/crates/cdk-sqlite/src/mint/migrations/20250415093121_drop_keystore_foreign.sql new file mode 100644 index 00000000..bea8637d --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20250415093121_drop_keystore_foreign.sql @@ -0,0 +1,31 @@ +CREATE TABLE proof_new ( + y BLOB PRIMARY KEY, + amount INTEGER NOT NULL, + keyset_id TEXT NOT NULL, -- no FK constraint here + secret TEXT NOT NULL, + c BLOB NOT NULL, + witness TEXT, + state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL, + quote_id TEXT, + created_time INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO proof_new SELECT * FROM proof; +DROP TABLE proof; +ALTER TABLE proof_new RENAME TO proof; + + +CREATE TABLE blind_signature_new ( + y BLOB PRIMARY KEY, + amount INTEGER NOT NULL, + keyset_id TEXT NOT NULL, -- FK removed + c BLOB NOT NULL, + dleq_e TEXT, + dleq_s TEXT, + quote_id TEXT, + created_time INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO blind_signature_new SELECT * FROM blind_signature; +DROP TABLE blind_signature; +ALTER TABLE blind_signature_new RENAME TO blind_signature; diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 340f50ba..e7daf1a8 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -42,6 +42,11 @@ utoipa = { workspace = true, optional = true } uuid.workspace = true jsonwebtoken = { workspace = true, optional = true } +# -Z minimal-versions +sync_wrapper = "0.1.2" +bech32 = "0.9.1" +arc-swap = "1.7.1" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true, features = [ "rt-multi-thread", @@ -50,6 +55,7 @@ tokio = { workspace = true, features = [ "sync", ] } getrandom = { version = "0.2" } +cdk-signatory = { workspace = true, features = ["grpc"] } tokio-tungstenite = { workspace = true, features = [ "rustls", "rustls-tls-native-roots", @@ -58,6 +64,7 @@ tokio-tungstenite = { workspace = true, features = [ [target.'cfg(target_arch = "wasm32")'.dependencies] tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] } +cdk-signatory = { workspace = true, default-features = false } getrandom = { version = "0.2", features = ["js"] } [[example]] diff --git a/crates/cdk/src/mint/auth/mod.rs b/crates/cdk/src/mint/auth/mod.rs index 4d3f5ab8..ae969820 100644 --- a/crates/cdk/src/mint/auth/mod.rs +++ b/crates/cdk/src/mint/auth/mod.rs @@ -1,13 +1,10 @@ -use cdk_common::{CurrencyUnit, MintKeySet}; use tracing::instrument; use super::nut21::ProtectedEndpoint; use super::{ - AuthProof, AuthRequired, AuthToken, BlindAuthToken, BlindSignature, BlindedMessage, Error, Id, + AuthProof, AuthRequired, AuthToken, BlindAuthToken, BlindSignature, BlindedMessage, Error, Mint, State, }; -use crate::dhke::{sign_message, verify_message}; -use crate::Amount; impl Mint { /// Check if and what kind of auth is required for a method @@ -34,109 +31,12 @@ impl Mint { .await?) } - /// Ensure Keyset is loaded in mint - #[instrument(skip(self))] - pub async fn ensure_blind_auth_keyset_loaded(&self, id: &Id) -> Result { - { - if let Some(keyset) = self.keysets.read().await.get(id) { - return Ok(keyset.clone()); - } - } - - tracing::info!( - "Keyset {:?} not found in memory, attempting to load from storage", - id - ); - - let mut keysets = self.keysets.write().await; - - // Get auth_localstore reference - let auth_localstore = match self.auth_localstore.as_ref() { - Some(store) => store, - None => { - tracing::error!("Auth localstore is not configured"); - return Err(Error::AmountKey); - } - }; - - // Get keyset info from storage - let keyset_info = match auth_localstore.get_keyset_info(id).await { - Ok(Some(info)) => { - tracing::debug!("Found keyset info in storage for ID {:?}", id); - info - } - Ok(None) => { - tracing::error!("Keyset with ID {:?} not found in storage", id); - return Err(Error::KeysetUnknown(*id)); - } - Err(e) => { - tracing::error!("Error retrieving keyset info from storage: {:?}", e); - return Err(e.into()); - } - }; - - let id = keyset_info.id; - tracing::info!("Generating and inserting keyset {:?} into memory", id); - let keyset = self.generate_keyset(keyset_info); - - keysets.insert(id, keyset.clone()); - tracing::debug!("Keyset {:?} successfully loaded", id); - Ok(keyset) - } - /// Verify Blind auth #[instrument(skip(self, token))] pub async fn verify_blind_auth(&self, token: &BlindAuthToken) -> Result<(), Error> { - let proof = &token.auth_proof; - let keyset_id = proof.keyset_id; - - tracing::trace!( - "Starting blind auth verification for keyset ID: {:?}", - keyset_id - ); - - // Ensure the keyset is loaded - let keyset = self - .ensure_blind_auth_keyset_loaded(&keyset_id) + self.signatory + .verify_proofs(vec![token.auth_proof.clone().into()]) .await - .map_err(|err| { - tracing::error!("Failed to load keyset: {:?}", err); - err - })?; - - // Verify keyset is for auth - if keyset.unit != CurrencyUnit::Auth { - tracing::warn!( - "Blind auth attempted with non-auth keyset. Found unit: {:?}", - keyset.unit - ); - return Err(Error::BlindAuthFailed); - } - - // Get the keypair for amount 1 - let keypair = match keyset.keys.get(&Amount::from(1)) { - Some(key_pair) => key_pair, - None => { - tracing::error!("No keypair found for amount 1 in keyset {:?}", keyset_id); - return Err(Error::AmountKey); - } - }; - - // Verify the message - match verify_message(&keypair.secret_key, proof.c, proof.secret.as_bytes()) { - Ok(_) => { - tracing::trace!( - "Blind signature verification successful for keyset ID: {:?}", - keyset_id - ); - } - Err(e) => { - tracing::error!("Blind signature verification failed: {:?}", e); - return Err(e.into()); - } - } - - Ok(()) } /// Verify Auth @@ -148,84 +48,92 @@ impl Mint { auth_token: Option, endpoint: &ProtectedEndpoint, ) -> Result<(), Error> { - if let Some(auth_required) = self.is_protected(endpoint).await? { + let auth_required = if let Some(auth_required) = self.is_protected(endpoint).await? { tracing::info!( "Auth required for endpoint: {:?}, type: {:?}", endpoint, auth_required ); + auth_required + } else { + tracing::debug!("No auth required for endpoint: {:?}", endpoint); + return Ok(()); + }; - let auth_token = match auth_token { - Some(token) => token, - None => match auth_required { - AuthRequired::Clear => { - tracing::warn!( - "No auth token provided for protected endpoint: {:?}, expected clear auth.", - endpoint - ); - return Err(Error::ClearAuthRequired); - } - AuthRequired::Blind => { - tracing::warn!( - "No auth token provided for protected endpoint: {:?}, expected blind auth.", - endpoint - ); - return Err(Error::BlindAuthRequired); - } - }, - }; + tracing::info!( + "Auth required for endpoint: {:?}, type: {:?}", + endpoint, + auth_required + ); - match (auth_required, auth_token) { - (AuthRequired::Clear, AuthToken::ClearAuth(token)) => { - tracing::debug!("Verifying clear auth token"); - match self.verify_clear_auth(token.clone()).await { - Ok(_) => tracing::info!("Clear auth verification successful"), - Err(e) => { - tracing::error!("Clear auth verification failed: {:?}", e); - return Err(e); - } - } - } - (AuthRequired::Blind, AuthToken::BlindAuth(token)) => { - tracing::debug!( - "Verifying blind auth token with keyset_id: {:?}", - token.auth_proof.keyset_id - ); - - match self.verify_blind_auth(&token).await { - Ok(_) => tracing::debug!("Blind auth signature verification successful"), - Err(e) => { - tracing::error!("Blind auth verification failed: {:?}", e); - return Err(e); - } - } - - let auth_proof = token.auth_proof; - - self.check_blind_auth_proof_spendable(auth_proof) - .await - .map_err(|err| { - tracing::error!("Failed to spend blind auth proof: {:?}", err); - err - })?; - } - (AuthRequired::Blind, other) => { + let auth_token = match auth_token { + Some(token) => token, + None => match auth_required { + AuthRequired::Clear => { tracing::warn!( - "Blind auth required but received different auth type: {:?}", - other - ); - return Err(Error::BlindAuthRequired); - } - (AuthRequired::Clear, other) => { - tracing::warn!( - "Clear auth required but received different auth type: {:?}", - other + "No auth token provided for protected endpoint: {:?}, expected clear auth.", + endpoint ); return Err(Error::ClearAuthRequired); } + AuthRequired::Blind => { + tracing::warn!( + "No auth token provided for protected endpoint: {:?}, expected blind auth.", + endpoint + ); + return Err(Error::BlindAuthRequired); + } + }, + }; + + match (auth_required, auth_token) { + (AuthRequired::Clear, AuthToken::ClearAuth(token)) => { + tracing::debug!("Verifying clear auth token"); + match self.verify_clear_auth(token.clone()).await { + Ok(_) => tracing::info!("Clear auth verification successful"), + Err(e) => { + tracing::error!("Clear auth verification failed: {:?}", e); + return Err(e); + } + } + } + (AuthRequired::Blind, AuthToken::BlindAuth(token)) => { + tracing::debug!( + "Verifying blind auth token with keyset_id: {:?}", + token.auth_proof.keyset_id + ); + + match self.verify_blind_auth(&token).await { + Ok(_) => tracing::debug!("Blind auth signature verification successful"), + Err(e) => { + tracing::error!("Blind auth verification failed: {:?}", e); + return Err(e); + } + } + + let auth_proof = token.auth_proof; + + self.check_blind_auth_proof_spendable(auth_proof) + .await + .map_err(|err| { + tracing::error!("Failed to spend blind auth proof: {:?}", err); + err + })?; + } + (AuthRequired::Blind, other) => { + tracing::warn!( + "Blind auth required but received different auth type: {:?}", + other + ); + return Err(Error::BlindAuthRequired); + } + (AuthRequired::Clear, other) => { + tracing::warn!( + "Clear auth required but received different auth type: {:?}", + other + ); + return Err(Error::ClearAuthRequired); } - } else { - tracing::debug!("No auth required for endpoint: {:?}", endpoint); } tracing::debug!("Auth verification completed successfully"); @@ -306,108 +214,10 @@ impl Mint { &self, blinded_message: &BlindedMessage, ) -> Result { - let BlindedMessage { - amount, - blinded_secret, - keyset_id, - .. - } = blinded_message; - - // Ensure the keyset is loaded - let keyset = match self.ensure_blind_auth_keyset_loaded(keyset_id).await { - Ok(keyset) => keyset, - Err(e) => { - tracing::error!("Failed to load keyset: {:?}", e); - return Err(e); - } - }; - - // Get auth_localstore reference - let auth_localstore = match self.auth_localstore.as_ref() { - Some(store) => store, - None => { - tracing::error!("Auth localstore is not configured"); - return Err(Error::AuthSettingsUndefined); - } - }; - - // Get keyset info - let keyset_info = match auth_localstore.get_keyset_info(keyset_id).await { - Ok(Some(info)) => info, - Ok(None) => { - tracing::error!("Keyset with ID {:?} not found in storage", keyset_id); - return Err(Error::UnknownKeySet); - } - Err(e) => { - tracing::error!("Error retrieving keyset info from storage: {:?}", e); - return Err(e.into()); - } - }; - - // Get active keyset ID - let active = match auth_localstore.get_active_keyset_id().await { - Ok(Some(id)) => id, - Ok(None) => { - tracing::error!("No active keyset found"); - return Err(Error::InactiveKeyset); - } - Err(e) => { - tracing::error!("Error retrieving active keyset ID: {:?}", e); - return Err(e.into()); - } - }; - - // Check that the keyset is active and should be used to sign - if keyset_info.id.ne(&active) { - tracing::warn!( - "Keyset {:?} is not active. Active keyset is {:?}", - keyset_info.id, - active - ); - return Err(Error::InactiveKeyset); - } - - // Get the keypair for the specified amount - let key_pair = match keyset.keys.get(amount) { - Some(key_pair) => key_pair, - None => { - tracing::error!( - "No keypair found for amount {:?} in keyset {:?}", - amount, - keyset_id - ); - return Err(Error::AmountKey); - } - }; - - // Sign the message - let c = match sign_message(&key_pair.secret_key, blinded_secret) { - Ok(signature) => signature, - Err(e) => { - tracing::error!("Failed to sign message: {:?}", e); - return Err(e.into()); - } - }; - - // Create blinded signature - let blinded_signature = match BlindSignature::new( - *amount, - c, - keyset_info.id, - &blinded_message.blinded_secret, - key_pair.secret_key.clone(), - ) { - Ok(sig) => sig, - Err(e) => { - tracing::error!("Failed to create blinded signature: {:?}", e); - return Err(e.into()); - } - }; - - tracing::trace!( - "Blind signing completed successfully for keyset ID: {:?}", - keyset_id - ); - Ok(blinded_signature) + self.signatory + .blind_sign(vec![blinded_message.to_owned()]) + .await? + .pop() + .ok_or(Error::Internal) } } diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index 98520b87..4f85e057 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -5,12 +5,13 @@ use std::sync::Arc; use anyhow::anyhow; use bitcoin::bip32::DerivationPath; -use cdk_common::database::{self, MintDatabase}; +use cdk_common::database::{self, MintDatabase, MintKeysDatabase}; use cdk_common::error::Error; use cdk_common::nut04::MintMethodOptions; use cdk_common::nut05::MeltMethodOptions; use cdk_common::payment::Bolt11Settings; use cdk_common::{nut21, nut22}; +use cdk_signatory::signatory::Signatory; use super::nut17::SupportedMethods; use super::nut19::{self, CachedEndpoint}; @@ -34,7 +35,9 @@ pub struct MintBuilder { /// Mint Info pub mint_info: MintInfo, /// Mint Storage backend - localstore: Option + Send + Sync>>, + pub localstore: Option + Send + Sync>>, + /// Database for the Signatory + keystore: Option + Send + Sync>>, /// Mint Storage backend #[cfg(feature = "auth")] auth_localstore: Option + Send + Sync>>, @@ -47,6 +50,7 @@ pub struct MintBuilder { custom_paths: HashMap, // protected_endpoints: HashMap, openid_discovery: Option, + signatory: Option>, } impl MintBuilder { @@ -69,6 +73,18 @@ impl MintBuilder { builder } + /// Set signatory service + pub fn with_signatory(mut self, signatory: Arc) -> Self { + self.signatory = Some(signatory); + self + } + + /// Set seed + pub fn with_seed(mut self, seed: Vec) -> Self { + self.seed = Some(seed); + self + } + /// Set localstore pub fn with_localstore( mut self, @@ -78,6 +94,15 @@ impl MintBuilder { self } + /// Set keystore database + pub fn with_keystore( + mut self, + keystore: Arc + Send + Sync>, + ) -> MintBuilder { + self.keystore = Some(keystore); + self + } + /// Set auth localstore #[cfg(feature = "auth")] pub fn with_auth_localstore( @@ -88,18 +113,12 @@ impl MintBuilder { self } - /// Set Openid discovery url + /// Set Openid discovery url pub fn with_openid_discovery(mut self, openid_discovery: String) -> Self { self.openid_discovery = Some(openid_discovery); self } - /// Set seed - pub fn with_seed(mut self, seed: Vec) -> Self { - self.seed = Some(seed); - self - } - /// Set name pub fn with_name(mut self, name: String) -> Self { self.mint_info.name = Some(name); @@ -319,9 +338,25 @@ impl MintBuilder { .localstore .clone() .ok_or(anyhow!("Localstore not set"))?; - let seed = self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?; let ln = self.ln.clone().ok_or(anyhow!("Ln backends not set"))?; + let signatory = if let Some(signatory) = self.signatory.as_ref() { + signatory.clone() + } else { + let seed = self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?; + let in_memory_signatory = cdk_signatory::db_signatory::DbSignatory::new( + self.keystore.clone().ok_or(anyhow!("keystore not set"))?, + seed, + self.supported_units.clone(), + HashMap::new(), + ) + .await?; + + Arc::new(cdk_signatory::embedded::Service::new(Arc::new( + in_memory_signatory, + ))) + }; + #[cfg(feature = "auth")] if let Some(openid_discovery) = &self.openid_discovery { let auth_localstore = self @@ -330,12 +365,10 @@ impl MintBuilder { .ok_or(anyhow!("Auth localstore not set"))?; return Ok(Mint::new_with_auth( - seed, + signatory, localstore, auth_localstore, ln, - self.supported_units.clone(), - self.custom_paths.clone(), openid_discovery.clone(), ) .await?); @@ -348,14 +381,7 @@ impl MintBuilder { )); } - Ok(Mint::new( - seed, - localstore, - ln, - self.supported_units.clone(), - self.custom_paths.clone(), - ) - .await?) + Ok(Mint::new(signatory, localstore, ln).await?) } } diff --git a/crates/cdk/src/mint/issue/issue_nut04.rs b/crates/cdk/src/mint/issue/issue_nut04.rs index 08a0e789..b2808cc7 100644 --- a/crates/cdk/src/mint/issue/issue_nut04.rs +++ b/crates/cdk/src/mint/issue/issue_nut04.rs @@ -309,7 +309,7 @@ impl Mint { let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len()); for blinded_message in mint_request.outputs.iter() { - let blind_signature = self.blind_sign(blinded_message).await?; + let blind_signature = self.blind_sign(blinded_message.clone()).await?; blind_signatures.push(blind_signature); } diff --git a/crates/cdk/src/mint/keysets/auth.rs b/crates/cdk/src/mint/keysets/auth.rs index 2650362a..84850245 100644 --- a/crates/cdk/src/mint/keysets/auth.rs +++ b/crates/cdk/src/mint/keysets/auth.rs @@ -1,65 +1,50 @@ //! Auth keyset functions +use cdk_common::{CurrencyUnit, KeySetInfo}; use tracing::instrument; -use crate::mint::{CurrencyUnit, Id, KeySetInfo, KeysResponse, KeysetResponse}; +use crate::mint::{KeysResponse, KeysetResponse}; use crate::{Error, Mint}; impl Mint { /// Retrieve the auth public keys of the active keyset for distribution to wallet /// clients #[instrument(skip_all)] - pub async fn auth_pubkeys(&self) -> Result { - let active_keyset_id = self - .auth_localstore - .as_ref() - .ok_or(Error::AuthLocalstoreUndefined)? - .get_active_keyset_id() - .await? - .ok_or(Error::AmountKey)?; - - self.ensure_blind_auth_keyset_loaded(&active_keyset_id) - .await?; - - let keysets = self.keysets.read().await; + pub fn auth_pubkeys(&self) -> Result { + let key = self + .keysets + .load() + .iter() + .find(|key| key.unit == CurrencyUnit::Auth) + .ok_or(Error::NoActiveKeyset)? + .clone(); Ok(KeysResponse { - keysets: vec![keysets - .get(&active_keyset_id) - .ok_or(Error::KeysetUnknown(active_keyset_id))? - .clone() - .into()], + keysets: vec![key.into()], }) } /// Return a list of auth keysets #[instrument(skip_all)] - pub async fn auth_keysets(&self) -> Result { - let keysets = self - .auth_localstore - .clone() - .ok_or(Error::AuthLocalstoreUndefined)? - .get_keyset_infos() - .await?; - let active_keysets: Id = self - .auth_localstore - .as_ref() - .ok_or(Error::AuthLocalstoreUndefined)? - .get_active_keyset_id() - .await? - .ok_or(Error::NoActiveKeyset)?; - - let keysets = keysets - .into_iter() - .filter(|k| k.unit == CurrencyUnit::Auth) - .map(|k| KeySetInfo { - id: k.id, - unit: k.unit, - active: active_keysets == k.id, - input_fee_ppk: k.input_fee_ppk, - }) - .collect(); - - Ok(KeysetResponse { keysets }) + pub fn auth_keysets(&self) -> KeysetResponse { + KeysetResponse { + keysets: self + .keysets + .load() + .iter() + .filter_map(|key| { + if key.unit == CurrencyUnit::Auth { + Some(KeySetInfo { + id: key.id, + unit: key.unit.clone(), + active: key.active, + input_fee_ppk: key.input_fee_ppk, + }) + } else { + None + } + }) + .collect(), + } } } diff --git a/crates/cdk/src/mint/keysets/mod.rs b/crates/cdk/src/mint/keysets/mod.rs index da3d2d0a..7e6f62b3 100644 --- a/crates/cdk/src/mint/keysets/mod.rs +++ b/crates/cdk/src/mint/keysets/mod.rs @@ -1,15 +1,8 @@ -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; - -use bitcoin::bip32::{DerivationPath, Xpriv}; -use bitcoin::key::Secp256k1; -use bitcoin::secp256k1::All; -use cdk_common::database::{self, MintDatabase}; +use cdk_signatory::signatory::RotateKeyArguments; use tracing::instrument; use super::{ - create_new_keyset, derivation_path_from_unit, CurrencyUnit, Id, KeySet, KeySetInfo, - KeysResponse, KeysetResponse, Mint, MintKeySet, MintKeySetInfo, + CurrencyUnit, Id, KeySet, KeySetInfo, KeysResponse, KeysetResponse, Mint, MintKeySetInfo, }; use crate::Error; @@ -17,298 +10,85 @@ use crate::Error; mod auth; impl Mint { - /// Initialize keysets and returns a [`Result`] with a tuple of the following: - /// * a [`HashMap`] mapping each active keyset `Id` to `MintKeySet` - /// * a [`Vec`] of `CurrencyUnit` containing active keysets units - pub async fn init_keysets( - xpriv: Xpriv, - secp_ctx: &Secp256k1, - 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()], - }) + pub fn keyset_pubkeys(&self, keyset_id: &Id) -> Result { + self.keysets + .load() + .iter() + .find(|keyset| &keyset.id == keyset_id) + .ok_or(Error::UnknownKeySet) + .map(|key| KeysResponse { + keysets: vec![key.into()], + }) } /// Retrieve the public keys of the active keyset for distribution to wallet /// clients #[instrument(skip_all)] - pub async fn pubkeys(&self) -> Result { - let mut active_keysets = self.localstore.get_active_keysets().await?; - - // We don't want to return auth keys here even though in the db we treat them the same - active_keysets.remove(&CurrencyUnit::Auth); - - let active_keysets: HashSet<&Id> = active_keysets.values().collect(); - - for id in active_keysets.iter() { - self.ensure_keyset_loaded(id).await?; - } - - Ok(KeysResponse { + pub fn pubkeys(&self) -> KeysResponse { + KeysResponse { keysets: self .keysets - .read() - .await - .values() - .filter_map(|k| match active_keysets.contains(&k.id) { - true => Some(k.clone().into()), - false => None, - }) - .collect(), - }) + .load() + .iter() + .filter(|keyset| keyset.active && keyset.unit != CurrencyUnit::Auth) + .map(|key| key.into()) + .collect::>(), + } } /// 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() - .filter(|k| k.unit != CurrencyUnit::Auth) - .map(|k| KeySetInfo { - id: k.id, - unit: k.unit, - active: active_keysets.contains(&k.id), - input_fee_ppk: k.input_fee_ppk, - }) - .collect(); - - Ok(KeysetResponse { keysets }) + pub fn keysets(&self) -> KeysetResponse { + KeysetResponse { + keysets: self + .keysets + .load() + .iter() + .filter(|k| k.unit != CurrencyUnit::Auth) + .map(|k| KeySetInfo { + id: k.id, + unit: k.unit.clone(), + active: k.active, + input_fee_ppk: k.input_fee_ppk, + }) + .collect(), + } } /// Get keysets #[instrument(skip(self))] - pub async fn keyset(&self, id: &Id) -> Result, Error> { - self.ensure_keyset_loaded(id).await?; - let keysets = self.keysets.read().await; - let keyset = keysets.get(id).map(|k| k.clone().into()); - Ok(keyset) + pub fn keyset(&self, id: &Id) -> Option { + self.keysets + .load() + .iter() + .find(|key| &key.id == id) + .map(|x| x.into()) } /// Add current keyset to inactive keysets /// Generate new keyset - #[instrument(skip(self, custom_paths))] + #[instrument(skip(self))] pub async fn rotate_keyset( - &self, - unit: CurrencyUnit, - derivation_path_index: u32, - max_order: u8, - input_fee_ppk: u64, - custom_paths: &HashMap, - ) -> 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) - } - - /// Rotate to next keyset for unit - #[instrument(skip(self))] - pub async fn rotate_next_keyset( &self, unit: CurrencyUnit, 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)?; - - tracing::debug!( - "Current active keyset {} path index {:?}", - keyset_info.id, - keyset_info.derivation_path_index - ); - - let keyset_info = self - .rotate_keyset( + let result = self + .signatory + .rotate_keyset(RotateKeyArguments { unit, - keyset_info.derivation_path_index.unwrap_or(1) + 1, max_order, input_fee_ppk, - &self.custom_paths, - ) + }) .await?; - Ok(keyset_info) - } + let new_keyset = self.signatory.keysets().await?; + self.keysets.store(new_keyset.keysets.into()); - /// 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, - ) + Ok(result.into()) } } diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index 8f454ce1..7293e9fb 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -701,7 +701,7 @@ impl Mint { for (amount, blinded_message) in amounts.iter().zip(&mut outputs) { blinded_message.amount = *amount; - let blinded_signature = self.blind_sign(blinded_message).await?; + let blinded_signature = self.blind_sign(blinded_message.clone()).await?; change_sigs.push(blinded_signature) } diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index c554bf23..5b7c8580 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -3,30 +3,33 @@ use std::collections::HashMap; use std::sync::Arc; -use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; -use bitcoin::secp256k1::{self, Secp256k1}; +use arc_swap::ArcSwap; +use bitcoin::bip32::{DerivationPath, Xpriv}; +use bitcoin::secp256k1; use cdk_common::common::{PaymentProcessorKey, QuoteTTL}; #[cfg(feature = "auth")] use cdk_common::database::MintAuthDatabase; use cdk_common::database::{self, MintDatabase}; +use cdk_common::nuts::{self, BlindSignature, BlindedMessage, CurrencyUnit, Id, Kind, MintKeySet}; +use cdk_common::secret; +use cdk_signatory::signatory::{Signatory, SignatoryKeySet}; use futures::StreamExt; #[cfg(feature = "auth")] use nut21::ProtectedEndpoint; 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_payment::{self, MintPayment}; -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; #[cfg(feature = "auth")] use crate::OidcClient; -use crate::{ensure_cdk, Amount}; #[cfg(feature = "auth")] pub(crate) mod auth; @@ -48,6 +51,11 @@ pub use verification::Verification; /// Cashu Mint #[derive(Clone)] pub struct Mint { + /// Signatory backend. + /// + /// It is implemented in the cdk-signatory crate, and it can be embedded in the mint or it can + /// be a gRPC client to a remote signatory server. + pub signatory: Arc, /// Mint Storage backend pub localstore: Arc + Send + Sync>, /// Auth Storage backend (only available with auth feature) @@ -60,10 +68,8 @@ pub struct Mint { pub pubsub_manager: Arc, #[cfg(feature = "auth")] oidc_client: Option, - secp_ctx: Secp256k1, - xpriv: Xpriv, - keysets: Arc>>, - custom_paths: HashMap, + /// In-memory keyset + keysets: Arc>>, } impl Mint { @@ -86,23 +92,19 @@ impl Mint { /// Create new [`Mint`] without authentication pub async fn new( - seed: &[u8], + signatory: Arc, localstore: Arc + Send + Sync>, ln: HashMap< PaymentProcessorKey, Arc + Send + Sync>, >, - supported_units: HashMap, - custom_paths: HashMap, ) -> Result { Self::new_internal( - seed, + signatory, localstore, #[cfg(feature = "auth")] None, ln, - supported_units, - custom_paths, #[cfg(feature = "auth")] None, ) @@ -112,32 +114,29 @@ impl Mint { /// Create new [`Mint`] with authentication support #[cfg(feature = "auth")] pub async fn new_with_auth( - seed: &[u8], + signatory: Arc, localstore: Arc + Send + Sync>, auth_localstore: Arc + Send + Sync>, ln: HashMap< PaymentProcessorKey, Arc + Send + Sync>, >, - supported_units: HashMap, - custom_paths: HashMap, open_id_discovery: String, ) -> Result { Self::new_internal( - seed, + signatory, localstore, Some(auth_localstore), ln, - supported_units, - custom_paths, Some(open_id_discovery), ) .await } /// Internal function to create a new [`Mint`] with shared logic + #[inline] async fn new_internal( - seed: &[u8], + signatory: Arc, localstore: Arc + Send + Sync>, #[cfg(feature = "auth")] auth_localstore: Option< Arc + Send + Sync>, @@ -146,106 +145,41 @@ impl Mint { PaymentProcessorKey, Arc + Send + Sync>, >, - supported_units: HashMap, - custom_paths: HashMap, #[cfg(feature = "auth")] open_id_discovery: Option, ) -> Result { - let secp_ctx = Secp256k1::new(); - let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); + #[cfg(feature = "auth")] + let oidc_client = + open_id_discovery.map(|openid_discovery| OidcClient::new(openid_discovery.clone())); - 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 = signatory.keysets().await?; + if !keysets + .keysets + .iter() + .any(|keyset| keyset.active && keyset.unit != CurrencyUnit::Auth) + { + return Err(Error::NoActiveKeyset); } - #[cfg(feature = "auth")] - let oidc_client = if let Some(openid_discovery) = open_id_discovery { - { - tracing::info!("Auth enabled creating auth keysets"); - let auth_localstore = auth_localstore - .as_ref() - .ok_or(Error::AuthSettingsUndefined)?; - - let derivation_path = match custom_paths.get(&CurrencyUnit::Auth) { - Some(path) => path.clone(), - None => derivation_path_from_unit(CurrencyUnit::Auth, 0) - .ok_or(Error::UnsupportedUnit)?, - }; - - let (keyset, keyset_info) = create_new_keyset( - &secp_ctx, - xpriv, - derivation_path, - Some(0), - CurrencyUnit::Auth, - 1, - 0, - ); - - let id = keyset_info.id; - auth_localstore.add_keyset_info(keyset_info).await?; - auth_localstore.set_active_keyset(id).await?; - active_keysets.insert(id, keyset); - - Some(OidcClient::new(openid_discovery.clone())) - } - - #[cfg(not(feature = "auth"))] - { - tracing::error!("CDK must be compiled with auth feature to be used with auth."); - return Err(Error::Custom( - "Openid passed but cdk compiled without auth.".to_string(), - )); - } - } else { - None - }; - - let keysets = Arc::new(RwLock::new(active_keysets)); + tracing::info!( + "Using Signatory {} with {} active keys", + signatory.name(), + keysets + .keysets + .iter() + .filter(|keyset| keyset.active && keyset.unit != CurrencyUnit::Auth) + .count() + ); Ok(Self { + signatory, pubsub_manager: Arc::new(localstore.clone().into()), - secp_ctx, - xpriv, localstore, #[cfg(feature = "auth")] oidc_client, ln, - custom_paths, #[cfg(feature = "auth")] auth_localstore, - keysets, + keysets: Arc::new(ArcSwap::new(keysets.keysets.into())), }) } @@ -378,9 +312,7 @@ impl Mint { fee_per_keyset.entry(proof.keyset_id) { let mint_keyset_info = self - .localstore .get_keyset_info(&proof.keyset_id) - .await? .ok_or(Error::UnknownKeySet)?; e.insert(mint_keyset_info.input_fee_ppk); } @@ -396,82 +328,79 @@ impl Mint { Ok(fee) } + /// Get active keysets + pub fn get_active_keysets(&self) -> HashMap { + self.keysets + .load() + .iter() + .filter_map(|keyset| { + if keyset.active { + Some((keyset.unit.clone(), keyset.id)) + } else { + None + } + }) + .collect() + } + + /// Get keyset info + pub fn get_keyset_info(&self, id: &Id) -> Option { + self.keysets + .load() + .iter() + .filter_map(|keyset| { + if keyset.id == *id { + Some(keyset.into()) + } else { + None + } + }) + .next() + } + /// Blind Sign - #[instrument(skip_all)] + #[tracing::instrument(skip_all)] pub async fn blind_sign( &self, - blinded_message: &BlindedMessage, + 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) + self.signatory + .blind_sign(vec![blinded_message]) .await? - .ok_or(Error::UnknownKeySet)?; - - // Check that the keyset is active and should be used to sign - let active_id = self - .localstore - .get_active_keyset_id(&keyset_info.unit) - .await? - .ok_or(Error::InactiveKeyset)?; - ensure_cdk!(keyset_info.id.eq(&active_id), Error::InactiveKeyset); - - let keysets = self.keysets.read().await; - let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?; - let key_pair = keyset.keys.get(amount).ok_or(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) + .pop() + .ok_or(Error::Internal) } /// 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()?; + #[tracing::instrument(skip_all)] + pub async fn verify_proofs(&self, proofs: Proofs) -> Result<(), Error> { + proofs + .iter() + .map(|proof| { + // Check if secret is a nut10 secret with conditions + if let Ok(secret) = + <&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()?; + } + } } - Kind::HTLC => { - proof.verify_htlc()?; - } - } - } + Ok(()) + }) + .collect::, Error>>()?; - 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 = keyset.keys.get(&proof.amount).ok_or(Error::AmountKey)?; - - verify_message(&keypair.secret_key, proof.c, proof.secret.as_bytes())?; - - Ok(()) + self.signatory.verify_proofs(proofs).await } /// Verify melt request is valid @@ -565,7 +494,7 @@ impl Mint { /// Get the total amount issed by keyset #[instrument(skip_all)] pub async fn total_issued(&self) -> Result, Error> { - let keysets = self.localstore.get_keyset_infos().await?; + let keysets = self.keysets().keysets; let mut total_issued = HashMap::new(); @@ -586,7 +515,7 @@ impl Mint { /// Total redeemed for keyset #[instrument(skip_all)] pub async fn total_redeemed(&self) -> Result, Error> { - let keysets = self.localstore.get_keyset_infos().await?; + let keysets = self.keysets().keysets; let mut total_redeemed = HashMap::new(); @@ -641,115 +570,16 @@ fn create_new_keyset( (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::PaymentProcessorKey; use cdk_sqlite::mint::memory::new_with_state; - use secp256k1::Secp256k1; 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, @@ -780,77 +610,73 @@ mod tests { .unwrap(), ); - Mint::new( - config.seed, - localstore, - HashMap::new(), - config.supported_units, - HashMap::new(), - ) - .await - .unwrap() + let signatory = Arc::new( + cdk_signatory::db_signatory::DbSignatory::new( + localstore.clone(), + config.seed, + config.supported_units, + HashMap::new(), + ) + .await + .expect("Failed to create signatory"), + ); + + Mint::new(signatory, localstore, HashMap::new()) + .await + .unwrap() } #[tokio::test] async fn mint_mod_new_mint() { + let mut supported_units = HashMap::new(); + supported_units.insert(CurrencyUnit::default(), (0, 32)); let config = MintConfig::<'_> { + supported_units, ..Default::default() }; let mint = create_mint(config).await; assert_eq!( - mint.pubkeys().await.unwrap(), - KeysResponse { - keysets: Vec::new() - } + mint.total_issued() + .await + .unwrap() + .into_values() + .collect::>(), + vec![Amount::default()] ); assert_eq!( - mint.keysets().await.unwrap(), - KeysetResponse { - keysets: Vec::new() - } - ); - - assert_eq!( - mint.total_issued().await.unwrap(), - HashMap::::new() - ); - - assert_eq!( - mint.total_redeemed().await.unwrap(), - HashMap::::new() + mint.total_issued() + .await + .unwrap() + .into_values() + .collect::>(), + vec![Amount::default()] ); } #[tokio::test] async fn mint_mod_rotate_keyset() { + let mut supported_units = HashMap::new(); + supported_units.insert(CurrencyUnit::default(), (0, 32)); + let config = MintConfig::<'_> { + supported_units, ..Default::default() }; let mint = create_mint(config).await; - let keysets = mint.keysets().await.unwrap(); - assert!(keysets.keysets.is_empty()); - - // generate the first keyset and set it to active - mint.rotate_keyset(CurrencyUnit::default(), 0, 1, 1, &HashMap::new()) - .await - .unwrap(); - - let keysets = mint.keysets().await.unwrap(); - assert!(keysets.keysets.len().eq(&1)); - assert!(keysets.keysets[0].active); + let keysets = mint.keysets(); let first_keyset_id = keysets.keysets[0].id; // set the first keyset to inactive and generate a new keyset - mint.rotate_keyset(CurrencyUnit::default(), 1, 1, 1, &HashMap::new()) + mint.rotate_keyset(CurrencyUnit::default(), 1, 1) .await - .unwrap(); + .expect("test"); - let keysets = mint.keysets().await.unwrap(); + let keysets = mint.keysets(); - assert!(keysets.keysets.len().eq(&2)); + assert_eq!(2, keysets.keysets.len()); for keyset in &keysets.keysets { if keyset.id == first_keyset_id { assert!(!keyset.active); @@ -866,20 +692,19 @@ mod tests { "dismiss price public alone audit gallery ignore process swap dance crane furnace", ) .unwrap(); + let mut supported_units = HashMap::new(); + supported_units.insert(CurrencyUnit::default(), (0, 32)); let config = MintConfig::<'_> { seed: &seed.to_seed_normalized(""), + supported_units, ..Default::default() }; let mint = create_mint(config).await; - mint.rotate_keyset(CurrencyUnit::default(), 0, 32, 1, &HashMap::new()) - .await - .unwrap(); + let keys = mint.pubkeys(); - let keys = mint.keysets.read().await.clone(); - - 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","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()); } diff --git a/crates/cdk/src/mint/swap.rs b/crates/cdk/src/mint/swap.rs index 89488394..2fef698f 100644 --- a/crates/cdk/src/mint/swap.rs +++ b/crates/cdk/src/mint/swap.rs @@ -3,7 +3,7 @@ use tracing::instrument; use super::nut11::{enforce_sig_flag, EnforceSigFlag}; use super::{Mint, PublicKey, SigFlag, State, SwapRequest, SwapResponse}; use crate::nuts::nut00::ProofsMethods; -use crate::Error; +use crate::{cdk_database, Error}; impl Mint { /// Process Swap @@ -32,13 +32,23 @@ impl Mint { let mut promises = Vec::with_capacity(swap_request.outputs().len()); for blinded_message in swap_request.outputs() { - let blinded_signature = self.blind_sign(blinded_message).await?; + let blinded_signature = self.blind_sign(blinded_message.clone()).await?; promises.push(blinded_signature); } + // TODO: It may be possible to have a race condition, that's why an error when changing the + // state can be converted to a TokenAlreadySpent error. + // + // A concept of transaction/writer for the Database trait would eliminate this problem and + // will remove all the "reset" codebase, resulting in fewer lines of code, and less + // error-prone database updates self.localstore .update_proofs_states(&input_ys, State::Spent) - .await?; + .await + .map_err(|e| match e { + cdk_database::Error::AttemptUpdateSpentProof => Error::TokenAlreadySpent, + e => e.into(), + })?; for pub_key in input_ys { self.pubsub_manager.proof_state((pub_key, State::Spent)); diff --git a/crates/cdk/src/mint/verification.rs b/crates/cdk/src/mint/verification.rs index 7d598aa3..c0849dd7 100644 --- a/crates/cdk/src/mint/verification.rs +++ b/crates/cdk/src/mint/verification.rs @@ -66,7 +66,7 @@ impl Mint { let output_keyset_ids: HashSet = outputs.iter().map(|p| p.keyset_id).collect(); for id in &output_keyset_ids { - match self.localstore.get_keyset_info(id).await? { + match self.get_keyset_info(id) { Some(keyset) => { if !keyset.active { tracing::debug!( @@ -114,7 +114,7 @@ impl Mint { let inputs_keyset_ids: HashSet = inputs.iter().map(|p| p.keyset_id).collect(); for id in &inputs_keyset_ids { - match self.localstore.get_keyset_info(id).await? { + match self.get_keyset_info(id) { Some(keyset) => { keyset_units.insert(keyset.unit); } @@ -203,9 +203,7 @@ impl Mint { let unit = self.verify_inputs_keyset(inputs).await?; let amount = inputs.total_amount()?; - for proof in inputs { - self.verify_proof(proof).await?; - } + self.verify_proofs(inputs.clone()).await?; Ok(Verification { amount, diff --git a/justfile b/justfile index f84151e1..0182bf37 100644 --- a/justfile +++ b/justfile @@ -57,12 +57,13 @@ test-pure db="memory": build fi # Run pure integration tests - CDK_TEST_DB_TYPE={{db}} cargo test -p cdk-integration-tests --test integration_tests_pure + CDK_TEST_DB_TYPE={{db}} cargo test -p cdk-integration-tests --test integration_tests_pure -- --test-threads 1 test-all db="memory": #!/usr/bin/env bash just test {{db}} ./misc/itests.sh "{{db}}" + ./misc/fake_itests.sh "{{db}}" external_signatory ./misc/fake_itests.sh "{{db}}" test-nutshell: @@ -119,6 +120,7 @@ itest db: fake-mint-itest db: #!/usr/bin/env bash + ./misc/fake_itests.sh "{{db}}" external_signatory ./misc/fake_itests.sh "{{db}}" diff --git a/misc/fake_itests.sh b/misc/fake_itests.sh index 037a2abb..4dcb6ab7 100755 --- a/misc/fake_itests.sh +++ b/misc/fake_itests.sh @@ -7,13 +7,15 @@ cleanup() { echo "Killing the cdk mintd" kill -2 $CDK_MINTD_PID wait $CDK_MINTD_PID + kill -9 $CDK_SIGNATORY_PID + wait $CDK_SIGNATORY_PID echo "Mint binary terminated" - + # Remove the temporary directory rm -rf "$CDK_ITESTS_DIR" echo "Temp directory removed: $CDK_ITESTS_DIR" - + # Unset all environment variables unset CDK_ITESTS_DIR unset CDK_ITESTS_MINT_ADDR @@ -49,7 +51,7 @@ fi echo "Temp directory created: $CDK_ITESTS_DIR" export CDK_MINTD_DATABASE="$1" -cargo build -p cdk-integration-tests +cargo build -p cdk-integration-tests export CDK_MINTD_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT" @@ -62,6 +64,14 @@ export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt lugg export CDK_MINTD_FAKE_WALLET_FEE_PERCENT="0" export CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN="1" +if [ "$2" = "external_signatory" ]; then + export CDK_MINTD_SIGNATORY_URL="https://127.0.0.1:15060" + export CDK_MINTD_SIGNATORY_CERTS="$CDK_ITESTS_DIR" + bash -x `dirname $0`/../crates/cdk-signatory/generate_certs.sh $CDK_ITESTS_DIR + cargo run --bin signatory -- -w $CDK_ITESTS_DIR -u "sat" -u "usd" & + export CDK_SIGNATORY_PID=$! + sleep 5 +fi echo "Starting fake mintd" cargo run --bin cdk-mintd --features "redb" & @@ -74,7 +84,7 @@ START_TIME=$(date +%s) while true; do # Get the current time CURRENT_TIME=$(date +%s) - + # Calculate the elapsed time ELAPSED_TIME=$((CURRENT_TIME - START_TIME))