Files
cdk/crates/cdk-integration-tests/tests/mint.rs
C f7d9a1b5db Drop the in-memory database (#613)
* Drop the in-memory database

Fixes #607

This PR drops the implementation of in-memory database traits.

They are useful for testing purposes since the tests should test our codebase
and assume the database works as expected (although a follow-up PR should write
a sanity test suite for all database trait implementors).

As complexity is worth with database requirements to simplify complexity and
add more robustness, for instance, with the following plans to add support for
transactions or buffered writes, it would become more complex and
time-consuming to support a correct database trait. This PR drops the
implementation and replaces it with a SQLite memory instance

* Remove OnceCell<Mint>

Without this change, a single Mint is shared for all tests, and the first tests
to run and shutdown makes the other databases (not-reachable, as dropping the
tokio engine would also drop the database instance).

There is no real reason, other than perhaps performance. The mint should
perhaps run in their own tokio engine and share channels as API interfaces, or
a new instance should be created in each tests

* Fixed bug with foreign keys

[1] https://gist.github.com/crodas/bad00997c63bd5ac58db3c5bd90747ed

* Show more debug on failure

* Remove old code

* Remove old references to WalletMemoryDatabase
2025-03-04 19:44:34 +00:00

512 lines
16 KiB
Rust

//! Mint tests
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{bail, Result};
use bip39::Mnemonic;
use cdk::amount::{Amount, SplitTarget};
use cdk::cdk_database::MintDatabase;
use cdk::dhke::construct_proofs;
use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits, MintQuote};
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{
CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod,
PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest,
};
use cdk::subscription::{IndexableParams, Params};
use cdk::types::QuoteTTL;
use cdk::util::unix_time;
use cdk::Mint;
use cdk_fake_wallet::FakeWallet;
use cdk_sqlite::mint::memory;
use tokio::time::sleep;
pub const MINT_URL: &str = "http://127.0.0.1:8088";
async fn new_mint(fee: u64) -> Mint {
let mut supported_units = HashMap::new();
supported_units.insert(CurrencyUnit::Sat, (fee, 32));
let nuts = Nuts::new()
.nut07(true)
.nut08(true)
.nut09(true)
.nut10(true)
.nut11(true)
.nut12(true)
.nut14(true);
let mint_info = MintInfo::new().nuts(nuts);
let localstore = memory::empty().await.expect("valid db instance");
localstore
.set_mint_info(mint_info)
.await
.expect("Could not set mint info");
let mnemonic = Mnemonic::generate(12).unwrap();
Mint::new(
&mnemonic.to_seed_normalized(""),
Arc::new(localstore),
HashMap::new(),
supported_units,
HashMap::new(),
)
.await
.unwrap()
}
async fn initialize() -> Mint {
new_mint(0).await
}
async fn mint_proofs(
mint: &Mint,
amount: Amount,
split_target: &SplitTarget,
keys: cdk::nuts::Keys,
) -> Result<Proofs> {
let request_lookup = uuid::Uuid::new_v4().to_string();
let quote = MintQuote::new(
"".to_string(),
CurrencyUnit::Sat,
amount,
unix_time() + 36000,
request_lookup.to_string(),
None,
);
mint.localstore.add_mint_quote(quote.clone()).await?;
mint.pay_mint_quote_for_request_id(&request_lookup).await?;
let keyset_id = Id::from(&keys);
let premint = PreMintSecrets::random(keyset_id, amount, split_target)?;
let mint_request = MintBolt11Request {
quote: quote.id,
outputs: premint.blinded_messages(),
signature: None,
};
let after_mint = mint.process_mint_request(mint_request).await?;
let proofs = construct_proofs(
after_mint.signatures,
premint.rs(),
premint.secrets(),
&keys,
)?;
Ok(proofs)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_mint_double_spend() -> Result<()> {
let mint = initialize().await;
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
let keyset_id = Id::from(&keys);
let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?;
let preswap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
let swap = mint.process_swap_request(swap_request).await;
assert!(swap.is_ok());
let preswap_two = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?;
let swap_two_request = SwapRequest::new(proofs, preswap_two.blinded_messages());
match mint.process_swap_request(swap_two_request).await {
Ok(_) => bail!("Proofs double spent"),
Err(err) => match err {
cdk::Error::TokenAlreadySpent => (),
_ => bail!("Wrong error returned"),
},
}
Ok(())
}
/// This attempts to swap for more outputs then inputs.
/// This will work if the mint does not check for outputs amounts overflowing
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_attempt_to_swap_by_overflowing() -> Result<()> {
let mint = initialize().await;
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
let keyset_id = Id::from(&keys);
let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?;
let amount = 2_u64.pow(63);
let pre_mint_amount =
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default())?;
let pre_mint_amount_two =
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default())?;
let mut pre_mint = PreMintSecrets::random(keyset_id, 1.into(), &SplitTarget::default())?;
pre_mint.combine(pre_mint_amount);
pre_mint.combine(pre_mint_amount_two);
let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
match mint.process_swap_request(swap_request).await {
Ok(_) => bail!("Swap occurred with overflow"),
Err(err) => match err {
cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)) => (),
cdk::Error::AmountOverflow => (),
cdk::Error::AmountError(_) => (),
_ => {
println!("{:?}", err);
bail!("Wrong error returned in swap overflow")
}
},
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
pub async fn test_p2pk_swap() -> Result<()> {
let mint = initialize().await;
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
let keyset_id = Id::from(&keys);
let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?;
let secret = SecretKey::generate();
let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
let pre_swap = PreMintSecrets::with_conditions(
keyset_id,
100.into(),
&SplitTarget::default(),
&spending_conditions,
)?;
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
let keys = mint.pubkeys().await?.keysets.first().cloned().unwrap().keys;
let post_swap = mint.process_swap_request(swap_request).await?;
let mut proofs = construct_proofs(
post_swap.signatures,
pre_swap.rs(),
pre_swap.secrets(),
&keys,
)?;
let pre_swap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
let public_keys_to_listen: Vec<_> = swap_request
.inputs
.ys()
.expect("key")
.into_iter()
.enumerate()
.filter_map(|(key, pk)| {
if key % 2 == 0 {
// Only expect messages from every other key
Some(pk.to_string())
} else {
None
}
})
.collect();
let mut listener = mint
.pubsub_manager
.try_subscribe::<IndexableParams>(
Params {
kind: cdk::nuts::nut17::Kind::ProofState,
filters: public_keys_to_listen.clone(),
id: "test".into(),
}
.into(),
)
.await
.expect("valid subscription");
match mint.process_swap_request(swap_request).await {
Ok(_) => bail!("Proofs spent without sig"),
Err(err) => match err {
cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided) => (),
_ => {
println!("{:?}", err);
bail!("Wrong error returned")
}
},
}
for proof in &mut proofs {
proof.sign_p2pk(secret.clone())?;
}
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
let attempt_swap = mint.process_swap_request(swap_request).await;
assert!(attempt_swap.is_ok());
sleep(Duration::from_millis(10)).await;
let mut msgs = HashMap::new();
while let Ok((sub_id, msg)) = listener.try_recv() {
assert_eq!(sub_id, "test".into());
match msg {
NotificationPayload::ProofState(ProofState { y, state, .. }) => {
let pk = y.to_string();
msgs.get_mut(&pk)
.map(|x: &mut Vec<State>| {
x.push(state);
})
.unwrap_or_else(|| {
msgs.insert(pk, vec![state]);
});
}
_ => bail!("Wrong message received"),
}
}
for keys in public_keys_to_listen {
let statuses = msgs.remove(&keys).expect("some events");
assert_eq!(statuses, vec![State::Pending, State::Pending, State::Spent]);
}
assert!(listener.try_recv().is_err(), "no other event is happening");
assert!(msgs.is_empty(), "Only expected key events are received");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_swap_unbalanced() -> Result<()> {
let mint = initialize().await;
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
let keyset_id = Id::from(&keys);
let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?;
let preswap = PreMintSecrets::random(keyset_id, 95.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
match mint.process_swap_request(swap_request).await {
Ok(_) => bail!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => bail!("Wrong error returned"),
},
}
let preswap = PreMintSecrets::random(keyset_id, 101.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
match mint.process_swap_request(swap_request).await {
Ok(_) => bail!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => bail!("Wrong error returned"),
},
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_swap_overpay_underpay_fee() -> Result<()> {
let mint = new_mint(1).await;
mint.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new())
.await?;
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
let keyset_id = Id::from(&keys);
let proofs = mint_proofs(&mint, 1000.into(), &SplitTarget::default(), keys).await?;
let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
// Attempt to swap overpaying fee
match mint.process_swap_request(swap_request).await {
Ok(_) => bail!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => {
println!("{:?}", err);
bail!("Wrong error returned")
}
},
}
let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
// Attempt to swap underpaying fee
match mint.process_swap_request(swap_request).await {
Ok(_) => bail!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => {
println!("{:?}", err);
bail!("Wrong error returned")
}
},
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_mint_enforce_fee() -> Result<()> {
let mint = new_mint(1).await;
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
let keyset_id = Id::from(&keys);
let mut proofs = mint_proofs(&mint, 1010.into(), &SplitTarget::Value(1.into()), keys).await?;
let five_proofs: Vec<_> = proofs.drain(..5).collect();
let preswap = PreMintSecrets::random(keyset_id, 5.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
// Attempt to swap underpaying fee
match mint.process_swap_request(swap_request).await {
Ok(_) => bail!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => {
println!("{:?}", err);
bail!("Wrong error returned")
}
},
}
let preswap = PreMintSecrets::random(keyset_id, 4.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
let _ = mint.process_swap_request(swap_request).await?;
let thousnad_proofs: Vec<_> = proofs.drain(..1001).collect();
let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
// Attempt to swap underpaying fee
match mint.process_swap_request(swap_request).await {
Ok(_) => bail!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => {
println!("{:?}", err);
bail!("Wrong error returned")
}
},
}
let preswap = PreMintSecrets::random(keyset_id, 999.into(), &SplitTarget::default())?;
let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
let _ = mint.process_swap_request(swap_request).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_correct_keyset() -> Result<()> {
let mnemonic = Mnemonic::generate(12)?;
let fee_reserve = FeeReserve {
min_fee_reserve: 1.into(),
percent_fee_reserve: 1.0,
};
let database = memory::empty().await.expect("valid db instance");
let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0);
let mut mint_builder = MintBuilder::new();
let localstore = Arc::new(database);
mint_builder = mint_builder.with_localstore(localstore.clone());
mint_builder = mint_builder.add_ln_backend(
CurrencyUnit::Sat,
PaymentMethod::Bolt11,
MintMeltLimits::new(1, 5_000),
Arc::new(fake_wallet),
);
mint_builder = mint_builder
.with_name("regtest mint".to_string())
.with_description("regtest mint".to_string())
.with_seed(mnemonic.to_seed_normalized("").to_vec());
let mint = mint_builder.build().await?;
localstore
.set_mint_info(mint_builder.mint_info.clone())
.await?;
let quote_ttl = QuoteTTL::new(10000, 10000);
localstore.set_quote_ttl(quote_ttl).await?;
mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0).await?;
mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0).await?;
let active = mint.localstore.get_active_keysets().await?;
let active = active
.get(&CurrencyUnit::Sat)
.expect("There is a keyset for unit");
let keyset_info = mint
.localstore
.get_keyset_info(active)
.await?
.expect("There is keyset");
assert!(keyset_info.derivation_path_index == Some(2));
let mint = mint_builder.build().await?;
let active = mint.localstore.get_active_keysets().await?;
let active = active
.get(&CurrencyUnit::Sat)
.expect("There is a keyset for unit");
let keyset_info = mint
.localstore
.get_keyset_info(active)
.await?
.expect("There is keyset");
assert!(keyset_info.derivation_path_index == Some(2));
Ok(())
}