mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-21 14:44:46 +01:00
* refactor: consolidate validation checks * refactor: melt verification checks * refactor: mint verification * chore: clippy * chore: use error codes * fix: order of verifications * fix: p2pk test ws updates We only expect the proof to be set to pending once. As a proof without a signature failes before the spent check where the state is chaged. * fix: mint_melt regtest frome wait
515 lines
16 KiB
Rust
515 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::mint_memory::MintMemoryDatabase;
|
|
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 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 localstore = MintMemoryDatabase::default();
|
|
|
|
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() -> &'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(
|
|
"".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::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 = MintMemoryDatabase::default();
|
|
|
|
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(())
|
|
}
|