Files
cdk/crates/cdk-integration-tests/tests/mint.rs
Cesar Rodas 760564cee0 Introduce subscription support in the Wallet crate.
The main goal is to add a subscription to CDK Mint updates into the wallet.
This feature will be particularly useful for improving the code whenever loops
hit the mint server to check status changes.

The goal is to add an easy-to-use interface that will hide the fact that we're
connecting to WebSocket and subscribing to events. This will also hide the fact
that the CDK-mint server may not support WebSocket updates.

To be fully backward compatible, the HttpClientMethods traits have a new
method, `subscribe,` which will return an object that implements
`ActiveSubscription.`

In the primary implementation, there is a `SubscriptionClient` that will
attempt to connect through WebSocket and will fall to the HTTP-status pull and
sleep approach (the current approach), but upper stream code will receive
updates as if they come from a stream of updates through WebSocket. This
`SubscriptionClient` struct will also manage reconnections to WebSockets (with
automatic resubscriptions) and all the low-level stuff, providing an
easy-to-use interface and leaving the upper-level code with a nice interface
that is hard to misuse. When `ActiveSubscription` is dropped, it will
automatically unsubscribe.

Fixed bug with Default as described in https://github.com/cashubtc/cdk/pull/473#discussion_r1871032297
2024-12-08 16:25:00 +00:00

435 lines
13 KiB
Rust

//! Mint tests
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{bail, Result};
use bip39::Mnemonic;
use cdk::amount::{Amount, SplitTarget};
use cdk::cdk_database::mint_memory::MintMemoryDatabase;
use cdk::dhke::construct_proofs;
use cdk::mint::MintQuote;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::nut17::Params;
use cdk::nuts::{
CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PreMintSecrets,
ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest,
};
use cdk::types::QuoteTTL;
use cdk::util::unix_time;
use cdk::Mint;
use tokio::sync::OnceCell;
use tokio::time::sleep;
pub const MINT_URL: &str = "http://127.0.0.1:8088";
static INSTANCE: OnceCell<Mint> = OnceCell::const_new();
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 mnemonic = Mnemonic::generate(12).unwrap();
let quote_ttl = QuoteTTL::new(10000, 10000);
Mint::new(
MINT_URL,
&mnemonic.to_seed_normalized(""),
mint_info,
quote_ttl,
Arc::new(MintMemoryDatabase::default()),
HashMap::new(),
supported_units,
HashMap::new(),
)
.await
.unwrap()
}
async fn initialize() -> &'static Mint {
INSTANCE.get_or_init(|| 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(
mint.mint_url.clone(),
"".to_string(),
CurrencyUnit::Sat,
amount,
unix_time() + 36000,
request_lookup.to_string(),
);
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(),
};
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(_)) => (),
_ => {
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(Params {
kind: cdk::nuts::nut17::Kind::ProofState,
filters: public_keys_to_listen.clone(),
id: "test".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(())
}