mint.rs 8.0 KB

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