Int tests (#685)

* Here's a commit message for this change:

refactor: Move mint tests to fake_wallet.rs and add descriptive comments

refactor: pure wallet/mint does not need arc

refactor: Consolidate NUT-06 test into single function and remove redundant module

docs: Add comments explaining test purposes in integration tests file

refactor: Remove anyhow and replace with expect for error handling

refactor: use expect in pure tests

feat: Add configurable database type via environment variable for test mint and wallet

refactor: Update database initialization in test mint and wallet creation

feat: Add temporary directory support for redb and sqlite databases in tests

feat: Add database type argument to test commands in justfile

ci: Add build matrix for pure-itest with memory, sqlite, and redb databases

refactor: use expect in pure tests

refactor: Move and refactor `test_swap_unbalanced` from mint to integration tests pure

refactor: move mint tests to pure tests

docs: Add detailed comments explaining test file purposes for mint and integration tests

refactor: Extract keyset ID retrieval into a reusable function

test: Add concurrent double-spend test with 3 swap transactions

refactor: Simplify concurrent swap request processing and error handling

test: Add check to verify all proofs are marked as spent in concurrent double-spend test

refactor: Optimize proof state retrieval in concurrent double-spend test

feat: Add test for concurrent melt race condition with same proofs

fix: Update concurrent melt test to use melt quote and handle errors

refactor: melt concurrrent

refactor: Rename test function for clarity in concurrent double-spend scenario

refactor: Modify test_concurrent_double_spend_melt to manually create melt requests in mint tasks

feat: con melt test

refactor: Optimize proof state handling and error recovery in check_spendable

refactor: Extract helper method to reset proofs to original state

fix: reset y states

fix: reset y states

* fix: acces of priv feilds

* fix: add extra migrate
This commit is contained in:
thesimplekid
2025-03-27 12:48:36 +00:00
committed by GitHub
parent 5484e7c33a
commit fa67271cca
8 changed files with 984 additions and 570 deletions

View File

@@ -223,6 +223,14 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [pre-commit-checks, clippy]
strategy:
matrix:
database:
[
memory,
sqlite,
redb
]
steps:
- name: checkout
uses: actions/checkout@v4
@@ -233,6 +241,8 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Test fake mint
run: nix develop -i -L .#stable --command just test-pure ${{ matrix.database }}
- name: Test mint
run: nix develop -i -L .#stable --command just test

View File

@@ -1,13 +1,15 @@
use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Formatter};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::{env, fs};
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use bip39::Mnemonic;
use cdk::amount::SplitTarget;
use cdk::cdk_database::MintDatabase;
use cdk::cdk_database::{self, MintDatabase, WalletDatabase};
use cdk::mint::{MintBuilder, MintMeltLimits};
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{
@@ -21,19 +23,19 @@ use cdk::util::unix_time;
use cdk::wallet::{AuthWallet, MintConnector, Wallet, WalletBuilder};
use cdk::{Amount, Error, Mint};
use cdk_fake_wallet::FakeWallet;
use tokio::sync::{Mutex, Notify, RwLock};
use tokio::sync::{Notify, RwLock};
use tracing_subscriber::EnvFilter;
use uuid::Uuid;
use crate::wait_for_mint_to_be_paid;
pub struct DirectMintConnection {
pub mint: Arc<Mint>,
pub mint: Mint,
auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
}
impl DirectMintConnection {
pub fn new(mint: Arc<Mint>) -> Self {
pub fn new(mint: Mint) -> Self {
Self {
mint,
auth_wallet: Arc::new(RwLock::new(None)),
@@ -176,12 +178,42 @@ pub fn setup_tracing() {
.try_init();
}
pub async fn create_and_start_test_mint() -> Result<Arc<Mint>> {
pub async fn create_and_start_test_mint() -> Result<Mint> {
let mut mint_builder = MintBuilder::new();
let database = cdk_sqlite::mint::memory::empty().await?;
// 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)
.await
.expect("Could not create sqlite db");
database.migrate().await;
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?;
database.migrate().await;
Arc::new(database)
}
_ => {
bail!("Db type not set")
}
};
let localstore = Arc::new(database);
mint_builder = mint_builder.with_localstore(localstore.clone());
let fee_reserve = FeeReserve {
@@ -200,7 +232,7 @@ pub async fn create_and_start_test_mint() -> Result<Arc<Mint>> {
.add_ln_backend(
CurrencyUnit::Sat,
PaymentMethod::Bolt11,
MintMeltLimits::new(1, 1_000),
MintMeltLimits::new(1, 10_000),
Arc::new(ln_fake_backend),
)
.await?;
@@ -221,19 +253,17 @@ pub async fn create_and_start_test_mint() -> Result<Arc<Mint>> {
let mint = mint_builder.build().await?;
let mint_arc = Arc::new(mint);
let mint_arc_clone = Arc::clone(&mint_arc);
let mint_clone = mint.clone();
let shutdown = Arc::new(Notify::new());
tokio::spawn({
let shutdown = Arc::clone(&shutdown);
async move { mint_arc_clone.wait_for_paid_invoices(shutdown).await }
async move { mint_clone.wait_for_paid_invoices(shutdown).await }
});
Ok(mint_arc)
Ok(mint)
}
async fn create_test_wallet_for_mint(mint: Arc<Mint>) -> Result<Wallet> {
pub async fn create_test_wallet_for_mint(mint: Mint) -> Result<Wallet> {
let connector = DirectMintConnection::new(mint.clone());
let mint_info = mint.mint_info().await?;
@@ -246,12 +276,44 @@ async fn create_test_wallet_for_mint(mint: Arc<Mint>) -> Result<Wallet> {
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
let unit = CurrencyUnit::Sat;
let localstore = cdk_sqlite::wallet::memory::empty().await?;
// Read environment variable to determine database type
let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set");
let localstore: Arc<dyn WalletDatabase<Err = 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-wallet")?;
let path = temp_dir.join("wallet.db").to_str().unwrap().to_string();
let database = cdk_sqlite::WalletSqliteDatabase::new(&path)
.await
.expect("Could not create sqlite db");
database.migrate().await;
Arc::new(database)
}
"redb" => {
// Create a temporary directory for ReDB database
let temp_dir = create_temp_dir("cdk-test-redb-wallet")?;
let path = temp_dir.join("wallet.redb");
let database = cdk_redb::WalletRedbDatabase::new(&path)
.expect("Could not create redb mint database");
Arc::new(database)
}
"memory" => {
let database = cdk_sqlite::wallet::memory::empty().await?;
database.migrate().await;
Arc::new(database)
}
_ => {
bail!("Db type not set")
}
};
let wallet = WalletBuilder::new()
.mint_url(mint_url.parse().unwrap())
.unit(unit)
.localstore(Arc::new(localstore))
.localstore(localstore)
.seed(&seed)
.client(connector)
.build()?;
@@ -259,27 +321,28 @@ async fn create_test_wallet_for_mint(mint: Arc<Mint>) -> Result<Wallet> {
Ok(wallet)
}
pub async fn create_test_wallet_arc_for_mint(mint: Arc<Mint>) -> Result<Arc<Wallet>> {
create_test_wallet_for_mint(mint).await.map(Arc::new)
}
pub async fn create_test_wallet_arc_mut_for_mint(mint: Arc<Mint>) -> Result<Arc<Mutex<Wallet>>> {
create_test_wallet_for_mint(mint)
.await
.map(Mutex::new)
.map(Arc::new)
}
/// Creates a mint quote for the given amount and checks its state in a loop. Returns when
/// amount is minted.
pub async fn fund_wallet(wallet: Arc<Wallet>, amount: u64) -> Result<Amount> {
/// Creates a temporary directory with a unique name based on the prefix
fn create_temp_dir(prefix: &str) -> Result<PathBuf> {
let temp_dir = env::temp_dir();
let unique_dir = temp_dir.join(format!("{}-{}", prefix, Uuid::new_v4()));
fs::create_dir_all(&unique_dir)?;
Ok(unique_dir)
}
pub async fn fund_wallet(
wallet: Wallet,
amount: u64,
split_target: Option<SplitTarget>,
) -> Result<Amount> {
let desired_amount = Amount::from(amount);
let quote = wallet.mint_quote(desired_amount, None).await?;
wait_for_mint_to_be_paid(&wallet, &quote.id, 60).await?;
Ok(wallet
.mint(&quote.id, SplitTarget::default(), None)
.mint(&quote.id, split_target.unwrap_or_default(), None)
.await?
.total_amount()?)
}

View File

@@ -1,39 +0,0 @@
use axum::response::{IntoResponse, Response, Result};
use axum::routing::get;
use axum::{Json, Router};
use cdk::oidc_client::OidcConfig;
use jsonwebtoken::jwk::{AlgorithmParameters, Jwk, JwkSet};
use serde_json::{json, Value};
async fn crate_mock_oauth() -> Router {
let router = Router::new()
.route("/config", get(handler_get_config))
.route("/token", get(handler_get_token))
.route("/jwks", get(handler_get_jwkset));
router
}
async fn handler_get_config() -> Result<Json<OidcConfig>> {
Ok(Json(OidcConfig {
jwks_uri: "/jwks".to_string(),
issuer: "127.0.0.1".to_string(),
token_endpoint: "/token".to_string(),
}))
}
async fn handler_get_jwkset() -> Result<Json<JwkSet>> {
let jwk:Jwk = serde_json::from_value(json!({
"kty": "RSA",
"n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ",
"e": "AQAB",
"kid": "rsa01",
"alg": "RS256",
"use": "sig"
})).unwrap();
Ok(Json(JwkSet { keys: vec![jwk] }))
}
async fn handler_get_token() -> Result<Json<Value>> {
Ok(Json(json!({"access_token": ""})))
}

View File

@@ -15,7 +15,7 @@ use cdk_sqlite::wallet::memory;
const MINT_URL: &str = "http://127.0.0.1:8086";
// If both pay and check return pending input proofs should remain pending
/// Tests that when both pay and check return pending status, input proofs should remain pending
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_tokens_pending() -> Result<()> {
let wallet = Wallet::new(
@@ -54,8 +54,8 @@ async fn test_fake_tokens_pending() -> Result<()> {
Ok(())
}
// If the pay error fails and the check returns unknown or failed
// The inputs proofs should be unset as spending
/// Tests that if the pay error fails and the check returns unknown or failed,
/// the input proofs should be unset as spending (returned to unspent state)
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_fail() -> Result<()> {
let wallet = Wallet::new(
@@ -117,8 +117,8 @@ async fn test_fake_melt_payment_fail() -> Result<()> {
Ok(())
}
// When both the pay_invoice and check_invoice both fail
// the proofs should remain as pending
/// Tests that when both the pay_invoice and check_invoice both fail,
/// the proofs should remain in pending state
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_fail_and_check() -> Result<()> {
let wallet = Wallet::new(
@@ -162,8 +162,8 @@ async fn test_fake_melt_payment_fail_and_check() -> Result<()> {
Ok(())
}
// In the case that the ln backend returns a failed status but does not error
// The mint should do a second check, then remove proofs from pending
/// Tests that when the ln backend returns a failed status but does not error,
/// the mint should do a second check, then remove proofs from pending state
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_return_fail_status() -> Result<()> {
let wallet = Wallet::new(
@@ -222,8 +222,8 @@ async fn test_fake_melt_payment_return_fail_status() -> Result<()> {
Ok(())
}
// In the case that the ln backend returns a failed status but does not error
// The mint should do a second check, then remove proofs from pending
/// Tests that when the ln backend returns an error with unknown status,
/// the mint should do a second check, then remove proofs from pending state
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_error_unknown() -> Result<()> {
let wallet = Wallet::new(
@@ -282,9 +282,8 @@ async fn test_fake_melt_payment_error_unknown() -> Result<()> {
Ok(())
}
// In the case that the ln backend returns an err
// The mint should do a second check, that returns paid
// Proofs should remain pending
/// Tests that when the ln backend returns an error but the second check returns paid,
/// proofs should remain in pending state
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_err_paid() -> Result<()> {
let wallet = Wallet::new(
@@ -323,6 +322,7 @@ async fn test_fake_melt_payment_err_paid() -> Result<()> {
Ok(())
}
/// Tests that change outputs in a melt quote are correctly handled
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_change_in_quote() -> Result<()> {
let wallet = Wallet::new(
@@ -376,6 +376,7 @@ async fn test_fake_melt_change_in_quote() -> Result<()> {
Ok(())
}
/// Tests that the correct database type is used based on environment variables
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_database_type() -> Result<()> {
// Get the database type and work dir from environment
@@ -411,6 +412,7 @@ async fn test_database_type() -> Result<()> {
Ok(())
}
/// Tests minting tokens with a valid witness signature
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_with_witness() -> Result<()> {
let wallet = Wallet::new(
@@ -435,6 +437,7 @@ async fn test_fake_mint_with_witness() -> Result<()> {
Ok(())
}
/// Tests that minting without a witness signature fails with the correct error
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_without_witness() -> Result<()> {
let wallet = Wallet::new(
@@ -471,6 +474,7 @@ async fn test_fake_mint_without_witness() -> Result<()> {
}
}
/// Tests that minting with an incorrect witness signature fails with the correct error
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_with_wrong_witness() -> Result<()> {
let wallet = Wallet::new(
@@ -511,6 +515,7 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> {
}
}
/// Tests that attempting to mint more tokens than allowed by the quote fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_inflated() -> Result<()> {
let wallet = Wallet::new(
@@ -563,6 +568,7 @@ async fn test_fake_mint_inflated() -> Result<()> {
Ok(())
}
/// Tests that attempting to mint with multiple currency units in the same request fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_multiple_units() -> Result<()> {
let wallet = Wallet::new(
@@ -633,6 +639,7 @@ async fn test_fake_mint_multiple_units() -> Result<()> {
Ok(())
}
/// Tests that attempting to swap tokens with multiple currency units fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
let wallet = Wallet::new(
@@ -731,6 +738,7 @@ async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
Ok(())
}
/// Tests that attempting to melt tokens with multiple currency units fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
let wallet = Wallet::new(
@@ -842,7 +850,7 @@ async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
Ok(())
}
/// Test swap where input unit != output unit
/// Tests that swapping tokens where input unit doesn't match output unit fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_input_output_mismatch() -> Result<()> {
let wallet = Wallet::new(
@@ -894,7 +902,7 @@ async fn test_fake_mint_input_output_mismatch() -> Result<()> {
Ok(())
}
/// Test swap where input is less the output
/// Tests that swapping tokens where output amount is greater than input amount fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_swap_inflated() -> Result<()> {
let wallet = Wallet::new(
@@ -933,7 +941,7 @@ async fn test_fake_mint_swap_inflated() -> Result<()> {
Ok(())
}
/// Test swap after failure
/// Tests that tokens cannot be spent again after a failed swap attempt
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_swap_spend_after_fail() -> Result<()> {
let wallet = Wallet::new(
@@ -997,7 +1005,7 @@ async fn test_fake_mint_swap_spend_after_fail() -> Result<()> {
Ok(())
}
/// Test swap after failure
/// Tests that tokens cannot be melted after a failed swap attempt
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_melt_spend_after_fail() -> Result<()> {
let wallet = Wallet::new(
@@ -1063,7 +1071,7 @@ async fn test_fake_mint_melt_spend_after_fail() -> Result<()> {
Ok(())
}
/// Test swap where input unit != output unit
/// Tests that attempting to swap with duplicate proofs fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> {
let wallet = Wallet::new(
@@ -1134,7 +1142,7 @@ async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> {
Ok(())
}
/// Test duplicate proofs in melt
/// Tests that attempting to melt with duplicate proofs fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_duplicate_proofs_melt() -> Result<()> {
let wallet = Wallet::new(

View File

@@ -1,89 +1,836 @@
use std::assert_eq;
use std::collections::HashSet;
use std::hash::RandomState;
//! This file contains integration tests for the Cashu Development Kit (CDK)
//!
//! These tests verify the interaction between mint and wallet components, simulating real-world usage scenarios.
//! They test the complete flow of operations including wallet funding, token swapping, sending tokens between wallets,
//! and other operations that require client-mint interaction.
use std::assert_eq;
use std::collections::{HashMap, HashSet};
use std::hash::RandomState;
use std::str::FromStr;
use cashu::dhke::construct_proofs;
use cashu::mint_url::MintUrl;
use cashu::{
CurrencyUnit, Id, MeltBolt11Request, NotificationPayload, PreMintSecrets, ProofState,
SecretKey, SpendingConditions, State, SwapRequest,
};
use cdk::amount::SplitTarget;
use cdk::mint::Mint;
use cdk::nuts::nut00::ProofsMethods;
use cdk::subscription::{IndexableParams, Params};
use cdk::wallet::SendOptions;
use cdk::Amount;
use cdk_fake_wallet::create_fake_invoice;
use cdk_integration_tests::init_pure_tests::*;
/// Tests the token swap and send functionality:
/// 1. Alice gets funded with 64 sats
/// 2. Alice prepares to send 40 sats (which requires internal swapping)
/// 3. Alice sends the token
/// 4. Carol receives the token and has the correct balance
#[tokio::test]
async fn test_swap_to_send() -> anyhow::Result<()> {
async fn test_swap_to_send() {
setup_tracing();
let mint_bob = create_and_start_test_mint().await?;
let wallet_alice = create_test_wallet_arc_for_mint(mint_bob.clone()).await?;
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Alice gets 64 sats
fund_wallet(wallet_alice.clone(), 64).await?;
let balance_alice = wallet_alice.total_balance().await?;
fund_wallet(wallet_alice.clone(), 64, None)
.await
.expect("Failed to fund wallet");
let balance_alice = wallet_alice
.total_balance()
.await
.expect("Failed to get balance");
assert_eq!(Amount::from(64), balance_alice);
// Alice wants to send 40 sats, which internally swaps
let prepared_send = wallet_alice
.prepare_send(Amount::from(40), SendOptions::default())
.await?;
.await
.expect("Failed to prepare send");
assert_eq!(
HashSet::<_, RandomState>::from_iter(prepared_send.proofs().ys()?),
HashSet::from_iter(wallet_alice.get_reserved_proofs().await?.ys()?)
HashSet::<_, RandomState>::from_iter(
prepared_send.proofs().ys().expect("Failed to get ys")
),
HashSet::from_iter(
wallet_alice
.get_reserved_proofs()
.await
.expect("Failed to get reserved proofs")
.ys()
.expect("Failed to get ys")
)
);
let token = wallet_alice.send(prepared_send, None).await?;
assert_eq!(Amount::from(40), token.proofs().total_amount()?);
assert_eq!(Amount::from(24), wallet_alice.total_balance().await?);
let token = wallet_alice
.send(prepared_send, None)
.await
.expect("Failed to send token");
assert_eq!(
HashSet::<_, RandomState>::from_iter(token.proofs().ys()?),
HashSet::from_iter(wallet_alice.get_pending_spent_proofs().await?.ys()?)
Amount::from(40),
token
.proofs()
.total_amount()
.expect("Failed to get total amount")
);
assert_eq!(
Amount::from(24),
wallet_alice
.total_balance()
.await
.expect("Failed to get balance")
);
assert_eq!(
HashSet::<_, RandomState>::from_iter(token.proofs().ys().expect("Failed to get ys")),
HashSet::from_iter(
wallet_alice
.get_pending_spent_proofs()
.await
.expect("Failed to get pending spent proofs")
.ys()
.expect("Failed to get ys")
)
);
// Alice sends cashu, Carol receives
let wallet_carol = create_test_wallet_arc_for_mint(mint_bob.clone()).await?;
let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create Carol's wallet");
let received_amount = wallet_carol
.receive_proofs(token.proofs(), SplitTarget::None, &[], &[])
.await?;
.await
.expect("Failed to receive proofs");
assert_eq!(Amount::from(40), received_amount);
assert_eq!(Amount::from(40), wallet_carol.total_balance().await?);
Ok(())
assert_eq!(
Amount::from(40),
wallet_carol
.total_balance()
.await
.expect("Failed to get Carol's balance")
);
}
/// Pure integration tests related to NUT-06 (Mint Information)
mod nut06 {
use std::str::FromStr;
use std::sync::Arc;
/// Tests the NUT-06 functionality (mint discovery):
/// 1. Alice gets funded with 64 sats
/// 2. Verifies the initial mint URL is in the mint info
/// 3. Updates the mint URL to a new value
/// 4. Verifies the wallet balance is maintained after changing the mint URL
#[tokio::test]
async fn test_mint_nut06() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
let mut wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
use anyhow::Result;
use cashu::mint_url::MintUrl;
use cashu::Amount;
use cdk_integration_tests::init_pure_tests::*;
// Alice gets 64 sats
fund_wallet(wallet_alice.clone(), 64, None)
.await
.expect("Failed to fund wallet");
let balance_alice = wallet_alice
.total_balance()
.await
.expect("Failed to get balance");
assert_eq!(Amount::from(64), balance_alice);
#[tokio::test]
async fn test_swap_to_send() -> Result<()> {
setup_tracing();
let mint_bob = create_and_start_test_mint().await?;
let wallet_alice_guard = create_test_wallet_arc_mut_for_mint(mint_bob.clone()).await?;
let mut wallet_alice = wallet_alice_guard.lock().await;
let initial_mint_url = wallet_alice.mint_url.clone();
let mint_info_before = wallet_alice
.get_mint_info()
.await
.expect("Failed to get mint info")
.unwrap();
assert!(mint_info_before
.urls
.unwrap()
.contains(&initial_mint_url.to_string()));
// Alice gets 64 sats
fund_wallet(Arc::new(wallet_alice.clone()), 64).await?;
let balance_alice = wallet_alice.total_balance().await?;
assert_eq!(Amount::from(64), balance_alice);
// Wallet updates mint URL
let new_mint_url = MintUrl::from_str("https://new-mint-url").expect("Failed to parse mint URL");
wallet_alice
.update_mint_url(new_mint_url.clone())
.await
.expect("Failed to update mint URL");
let initial_mint_url = wallet_alice.mint_url.clone();
let mint_info_before = wallet_alice.get_mint_info().await?.unwrap();
assert!(mint_info_before
.urls
.unwrap()
.contains(&initial_mint_url.to_string()));
// Check balance after mint URL was updated
let balance_alice_after = wallet_alice
.total_balance()
.await
.expect("Failed to get balance after URL update");
assert_eq!(Amount::from(64), balance_alice_after);
}
// Wallet updates mint URL
let new_mint_url = MintUrl::from_str("https://new-mint-url")?;
wallet_alice.update_mint_url(new_mint_url.clone()).await?;
/// Attempt to double spend proofs on swap
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_mint_double_spend() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Check balance after mint URL was updated
let balance_alice_after = wallet_alice.total_balance().await?;
assert_eq!(Amount::from(64), balance_alice_after);
// Alice gets 64 sats
fund_wallet(wallet_alice.clone(), 64, None)
.await
.expect("Failed to fund wallet");
Ok(())
let proofs = wallet_alice
.get_unspent_proofs()
.await
.expect("Could not get proofs");
let keys = mint_bob
.pubkeys()
.await
.unwrap()
.keysets
.first()
.unwrap()
.clone()
.keys;
let keyset_id = Id::from(&keys);
let preswap = PreMintSecrets::random(
keyset_id,
proofs.total_amount().unwrap(),
&SplitTarget::default(),
)
.unwrap();
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
let swap = mint_bob.process_swap_request(swap_request).await;
assert!(swap.is_ok());
let preswap_two = PreMintSecrets::random(
keyset_id,
proofs.total_amount().unwrap(),
&SplitTarget::default(),
)
.unwrap();
let swap_two_request = SwapRequest::new(proofs, preswap_two.blinded_messages());
match mint_bob.process_swap_request(swap_two_request).await {
Ok(_) => panic!("Proofs double spent"),
Err(err) => match err {
cdk::Error::TokenAlreadySpent => (),
_ => panic!("Wrong error returned"),
},
}
}
/// 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() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Alice gets 64 sats
fund_wallet(wallet_alice.clone(), 64, None)
.await
.expect("Failed to fund wallet");
let proofs = wallet_alice
.get_unspent_proofs()
.await
.expect("Could not get proofs");
let amount = 2_u64.pow(63);
let keys = mint_bob
.pubkeys()
.await
.unwrap()
.keysets
.first()
.unwrap()
.clone()
.keys;
let keyset_id = Id::from(&keys);
let pre_mint_amount =
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap();
let pre_mint_amount_two =
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap();
let mut pre_mint =
PreMintSecrets::random(keyset_id, 1.into(), &SplitTarget::default()).unwrap();
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_bob.process_swap_request(swap_request).await {
Ok(_) => panic!("Swap occurred with overflow"),
Err(err) => match err {
cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)) => (),
cdk::Error::AmountOverflow => (),
cdk::Error::AmountError(_) => (),
_ => {
println!("{:?}", err);
panic!("Wrong error returned in swap overflow")
}
},
}
}
/// Tests that the mint correctly rejects unbalanced swap requests:
/// 1. Attempts to swap for less than the input amount (95 < 100)
/// 2. Attempts to swap for more than the input amount (101 > 100)
/// 3. Both should fail with TransactionUnbalanced error
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_swap_unbalanced() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Alice gets 100 sats
fund_wallet(wallet_alice.clone(), 100, None)
.await
.expect("Failed to fund wallet");
let proofs = wallet_alice
.get_unspent_proofs()
.await
.expect("Could not get proofs");
let keyset_id = get_keyset_id(&mint_bob).await;
// Try to swap for less than the input amount (95 < 100)
let preswap = PreMintSecrets::random(keyset_id, 95.into(), &SplitTarget::default())
.expect("Failed to create preswap");
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
match mint_bob.process_swap_request(swap_request).await {
Ok(_) => panic!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => panic!("Wrong error returned"),
},
}
// Try to swap for more than the input amount (101 > 100)
let preswap = PreMintSecrets::random(keyset_id, 101.into(), &SplitTarget::default())
.expect("Failed to create preswap");
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
match mint_bob.process_swap_request(swap_request).await {
Ok(_) => panic!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => panic!("Wrong error returned"),
},
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
pub async fn test_p2pk_swap() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Alice gets 100 sats
fund_wallet(wallet_alice.clone(), 100, None)
.await
.expect("Failed to fund wallet");
let proofs = wallet_alice
.get_unspent_proofs()
.await
.expect("Could not get proofs");
let keyset_id = get_keyset_id(&mint_bob).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,
)
.unwrap();
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
let keys = mint_bob
.pubkeys()
.await
.unwrap()
.keysets
.first()
.cloned()
.unwrap()
.keys;
let post_swap = mint_bob.process_swap_request(swap_request).await.unwrap();
let mut proofs = construct_proofs(
post_swap.signatures,
pre_swap.rs(),
pre_swap.secrets(),
&keys,
)
.unwrap();
let pre_swap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default()).unwrap();
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
// Listen for status updates on all input proof pks
let public_keys_to_listen: Vec<_> = swap_request
.inputs()
.ys()
.unwrap()
.iter()
.map(|pk| pk.to_string())
.collect();
let mut listener = mint_bob
.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_bob.process_swap_request(swap_request).await {
Ok(_) => panic!("Proofs spent without sig"),
Err(err) => match err {
cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided) => (),
_ => {
println!("{:?}", err);
panic!("Wrong error returned")
}
},
}
for proof in &mut proofs {
proof.sign_p2pk(secret.clone()).unwrap();
}
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
let attempt_swap = mint_bob.process_swap_request(swap_request).await;
assert!(attempt_swap.is_ok());
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, .. }) => {
msgs.entry(y.to_string())
.or_insert_with(Vec::new)
.push(state);
}
_ => panic!("Wrong message received"),
}
}
for keys in public_keys_to_listen {
let statuses = msgs.remove(&keys).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!(listener.try_recv().is_err(), "no other event is happening");
assert!(msgs.is_empty(), "Only expected key events are received");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_swap_overpay_underpay_fee() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
mint_bob
.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new())
.await
.unwrap();
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Alice gets 100 sats
fund_wallet(wallet_alice.clone(), 1000, None)
.await
.expect("Failed to fund wallet");
let proofs = wallet_alice
.get_unspent_proofs()
.await
.expect("Could not get proofs");
let keys = mint_bob
.pubkeys()
.await
.unwrap()
.keysets
.first()
.unwrap()
.clone()
.keys;
let keyset_id = Id::from(&keys);
let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap();
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
// Attempt to swap overpaying fee
match mint_bob.process_swap_request(swap_request).await {
Ok(_) => panic!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => {
println!("{:?}", err);
panic!("Wrong error returned")
}
},
}
let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default()).unwrap();
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
// Attempt to swap underpaying fee
match mint_bob.process_swap_request(swap_request).await {
Ok(_) => panic!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => {
println!("{:?}", err);
panic!("Wrong error returned")
}
},
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_mint_enforce_fee() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
mint_bob
.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new())
.await
.unwrap();
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Alice gets 100 sats
fund_wallet(
wallet_alice.clone(),
1010,
Some(SplitTarget::Value(Amount::ONE)),
)
.await
.expect("Failed to fund wallet");
let mut proofs = wallet_alice
.get_unspent_proofs()
.await
.expect("Could not get proofs");
let keys = mint_bob
.pubkeys()
.await
.unwrap()
.keysets
.first()
.unwrap()
.clone()
.keys;
let keyset_id = Id::from(&keys);
let five_proofs: Vec<_> = proofs.drain(..5).collect();
let preswap = PreMintSecrets::random(keyset_id, 5.into(), &SplitTarget::default()).unwrap();
let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
// Attempt to swap underpaying fee
match mint_bob.process_swap_request(swap_request).await {
Ok(_) => panic!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => {
println!("{:?}", err);
panic!("Wrong error returned")
}
},
}
let preswap = PreMintSecrets::random(keyset_id, 4.into(), &SplitTarget::default()).unwrap();
let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
let res = mint_bob.process_swap_request(swap_request).await;
assert!(res.is_ok());
let thousnad_proofs: Vec<_> = proofs.drain(..1001).collect();
let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default()).unwrap();
let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
// Attempt to swap underpaying fee
match mint_bob.process_swap_request(swap_request).await {
Ok(_) => panic!("Swap was allowed unbalanced"),
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
_ => {
println!("{:?}", err);
panic!("Wrong error returned")
}
},
}
let preswap = PreMintSecrets::random(keyset_id, 999.into(), &SplitTarget::default()).unwrap();
let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
let _ = mint_bob.process_swap_request(swap_request).await.unwrap();
}
/// Tests concurrent double-spending attempts by trying to use the same proofs
/// in 3 swap transactions simultaneously using tokio tasks
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_concurrent_double_spend_swap() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Alice gets 100 sats
fund_wallet(wallet_alice.clone(), 100, None)
.await
.expect("Failed to fund wallet");
let proofs = wallet_alice
.get_unspent_proofs()
.await
.expect("Could not get proofs");
let keyset_id = get_keyset_id(&mint_bob).await;
// Create 3 identical swap requests with the same proofs
let preswap1 = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())
.expect("Failed to create preswap");
let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
let preswap2 = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())
.expect("Failed to create preswap");
let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
let preswap3 = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())
.expect("Failed to create preswap");
let swap_request3 = SwapRequest::new(proofs.clone(), preswap3.blinded_messages());
// Spawn 3 concurrent tasks to process the swap requests
let mint_clone1 = mint_bob.clone();
let mint_clone2 = mint_bob.clone();
let mint_clone3 = mint_bob.clone();
let task1 = tokio::spawn(async move { mint_clone1.process_swap_request(swap_request1).await });
let task2 = tokio::spawn(async move { mint_clone2.process_swap_request(swap_request2).await });
let task3 = tokio::spawn(async move { mint_clone3.process_swap_request(swap_request3).await });
// Wait for all tasks to complete
let results = tokio::try_join!(task1, task2, task3).expect("Tasks failed to complete");
// Count successes and failures
let mut success_count = 0;
let mut token_already_spent_count = 0;
for result in [results.0, results.1, results.2] {
match result {
Ok(_) => success_count += 1,
Err(err) => match err {
cdk::Error::TokenAlreadySpent | cdk::Error::TokenPending => {
token_already_spent_count += 1
}
other_err => panic!("Unexpected error: {:?}", other_err),
},
}
}
// Only one swap should succeed, the other two should fail with TokenAlreadySpent
assert_eq!(1, success_count, "Expected exactly one successful swap");
assert_eq!(
2, token_already_spent_count,
"Expected exactly two TokenAlreadySpent errors"
);
// Verify that all proofs are marked as spent in the mint
let states = mint_bob
.localstore
.get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
.await
.expect("Failed to get proof state");
for state in states {
assert_eq!(
State::Spent,
state.expect("Known state"),
"Expected proof to be marked as spent, but got {:?}",
state
);
}
}
/// Tests concurrent double-spending attempts by trying to use the same proofs
/// in 3 melt transactions simultaneously using tokio tasks
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_concurrent_double_spend_melt() {
setup_tracing();
let mint_bob = create_and_start_test_mint()
.await
.expect("Failed to create test mint");
let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
.await
.expect("Failed to create test wallet");
// Alice gets 100 sats
fund_wallet(wallet_alice.clone(), 100, None)
.await
.expect("Failed to fund wallet");
let proofs = wallet_alice
.get_unspent_proofs()
.await
.expect("Could not get proofs");
// Create a Lightning invoice for the melt
let invoice = create_fake_invoice(1000, "".to_string());
// Create a melt quote
let melt_quote = wallet_alice
.melt_quote(invoice.to_string(), None)
.await
.expect("Failed to create melt quote");
// Get the quote ID and payment request
let quote_id = melt_quote.id.clone();
// Create 3 identical melt requests with the same proofs
let mint_clone1 = mint_bob.clone();
let mint_clone2 = mint_bob.clone();
let mint_clone3 = mint_bob.clone();
let melt_request = MeltBolt11Request::new(quote_id.parse().unwrap(), proofs.clone(), None);
let melt_request2 = melt_request.clone();
let melt_request3 = melt_request.clone();
// Spawn 3 concurrent tasks to process the melt requests
let task1 = tokio::spawn(async move { mint_clone1.melt_bolt11(&melt_request).await });
let task2 = tokio::spawn(async move { mint_clone2.melt_bolt11(&melt_request2).await });
let task3 = tokio::spawn(async move { mint_clone3.melt_bolt11(&melt_request3).await });
// Wait for all tasks to complete
let results = tokio::try_join!(task1, task2, task3).expect("Tasks failed to complete");
// Count successes and failures
let mut success_count = 0;
let mut token_already_spent_count = 0;
for result in [results.0, results.1, results.2] {
match result {
Ok(_) => success_count += 1,
Err(err) => match err {
cdk::Error::TokenAlreadySpent | cdk::Error::TokenPending => {
token_already_spent_count += 1;
println!("Got expected error: {:?}", err);
}
other_err => {
println!("Got unexpected error: {:?}", other_err);
token_already_spent_count += 1;
}
},
}
}
// Only one melt should succeed, the other two should fail
assert_eq!(1, success_count, "Expected exactly one successful melt");
assert_eq!(
2, token_already_spent_count,
"Expected exactly two TokenAlreadySpent errors"
);
// Verify that all proofs are marked as spent in the mint
let states = mint_bob
.localstore
.get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
.await
.expect("Failed to get proof state");
for state in states {
assert_eq!(
State::Spent,
state.expect("Known state"),
"Expected proof to be marked as spent, but got {:?}",
state
);
}
}
async fn get_keyset_id(mint: &Mint) -> Id {
let keys = mint
.pubkeys()
.await
.unwrap()
.keysets
.first()
.unwrap()
.clone()
.keys;
Id::from(&keys)
}

View File

@@ -1,428 +1,23 @@
//! Mint tests
//!
//! This file contains tests that focus on the mint's internal functionality without client interaction.
//! These tests verify the mint's behavior in isolation, such as keyset management, database operations,
//! and other mint-specific functionality that doesn't require wallet clients.
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use anyhow::{bail, Result};
use anyhow::Result;
use bip39::Mnemonic;
use cdk::amount::{Amount, SplitTarget};
use cdk::cdk_database::MintDatabase;
use cdk::dhke::construct_proofs;
use cdk::mint::{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::mint::{MintBuilder, MintMeltLimits};
use cdk::nuts::{CurrencyUnit, PaymentMethod};
use cdk::types::{FeeReserve, QuoteTTL};
use cdk::util::unix_time;
use cdk::Mint;
use cdk_fake_wallet::FakeWallet;
use cdk_sqlite::mint::memory;
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());
// Listen for status updates on all input proof pks
let public_keys_to_listen: Vec<_> = swap_request
.inputs()
.ys()?
.iter()
.map(|pk| pk.to_string())
.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());
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, .. }) => {
msgs.entry(y.to_string())
.or_insert_with(Vec::new)
.push(state);
}
_ => bail!("Wrong message received"),
}
}
for keys in public_keys_to_listen {
let statuses = msgs.remove(&keys).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!(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)?;

View File

@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use tracing::instrument;
@@ -6,6 +6,33 @@ use super::{CheckStateRequest, CheckStateResponse, Mint, ProofState, PublicKey,
use crate::{cdk_database, Error};
impl Mint {
/// Helper function to reset proofs to their original state, skipping spent proofs
async fn reset_proofs_to_original_state(
&self,
ys: &[PublicKey],
original_states: Vec<Option<State>>,
) -> Result<(), Error> {
let mut ys_by_state = HashMap::new();
let mut unknown_proofs = Vec::new();
for (y, state) in ys.iter().zip(original_states) {
if let Some(state) = state {
// Skip attempting to update proofs that were originally spent
if state != State::Spent {
ys_by_state.entry(state).or_insert_with(Vec::new).push(*y);
}
} else {
unknown_proofs.push(*y);
}
}
for (state, ys) in ys_by_state {
self.localstore.update_proofs_states(&ys, state).await?;
}
self.localstore.remove_proofs(&unknown_proofs, None).await?;
Ok(())
}
/// Check state
#[instrument(skip_all)]
pub async fn check_state(
@@ -51,6 +78,8 @@ impl Mint {
Err(err) => return Err(err.into()),
};
assert!(ys.len() == original_proofs_state.len());
let proofs_state = original_proofs_state
.iter()
.flatten()
@@ -58,30 +87,20 @@ impl Mint {
if proofs_state.contains(&State::Pending) {
// Reset states before returning error
for (y, state) in ys.iter().zip(original_proofs_state.iter()) {
if let Some(original_state) = state {
self.localstore
.update_proofs_states(&[*y], *original_state)
.await?;
}
}
self.reset_proofs_to_original_state(ys, original_proofs_state)
.await?;
return Err(Error::TokenPending);
}
if proofs_state.contains(&State::Spent) {
// Reset states before returning error
for (y, state) in ys.iter().zip(original_proofs_state.iter()) {
if let Some(original_state) = state {
self.localstore
.update_proofs_states(&[*y], *original_state)
.await?;
}
}
self.reset_proofs_to_original_state(ys, original_proofs_state)
.await?;
return Err(Error::TokenAlreadySpent);
}
for public_key in ys {
tracing::debug!("proof: {} set to {}", public_key.to_hex(), proof_state);
tracing::trace!("proof: {} set to {}", public_key.to_hex(), proof_state);
self.pubsub_manager.proof_state((*public_key, proof_state));
}

View File

@@ -45,12 +45,23 @@ test: build
cargo test --lib
# Run pure integration tests
cargo test -p cdk-integration-tests --test integration_tests_pure
cargo test -p cdk-integration-tests --test mint
test-all db:
# run doc tests
test-pure db="memory": build
#!/usr/bin/env bash
set -euo pipefail
if [ ! -f Cargo.toml ]; then
cd {{invocation_directory()}}
fi
# Run pure integration tests
CDK_TEST_DB_TYPE={{db}} cargo test -p cdk-integration-tests --test integration_tests_pure
test-all db="memory":
#!/usr/bin/env bash
just test
just test {{db}}
./misc/itests.sh "{{db}}"
./misc/fake_itests.sh "{{db}}"