Introduce a SignatoryManager service. (#509)

* WIP: Introduce a SignatoryManager service.

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

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

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

The Signatory manager is concurrent and deferred any mechanism needed to handle
concurrency to the Signatory trait.

* Fixed missing default feature for signatory

* Do not read keys from the DB

* Removed KeysDatabase Trait from MintDatabase

All Keys operations should be done through the signatory

* Make sure signatory has all the keys in memory

Drop also foreign constraints on sqlite

* Fix race condition

* Adding debug info to failing test

* Add `sleep` in test

* Fixed issue with active auth keyset

* Fixed dependency

* Move all keys and keysets to an ArcSwap.

Since the keys and keysets exist in RAM, most wrapping functions are infallible
and synchronous, improving performance and adding breaking API changes.

The signatory will provide this information on the boot and update when the
`rotate_keyset` is executed.

Todo: Implement a subscription key to reload the keys when the GRPC server
changes the keys. For the embedded mode, that makes no sense since there is a
single way to rotate keys, and that bit is already covered.

* Implementing https://github.com/cashubtc/nuts/pull/250

* Add CLI for cdk-signatory to spawn an external signatory

Add to the pipeline the external signatory

* Update tests

* Apply suggestions from code review

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
Co-authored-by: thesimplekid <tsk@thesimplekid.com>

* Minor change

* Update proto buf to use the newest format

* Rename binary

* Add instrumentations

* Add more comments

* Use a single database for the signatory

Store all keys, even auth keys, in a single database. Leave the MintAuthDatabse
trait implementation for the CDK but not the signagtory

This commit also moves the cli mod to its own file

* Update dep

* Add `test_mint_keyset_gen` test

---------

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
C
2025-05-28 12:43:30 -03:00
committed by GitHub
parent d1e5b378cd
commit ade48cd8a9
47 changed files with 2745 additions and 1131 deletions

View File

@@ -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<D, A>(
pub async fn start_fake_mint_with_auth<D, A, K>(
_addr: &str,
_port: u16,
openid_discovery: String,
database: D,
auth_database: A,
key_store: K,
) -> Result<()>
where
D: MintDatabase<cdk_database::Error> + Send + Sync + 'static,
A: MintAuthDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
K: MintKeysDatabase<Err = cdk_database::Error> + 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(

View File

@@ -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<Vec<KeySet>, Error> {
self.mint.pubkeys().await.map(|pks| pks.keysets)
Ok(self.mint.pubkeys().keysets)
}
async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error> {
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<KeysetResponse, Error> {
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<Mint> {
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<dyn MintDatabase<cdk_database::Error> + 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<Mint> {
.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?;

View File

@@ -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)
}

View File

@@ -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);
}