mint.rs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. //! Mint Tests
  2. //!
  3. //! This file contains tests that focus on the mint's internal functionality without client interaction.
  4. //! These tests verify the mint's behavior in isolation, such as keyset management, database operations,
  5. //! and other mint-specific functionality that doesn't require wallet clients.
  6. //!
  7. //! Test Categories:
  8. //! - Keyset rotation and management
  9. //! - Database transaction handling
  10. //! - Internal state transitions
  11. //! - Fee calculation and enforcement
  12. //! - Proof validation and state management
  13. use std::collections::{HashMap, HashSet};
  14. use std::sync::Arc;
  15. use bip39::Mnemonic;
  16. use cashu::nut00::KnownMethod;
  17. use cashu::PaymentMethod;
  18. use cdk::mint::{MintBuilder, MintMeltLimits};
  19. use cdk::nuts::CurrencyUnit;
  20. use cdk::types::{FeeReserve, QuoteTTL};
  21. use cdk_fake_wallet::FakeWallet;
  22. use cdk_sqlite::mint::memory;
  23. pub const MINT_URL: &str = "http://127.0.0.1:8088";
  24. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  25. async fn test_correct_keyset() {
  26. let mnemonic = Mnemonic::generate(12).unwrap();
  27. let fee_reserve = FeeReserve {
  28. min_fee_reserve: 1.into(),
  29. percent_fee_reserve: 1.0,
  30. };
  31. let database = memory::empty().await.expect("valid db instance");
  32. let fake_wallet = FakeWallet::new(
  33. fee_reserve,
  34. HashMap::default(),
  35. HashSet::default(),
  36. 0,
  37. CurrencyUnit::Sat,
  38. );
  39. let localstore = Arc::new(database);
  40. let mut mint_builder = MintBuilder::new(localstore.clone());
  41. mint_builder = mint_builder
  42. .with_name("regtest mint".to_string())
  43. .with_description("regtest mint".to_string());
  44. mint_builder
  45. .add_payment_processor(
  46. CurrencyUnit::Sat,
  47. PaymentMethod::Known(KnownMethod::Bolt11),
  48. MintMeltLimits::new(1, 5_000),
  49. Arc::new(fake_wallet),
  50. )
  51. .await
  52. .unwrap();
  53. // .with_seed(mnemonic.to_seed_normalized("").to_vec());
  54. let mint = mint_builder
  55. .build_with_seed(localstore.clone(), &mnemonic.to_seed_normalized(""))
  56. .await
  57. .unwrap();
  58. let quote_ttl = QuoteTTL::new(10000, 10000);
  59. mint.set_quote_ttl(quote_ttl).await.unwrap();
  60. let active = mint.get_active_keysets();
  61. let active = active
  62. .get(&CurrencyUnit::Sat)
  63. .expect("There is a keyset for unit");
  64. let old_keyset_info = mint.get_keyset_info(active).expect("There is keyset");
  65. mint.rotate_keyset(
  66. CurrencyUnit::Sat,
  67. cdk_integration_tests::standard_keyset_amounts(32),
  68. 0,
  69. true,
  70. )
  71. .await
  72. .unwrap();
  73. let active = mint.get_active_keysets();
  74. let active = active
  75. .get(&CurrencyUnit::Sat)
  76. .expect("There is a keyset for unit");
  77. let keyset_info = mint.get_keyset_info(active).expect("There is keyset");
  78. assert_ne!(keyset_info.id, old_keyset_info.id);
  79. mint.rotate_keyset(
  80. CurrencyUnit::Sat,
  81. cdk_integration_tests::standard_keyset_amounts(32),
  82. 0,
  83. true,
  84. )
  85. .await
  86. .unwrap();
  87. let active = mint.get_active_keysets();
  88. let active = active
  89. .get(&CurrencyUnit::Sat)
  90. .expect("There is a keyset for unit");
  91. let new_keyset_info = mint.get_keyset_info(active).expect("There is keyset");
  92. assert_ne!(new_keyset_info.id, keyset_info.id);
  93. }
  94. /// Test concurrent payment processing to verify race condition fix
  95. ///
  96. /// This test simulates the real-world race condition where multiple concurrent
  97. /// payment notifications arrive for the same payment_id. Before the fix, this
  98. /// would cause "Payment ID already exists" errors. After the fix, all but one
  99. /// should gracefully handle the duplicate and return a Duplicate error.
  100. #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
  101. async fn test_concurrent_duplicate_payment_handling() {
  102. use cashu::PaymentMethod;
  103. use cdk::cdk_database::{MintDatabase, MintQuotesDatabase};
  104. use cdk::mint::MintQuote;
  105. use cdk::Amount;
  106. use cdk_common::payment::PaymentIdentifier;
  107. use tokio::task::JoinSet;
  108. // Create a test mint with in-memory database
  109. let mnemonic = Mnemonic::generate(12).unwrap();
  110. let fee_reserve = FeeReserve {
  111. min_fee_reserve: 1.into(),
  112. percent_fee_reserve: 1.0,
  113. };
  114. let database = Arc::new(memory::empty().await.expect("valid db instance"));
  115. let fake_wallet = FakeWallet::new(
  116. fee_reserve,
  117. HashMap::default(),
  118. HashSet::default(),
  119. 0,
  120. CurrencyUnit::Sat,
  121. );
  122. let mut mint_builder = MintBuilder::new(database.clone());
  123. mint_builder = mint_builder
  124. .with_name("concurrent test mint".to_string())
  125. .with_description("testing concurrent payment handling".to_string());
  126. mint_builder
  127. .add_payment_processor(
  128. CurrencyUnit::Sat,
  129. PaymentMethod::Known(KnownMethod::Bolt11),
  130. MintMeltLimits::new(1, 5_000),
  131. Arc::new(fake_wallet),
  132. )
  133. .await
  134. .unwrap();
  135. let mint = mint_builder
  136. .build_with_seed(database.clone(), &mnemonic.to_seed_normalized(""))
  137. .await
  138. .unwrap();
  139. let quote_ttl = QuoteTTL::new(10000, 10000);
  140. mint.set_quote_ttl(quote_ttl).await.unwrap();
  141. // Create a mint quote
  142. let current_time = cdk::util::unix_time();
  143. let mint_quote = MintQuote::new(
  144. None,
  145. "concurrent_test_invoice".to_string(),
  146. CurrencyUnit::Sat,
  147. Some(Amount::from(1000).with_unit(CurrencyUnit::Sat)),
  148. current_time + 3600, // expires in 1 hour
  149. PaymentIdentifier::CustomId("test_lookup_id".to_string()),
  150. None,
  151. Amount::ZERO.with_unit(CurrencyUnit::Sat),
  152. Amount::ZERO.with_unit(CurrencyUnit::Sat),
  153. PaymentMethod::Known(KnownMethod::Bolt11),
  154. current_time,
  155. vec![],
  156. vec![],
  157. None, // extra_json
  158. );
  159. // Add the quote to the database
  160. {
  161. let mut tx = MintDatabase::begin_transaction(&*database).await.unwrap();
  162. tx.add_mint_quote(mint_quote.clone()).await.unwrap();
  163. tx.commit().await.unwrap();
  164. }
  165. // Simulate 10 concurrent payment notifications with the SAME payment_id
  166. let payment_id = "duplicate_payment_test_12345";
  167. let mut join_set = JoinSet::new();
  168. for i in 0..10 {
  169. let db_clone = database.clone();
  170. let quote_id = mint_quote.id.clone();
  171. let payment_id_clone = payment_id.to_string();
  172. join_set.spawn(async move {
  173. let mut tx = MintDatabase::begin_transaction(&*db_clone).await.unwrap();
  174. let mut quote_from_db = tx
  175. .get_mint_quote(&quote_id)
  176. .await
  177. .expect("no error")
  178. .expect("some value");
  179. let result = if let Err(err) = quote_from_db.add_payment(
  180. Amount::from(10).with_unit(CurrencyUnit::Sat),
  181. payment_id_clone,
  182. None,
  183. ) {
  184. Err(err)
  185. } else {
  186. tx.update_mint_quote(&mut quote_from_db)
  187. .await
  188. .map_err(|err| cdk_common::Error::Database(err))
  189. };
  190. if result.is_ok() {
  191. tx.commit().await.unwrap();
  192. }
  193. (i, result)
  194. });
  195. }
  196. // Collect results
  197. let mut success_count = 0;
  198. let mut duplicate_errors = 0;
  199. let mut other_errors = Vec::new();
  200. while let Some(result) = join_set.join_next().await {
  201. let (task_id, db_result) = result.unwrap();
  202. match db_result {
  203. Ok(_) => success_count += 1,
  204. Err(e) => {
  205. let err_str = format!("{:?}", e);
  206. if err_str.contains("Duplicate") {
  207. duplicate_errors += 1;
  208. } else {
  209. other_errors.push((task_id, err_str));
  210. }
  211. }
  212. }
  213. }
  214. // Verify results
  215. assert_eq!(
  216. success_count, 1,
  217. "Exactly one task should successfully process the payment (got {})",
  218. success_count
  219. );
  220. assert!(
  221. other_errors.is_empty(),
  222. "No unexpected errors should occur. Got: {:?}",
  223. other_errors
  224. );
  225. assert_eq!(
  226. duplicate_errors, 9,
  227. "Nine tasks should receive Duplicate error (got {})",
  228. duplicate_errors
  229. );
  230. // Verify the quote was incremented exactly once
  231. let final_quote = MintQuotesDatabase::get_mint_quote(&*database, &mint_quote.id)
  232. .await
  233. .unwrap()
  234. .expect("Quote should exist");
  235. assert_eq!(
  236. final_quote.amount_paid(),
  237. Amount::from(10).with_unit(CurrencyUnit::Sat),
  238. "Quote amount should be incremented exactly once"
  239. );
  240. assert_eq!(
  241. final_quote.payments.len(),
  242. 1,
  243. "Should have exactly one payment recorded"
  244. );
  245. assert_eq!(
  246. final_quote.payments[0].payment_id, payment_id,
  247. "Payment ID should match"
  248. );
  249. }