test_swap_flow.rs 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210
  1. //! Comprehensive tests for the current swap flow
  2. //!
  3. //! These tests validate the swap operation's behavior including:
  4. //! - Happy path: successful token swaps
  5. //! - Error handling: validation failures, rollback scenarios
  6. //! - Edge cases: concurrent operations, double-spending
  7. //! - State management: proof states, blinded message tracking
  8. //!
  9. //! The tests focus on the current implementation using ProofWriter and BlindedMessageWriter
  10. //! patterns to ensure proper cleanup and rollback behavior.
  11. use std::collections::HashMap;
  12. use std::sync::Arc;
  13. use cashu::amount::SplitTarget;
  14. use cashu::dhke::construct_proofs;
  15. use cashu::{CurrencyUnit, Id, PreMintSecrets, SecretKey, SpendingConditions, State, SwapRequest};
  16. use cdk::mint::Mint;
  17. use cdk::nuts::nut00::ProofsMethods;
  18. use cdk::Amount;
  19. use cdk_integration_tests::init_pure_tests::*;
  20. /// Helper to get the active keyset ID from a mint
  21. async fn get_keyset_id(mint: &Mint) -> Id {
  22. let keys = mint.pubkeys().keysets.first().unwrap().clone();
  23. keys.verify_id()
  24. .expect("Keyset ID generation is successful");
  25. keys.id
  26. }
  27. /// Tests the complete happy path of a swap operation:
  28. /// 1. Wallet is funded with tokens
  29. /// 2. Blinded messages are added to database
  30. /// 3. Outputs are signed by mint
  31. /// 4. Input proofs are verified
  32. /// 5. Transaction is balanced
  33. /// 6. Proofs are added and marked as spent
  34. /// 7. Blind signatures are saved
  35. /// All steps should succeed and database should be in consistent state.
  36. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  37. async fn test_swap_happy_path() {
  38. setup_tracing();
  39. let mint = create_and_start_test_mint()
  40. .await
  41. .expect("Failed to create test mint");
  42. let wallet = create_test_wallet_for_mint(mint.clone())
  43. .await
  44. .expect("Failed to create test wallet");
  45. // Fund wallet with 100 sats
  46. fund_wallet(wallet.clone(), 100, None)
  47. .await
  48. .expect("Failed to fund wallet");
  49. let proofs = wallet
  50. .get_unspent_proofs()
  51. .await
  52. .expect("Could not get proofs");
  53. let keyset_id = get_keyset_id(&mint).await;
  54. // Check initial amounts after minting
  55. let total_issued = mint.total_issued().await.unwrap();
  56. let total_redeemed = mint.total_redeemed().await.unwrap();
  57. let initial_issued = total_issued
  58. .get(&keyset_id)
  59. .copied()
  60. .unwrap_or(Amount::ZERO);
  61. let initial_redeemed = total_redeemed
  62. .get(&keyset_id)
  63. .copied()
  64. .unwrap_or(Amount::ZERO);
  65. assert_eq!(
  66. initial_issued,
  67. Amount::from(100),
  68. "Should have issued 100 sats"
  69. );
  70. assert_eq!(
  71. initial_redeemed,
  72. Amount::ZERO,
  73. "Should have redeemed 0 sats initially"
  74. );
  75. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  76. // Create swap request for same amount (100 sats)
  77. let preswap = PreMintSecrets::random(
  78. keyset_id,
  79. 100.into(),
  80. &SplitTarget::default(),
  81. &fee_and_amounts,
  82. )
  83. .expect("Failed to create preswap");
  84. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  85. // Execute swap
  86. let swap_response = mint
  87. .process_swap_request(swap_request)
  88. .await
  89. .expect("Swap should succeed");
  90. // Verify response contains correct number of signatures
  91. assert_eq!(
  92. swap_response.signatures.len(),
  93. preswap.blinded_messages().len(),
  94. "Should receive signature for each blinded message"
  95. );
  96. // Verify input proofs are marked as spent
  97. let states = mint
  98. .localstore()
  99. .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
  100. .await
  101. .expect("Failed to get proof states");
  102. for state in states {
  103. assert_eq!(
  104. State::Spent,
  105. state.expect("State should be known"),
  106. "All input proofs should be marked as spent"
  107. );
  108. }
  109. // Verify blind signatures were saved
  110. let saved_signatures = mint
  111. .localstore()
  112. .get_blind_signatures(
  113. &preswap
  114. .blinded_messages()
  115. .iter()
  116. .map(|bm| bm.blinded_secret)
  117. .collect::<Vec<_>>(),
  118. )
  119. .await
  120. .expect("Failed to get blind signatures");
  121. assert_eq!(
  122. saved_signatures.len(),
  123. swap_response.signatures.len(),
  124. "All signatures should be saved"
  125. );
  126. // Check keyset amounts after swap
  127. // Swap redeems old proofs (100 sats) and issues new proofs (100 sats)
  128. let total_issued = mint.total_issued().await.unwrap();
  129. let total_redeemed = mint.total_redeemed().await.unwrap();
  130. let after_issued = total_issued
  131. .get(&keyset_id)
  132. .copied()
  133. .unwrap_or(Amount::ZERO);
  134. let after_redeemed = total_redeemed
  135. .get(&keyset_id)
  136. .copied()
  137. .unwrap_or(Amount::ZERO);
  138. assert_eq!(
  139. after_issued,
  140. Amount::from(200),
  141. "Should have issued 200 sats total (initial 100 + swap 100)"
  142. );
  143. assert_eq!(
  144. after_redeemed,
  145. Amount::from(100),
  146. "Should have redeemed 100 sats from the swap"
  147. );
  148. }
  149. /// Tests that duplicate blinded messages are rejected:
  150. /// 1. First swap with blinded messages succeeds
  151. /// 2. Second swap attempt with same blinded messages fails
  152. /// 3. BlindedMessageWriter should prevent reuse
  153. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  154. async fn test_swap_duplicate_blinded_messages() {
  155. setup_tracing();
  156. let mint = create_and_start_test_mint()
  157. .await
  158. .expect("Failed to create test mint");
  159. let wallet = create_test_wallet_for_mint(mint.clone())
  160. .await
  161. .expect("Failed to create test wallet");
  162. // Fund wallet with 200 sats (enough for two swaps)
  163. fund_wallet(wallet.clone(), 200, None)
  164. .await
  165. .expect("Failed to fund wallet");
  166. let all_proofs = wallet
  167. .get_unspent_proofs()
  168. .await
  169. .expect("Could not get proofs");
  170. // Split proofs into two sets
  171. let mid = all_proofs.len() / 2;
  172. let proofs1: Vec<_> = all_proofs.iter().take(mid).cloned().collect();
  173. let proofs2: Vec<_> = all_proofs.iter().skip(mid).cloned().collect();
  174. let keyset_id = get_keyset_id(&mint).await;
  175. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  176. // Create blinded messages for first swap
  177. let preswap = PreMintSecrets::random(
  178. keyset_id,
  179. proofs1.total_amount().unwrap(),
  180. &SplitTarget::default(),
  181. &fee_and_amounts,
  182. )
  183. .expect("Failed to create preswap");
  184. let blinded_messages = preswap.blinded_messages();
  185. // First swap should succeed
  186. let swap_request1 = SwapRequest::new(proofs1, blinded_messages.clone());
  187. mint.process_swap_request(swap_request1)
  188. .await
  189. .expect("First swap should succeed");
  190. // Second swap with SAME blinded messages should fail
  191. let swap_request2 = SwapRequest::new(proofs2, blinded_messages.clone());
  192. let result = mint.process_swap_request(swap_request2).await;
  193. assert!(
  194. result.is_err(),
  195. "Second swap with duplicate blinded messages should fail"
  196. );
  197. }
  198. /// Tests that swap correctly rejects double-spending attempts:
  199. /// 1. First swap with proofs succeeds
  200. /// 2. Second swap with same proofs fails with TokenAlreadySpent
  201. /// 3. ProofWriter should detect already-spent proofs
  202. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  203. async fn test_swap_double_spend_detection() {
  204. setup_tracing();
  205. let mint = create_and_start_test_mint()
  206. .await
  207. .expect("Failed to create test mint");
  208. let wallet = create_test_wallet_for_mint(mint.clone())
  209. .await
  210. .expect("Failed to create test wallet");
  211. // Fund wallet with 100 sats
  212. fund_wallet(wallet.clone(), 100, None)
  213. .await
  214. .expect("Failed to fund wallet");
  215. let proofs = wallet
  216. .get_unspent_proofs()
  217. .await
  218. .expect("Could not get proofs");
  219. let keyset_id = get_keyset_id(&mint).await;
  220. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  221. // First swap
  222. let preswap1 = PreMintSecrets::random(
  223. keyset_id,
  224. 100.into(),
  225. &SplitTarget::default(),
  226. &fee_and_amounts,
  227. )
  228. .expect("Failed to create preswap");
  229. let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
  230. mint.process_swap_request(swap_request1)
  231. .await
  232. .expect("First swap should succeed");
  233. // Second swap with same proofs should fail
  234. let preswap2 = PreMintSecrets::random(
  235. keyset_id,
  236. 100.into(),
  237. &SplitTarget::default(),
  238. &fee_and_amounts,
  239. )
  240. .expect("Failed to create preswap");
  241. let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
  242. let result = mint.process_swap_request(swap_request2).await;
  243. match result {
  244. Err(cdk::Error::TokenAlreadySpent) => {
  245. // Expected error
  246. }
  247. Err(err) => panic!("Wrong error type: {:?}", err),
  248. Ok(_) => panic!("Double spend should not succeed"),
  249. }
  250. }
  251. /// Tests that unbalanced swap requests are rejected:
  252. /// Case 1: Output amount < Input amount (trying to steal from mint)
  253. /// Case 2: Output amount > Input amount (trying to create tokens)
  254. /// Both should fail with TransactionUnbalanced error.
  255. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  256. async fn test_swap_unbalanced_transaction_detection() {
  257. setup_tracing();
  258. let mint = create_and_start_test_mint()
  259. .await
  260. .expect("Failed to create test mint");
  261. let wallet = create_test_wallet_for_mint(mint.clone())
  262. .await
  263. .expect("Failed to create test wallet");
  264. // Fund wallet with 100 sats
  265. fund_wallet(wallet.clone(), 100, None)
  266. .await
  267. .expect("Failed to fund wallet");
  268. let proofs = wallet
  269. .get_unspent_proofs()
  270. .await
  271. .expect("Could not get proofs");
  272. let keyset_id = get_keyset_id(&mint).await;
  273. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  274. // Case 1: Try to swap for LESS (95 < 100) - underpaying
  275. let preswap_less = PreMintSecrets::random(
  276. keyset_id,
  277. 95.into(),
  278. &SplitTarget::default(),
  279. &fee_and_amounts,
  280. )
  281. .expect("Failed to create preswap");
  282. let swap_request_less = SwapRequest::new(proofs.clone(), preswap_less.blinded_messages());
  283. match mint.process_swap_request(swap_request_less).await {
  284. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  285. // Expected error
  286. }
  287. Err(err) => panic!("Wrong error type for underpay: {:?}", err),
  288. Ok(_) => panic!("Unbalanced swap (underpay) should not succeed"),
  289. }
  290. // Case 2: Try to swap for MORE (105 > 100) - overpaying/creating tokens
  291. let preswap_more = PreMintSecrets::random(
  292. keyset_id,
  293. 105.into(),
  294. &SplitTarget::default(),
  295. &fee_and_amounts,
  296. )
  297. .expect("Failed to create preswap");
  298. let swap_request_more = SwapRequest::new(proofs.clone(), preswap_more.blinded_messages());
  299. match mint.process_swap_request(swap_request_more).await {
  300. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  301. // Expected error
  302. }
  303. Err(err) => panic!("Wrong error type for overpay: {:?}", err),
  304. Ok(_) => panic!("Unbalanced swap (overpay) should not succeed"),
  305. }
  306. }
  307. /// Tests P2PK (Pay-to-Public-Key) spending conditions:
  308. /// 1. Create proofs locked to a public key
  309. /// 2. Attempt swap without signature - should fail
  310. /// 3. Attempt swap with valid signature - should succeed
  311. /// Validates NUT-11 signature enforcement.
  312. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  313. async fn test_swap_p2pk_signature_validation() {
  314. setup_tracing();
  315. let mint = create_and_start_test_mint()
  316. .await
  317. .expect("Failed to create test mint");
  318. let wallet = create_test_wallet_for_mint(mint.clone())
  319. .await
  320. .expect("Failed to create test wallet");
  321. // Fund wallet with 100 sats
  322. fund_wallet(wallet.clone(), 100, None)
  323. .await
  324. .expect("Failed to fund wallet");
  325. let input_proofs = wallet
  326. .get_unspent_proofs()
  327. .await
  328. .expect("Could not get proofs");
  329. let keyset_id = get_keyset_id(&mint).await;
  330. let secret_key = SecretKey::generate();
  331. // Create P2PK locked outputs
  332. let spending_conditions = SpendingConditions::new_p2pk(secret_key.public_key(), None);
  333. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  334. let pre_swap = PreMintSecrets::with_conditions(
  335. keyset_id,
  336. 100.into(),
  337. &SplitTarget::default(),
  338. &spending_conditions,
  339. &fee_and_amounts,
  340. )
  341. .expect("Failed to create P2PK preswap");
  342. let swap_request = SwapRequest::new(input_proofs.clone(), pre_swap.blinded_messages());
  343. // First swap to get P2PK locked proofs
  344. let keys = mint.pubkeys().keysets.first().cloned().unwrap().keys;
  345. let post_swap = mint
  346. .process_swap_request(swap_request)
  347. .await
  348. .expect("Initial swap should succeed");
  349. // Construct proofs from swap response
  350. let mut p2pk_proofs = construct_proofs(
  351. post_swap.signatures,
  352. pre_swap.rs(),
  353. pre_swap.secrets(),
  354. &keys,
  355. )
  356. .expect("Failed to construct proofs");
  357. // Try to spend P2PK proofs WITHOUT signature - should fail
  358. let preswap_unsigned = PreMintSecrets::random(
  359. keyset_id,
  360. 100.into(),
  361. &SplitTarget::default(),
  362. &fee_and_amounts,
  363. )
  364. .expect("Failed to create preswap");
  365. let swap_request_unsigned =
  366. SwapRequest::new(p2pk_proofs.clone(), preswap_unsigned.blinded_messages());
  367. match mint.process_swap_request(swap_request_unsigned).await {
  368. Err(cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided)) => {
  369. // Expected error
  370. }
  371. Err(err) => panic!("Wrong error type: {:?}", err),
  372. Ok(_) => panic!("Unsigned P2PK spend should fail"),
  373. }
  374. // Sign the proofs with correct key
  375. for proof in &mut p2pk_proofs {
  376. proof
  377. .sign_p2pk(secret_key.clone())
  378. .expect("Failed to sign proof");
  379. }
  380. // Try again WITH signature - should succeed
  381. let preswap_signed = PreMintSecrets::random(
  382. keyset_id,
  383. 100.into(),
  384. &SplitTarget::default(),
  385. &fee_and_amounts,
  386. )
  387. .expect("Failed to create preswap");
  388. let swap_request_signed = SwapRequest::new(p2pk_proofs, preswap_signed.blinded_messages());
  389. mint.process_swap_request(swap_request_signed)
  390. .await
  391. .expect("Signed P2PK spend should succeed");
  392. }
  393. /// Tests rollback behavior when duplicate blinded messages are used:
  394. /// This validates that the BlindedMessageWriter prevents reuse of blinded messages.
  395. /// 1. First swap with blinded messages succeeds
  396. /// 2. Second swap with same blinded messages fails
  397. /// 3. The failure should happen early (during blinded message addition)
  398. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  399. async fn test_swap_rollback_on_duplicate_blinded_message() {
  400. setup_tracing();
  401. let mint = create_and_start_test_mint()
  402. .await
  403. .expect("Failed to create test mint");
  404. let wallet = create_test_wallet_for_mint(mint.clone())
  405. .await
  406. .expect("Failed to create test wallet");
  407. // Fund with enough for multiple swaps
  408. fund_wallet(wallet.clone(), 200, None)
  409. .await
  410. .expect("Failed to fund wallet");
  411. let all_proofs = wallet
  412. .get_unspent_proofs()
  413. .await
  414. .expect("Could not get proofs");
  415. let mid = all_proofs.len() / 2;
  416. let proofs1: Vec<_> = all_proofs.iter().take(mid).cloned().collect();
  417. let proofs2: Vec<_> = all_proofs.iter().skip(mid).cloned().collect();
  418. let keyset_id = get_keyset_id(&mint).await;
  419. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  420. // Create shared blinded messages
  421. let preswap = PreMintSecrets::random(
  422. keyset_id,
  423. proofs1.total_amount().unwrap(),
  424. &SplitTarget::default(),
  425. &fee_and_amounts,
  426. )
  427. .expect("Failed to create preswap");
  428. let blinded_messages = preswap.blinded_messages();
  429. // Extract proof2 ys before moving proofs2
  430. let proof2_ys: Vec<_> = proofs2.iter().map(|p| p.y().unwrap()).collect();
  431. // First swap succeeds
  432. let swap1 = SwapRequest::new(proofs1, blinded_messages.clone());
  433. mint.process_swap_request(swap1)
  434. .await
  435. .expect("First swap should succeed");
  436. // Second swap with duplicate blinded messages should fail early
  437. // The BlindedMessageWriter should detect duplicate and prevent the swap
  438. let swap2 = SwapRequest::new(proofs2, blinded_messages.clone());
  439. let result = mint.process_swap_request(swap2).await;
  440. assert!(
  441. result.is_err(),
  442. "Duplicate blinded messages should cause failure"
  443. );
  444. // Verify the second set of proofs are NOT marked as spent
  445. // (since the swap failed before processing them)
  446. let states = mint
  447. .localstore()
  448. .get_proofs_states(&proof2_ys)
  449. .await
  450. .expect("Failed to get proof states");
  451. for state in states {
  452. assert!(
  453. state.is_none(),
  454. "Proofs from failed swap should not be marked as spent"
  455. );
  456. }
  457. }
  458. /// Tests concurrent swap attempts with same proofs:
  459. /// Spawns 3 concurrent tasks trying to swap the same proofs.
  460. /// Only one should succeed, others should fail with TokenAlreadySpent or TokenPending.
  461. /// Validates that concurrent access is properly handled.
  462. #[tokio::test(flavor = "multi_thread", worker_threads = 3)]
  463. async fn test_swap_concurrent_double_spend_prevention() {
  464. setup_tracing();
  465. let mint = create_and_start_test_mint()
  466. .await
  467. .expect("Failed to create test mint");
  468. let wallet = create_test_wallet_for_mint(mint.clone())
  469. .await
  470. .expect("Failed to create test wallet");
  471. // Fund wallet
  472. fund_wallet(wallet.clone(), 100, None)
  473. .await
  474. .expect("Failed to fund wallet");
  475. let proofs = wallet
  476. .get_unspent_proofs()
  477. .await
  478. .expect("Could not get proofs");
  479. let keyset_id = get_keyset_id(&mint).await;
  480. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  481. // Create 3 different swap requests with SAME proofs but different outputs
  482. let preswap1 = PreMintSecrets::random(
  483. keyset_id,
  484. 100.into(),
  485. &SplitTarget::default(),
  486. &fee_and_amounts,
  487. )
  488. .expect("Failed to create preswap 1");
  489. let preswap2 = PreMintSecrets::random(
  490. keyset_id,
  491. 100.into(),
  492. &SplitTarget::default(),
  493. &fee_and_amounts,
  494. )
  495. .expect("Failed to create preswap 2");
  496. let preswap3 = PreMintSecrets::random(
  497. keyset_id,
  498. 100.into(),
  499. &SplitTarget::default(),
  500. &fee_and_amounts,
  501. )
  502. .expect("Failed to create preswap 3");
  503. let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
  504. let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
  505. let swap_request3 = SwapRequest::new(proofs.clone(), preswap3.blinded_messages());
  506. // Spawn concurrent tasks
  507. let mint1 = mint.clone();
  508. let mint2 = mint.clone();
  509. let mint3 = mint.clone();
  510. let task1 = tokio::spawn(async move { mint1.process_swap_request(swap_request1).await });
  511. let task2 = tokio::spawn(async move { mint2.process_swap_request(swap_request2).await });
  512. let task3 = tokio::spawn(async move { mint3.process_swap_request(swap_request3).await });
  513. // Wait for all tasks
  514. let results = tokio::try_join!(task1, task2, task3).expect("Tasks should complete");
  515. // Count successes and failures
  516. let mut success_count = 0;
  517. let mut failure_count = 0;
  518. for result in [results.0, results.1, results.2] {
  519. match result {
  520. Ok(_) => success_count += 1,
  521. Err(cdk::Error::TokenAlreadySpent) | Err(cdk::Error::TokenPending) => {
  522. failure_count += 1
  523. }
  524. Err(err) => panic!("Unexpected error: {:?}", err),
  525. }
  526. }
  527. assert_eq!(
  528. success_count, 1,
  529. "Exactly one swap should succeed in concurrent scenario"
  530. );
  531. assert_eq!(
  532. failure_count, 2,
  533. "Exactly two swaps should fail in concurrent scenario"
  534. );
  535. // Verify all proofs are marked as spent
  536. let states = mint
  537. .localstore()
  538. .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
  539. .await
  540. .expect("Failed to get proof states");
  541. for state in states {
  542. assert_eq!(
  543. State::Spent,
  544. state.expect("State should be known"),
  545. "All proofs should be marked as spent after concurrent attempts"
  546. );
  547. }
  548. }
  549. /// Tests swap with fees enabled:
  550. /// 1. Create mint with keyset that has fees (1 sat per proof)
  551. /// 2. Fund wallet with many small proofs
  552. /// 3. Attempt swap without paying fee - should fail
  553. /// 4. Attempt swap with correct fee deduction - should succeed
  554. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  555. async fn test_swap_with_fees() {
  556. setup_tracing();
  557. let mint = create_and_start_test_mint()
  558. .await
  559. .expect("Failed to create test mint");
  560. let wallet = create_test_wallet_for_mint(mint.clone())
  561. .await
  562. .expect("Failed to create test wallet");
  563. // Rotate to keyset with 1 sat per proof fee
  564. mint.rotate_keyset(
  565. CurrencyUnit::Sat,
  566. cdk_integration_tests::standard_keyset_amounts(32),
  567. 1,
  568. )
  569. .await
  570. .expect("Failed to rotate keyset");
  571. // Fund with 1000 sats as individual 1-sat proofs using the fee-based keyset
  572. // Wait a bit for keyset to be available
  573. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  574. fund_wallet(wallet.clone(), 1000, Some(SplitTarget::Value(Amount::ONE)))
  575. .await
  576. .expect("Failed to fund wallet");
  577. let proofs = wallet
  578. .get_unspent_proofs()
  579. .await
  580. .expect("Could not get proofs");
  581. // Take 100 proofs (100 sats total, will need to pay fee)
  582. let hundred_proofs: Vec<_> = proofs.iter().take(100).cloned().collect();
  583. // Get the keyset ID from the proofs (which will be the fee-based keyset)
  584. let keyset_id = hundred_proofs[0].keyset_id;
  585. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  586. // Try to swap for 100 outputs (same as input) - should fail due to unpaid fee
  587. let preswap_no_fee = PreMintSecrets::random(
  588. keyset_id,
  589. 100.into(),
  590. &SplitTarget::default(),
  591. &fee_and_amounts,
  592. )
  593. .expect("Failed to create preswap");
  594. let swap_no_fee = SwapRequest::new(hundred_proofs.clone(), preswap_no_fee.blinded_messages());
  595. match mint.process_swap_request(swap_no_fee).await {
  596. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  597. // Expected - didn't pay the fee
  598. }
  599. Err(err) => panic!("Wrong error type: {:?}", err),
  600. Ok(_) => panic!("Should fail when fee not paid"),
  601. }
  602. // Calculate correct fee (1 sat per input proof in this keyset)
  603. let fee = hundred_proofs.len() as u64; // 1 sat per proof = 100 sats fee
  604. let output_amount = 100 - fee;
  605. // Swap with correct fee deduction - should succeed if output_amount > 0
  606. if output_amount > 0 {
  607. let preswap_with_fee = PreMintSecrets::random(
  608. keyset_id,
  609. output_amount.into(),
  610. &SplitTarget::default(),
  611. &fee_and_amounts,
  612. )
  613. .expect("Failed to create preswap with fee");
  614. let swap_with_fee =
  615. SwapRequest::new(hundred_proofs.clone(), preswap_with_fee.blinded_messages());
  616. mint.process_swap_request(swap_with_fee)
  617. .await
  618. .expect("Swap with correct fee should succeed");
  619. }
  620. }
  621. /// Tests that swap correctly handles amount overflow:
  622. /// Attempts to create outputs that would overflow u64 when summed.
  623. /// This should be rejected before any database operations occur.
  624. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  625. async fn test_swap_amount_overflow_protection() {
  626. setup_tracing();
  627. let mint = create_and_start_test_mint()
  628. .await
  629. .expect("Failed to create test mint");
  630. let wallet = create_test_wallet_for_mint(mint.clone())
  631. .await
  632. .expect("Failed to create test wallet");
  633. // Fund wallet
  634. fund_wallet(wallet.clone(), 100, None)
  635. .await
  636. .expect("Failed to fund wallet");
  637. let proofs = wallet
  638. .get_unspent_proofs()
  639. .await
  640. .expect("Could not get proofs");
  641. let keyset_id = get_keyset_id(&mint).await;
  642. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  643. // Try to create outputs that would overflow
  644. // 2^63 + 2^63 + small amount would overflow u64
  645. let large_amount = 2_u64.pow(63);
  646. let pre_mint1 = PreMintSecrets::random(
  647. keyset_id,
  648. large_amount.into(),
  649. &SplitTarget::default(),
  650. &fee_and_amounts,
  651. )
  652. .expect("Failed to create pre_mint1");
  653. let pre_mint2 = PreMintSecrets::random(
  654. keyset_id,
  655. large_amount.into(),
  656. &SplitTarget::default(),
  657. &fee_and_amounts,
  658. )
  659. .expect("Failed to create pre_mint2");
  660. let mut combined_pre_mint = PreMintSecrets::random(
  661. keyset_id,
  662. 1.into(),
  663. &SplitTarget::default(),
  664. &fee_and_amounts,
  665. )
  666. .expect("Failed to create combined_pre_mint");
  667. combined_pre_mint.combine(pre_mint1);
  668. combined_pre_mint.combine(pre_mint2);
  669. let swap_request = SwapRequest::new(proofs, combined_pre_mint.blinded_messages());
  670. // Should fail with overflow/amount error
  671. match mint.process_swap_request(swap_request).await {
  672. Err(cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)))
  673. | Err(cdk::Error::AmountOverflow)
  674. | Err(cdk::Error::AmountError(_))
  675. | Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  676. // Any of these errors are acceptable for overflow
  677. }
  678. Err(err) => panic!("Unexpected error type: {:?}", err),
  679. Ok(_) => panic!("Overflow swap should not succeed"),
  680. }
  681. }
  682. /// Tests swap state transitions through pubsub notifications:
  683. /// 1. Subscribe to proof state changes
  684. /// 2. Execute swap
  685. /// 3. Verify Pending then Spent state transitions are received
  686. /// Validates NUT-17 notification behavior.
  687. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  688. async fn test_swap_state_transition_notifications() {
  689. setup_tracing();
  690. let mint = create_and_start_test_mint()
  691. .await
  692. .expect("Failed to create test mint");
  693. let wallet = create_test_wallet_for_mint(mint.clone())
  694. .await
  695. .expect("Failed to create test wallet");
  696. // Fund wallet
  697. fund_wallet(wallet.clone(), 100, None)
  698. .await
  699. .expect("Failed to fund wallet");
  700. let proofs = wallet
  701. .get_unspent_proofs()
  702. .await
  703. .expect("Could not get proofs");
  704. let keyset_id = get_keyset_id(&mint).await;
  705. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  706. let preswap = PreMintSecrets::random(
  707. keyset_id,
  708. 100.into(),
  709. &SplitTarget::default(),
  710. &fee_and_amounts,
  711. )
  712. .expect("Failed to create preswap");
  713. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  714. // Subscribe to proof state changes
  715. let proof_ys: Vec<String> = proofs.iter().map(|p| p.y().unwrap().to_string()).collect();
  716. let mut listener = mint
  717. .pubsub_manager()
  718. .subscribe(cdk::subscription::Params {
  719. kind: cdk::nuts::nut17::Kind::ProofState,
  720. filters: proof_ys.clone(),
  721. id: Arc::new("test_swap_notifications".into()),
  722. })
  723. .expect("Should subscribe successfully");
  724. // Execute swap
  725. mint.process_swap_request(swap_request)
  726. .await
  727. .expect("Swap should succeed");
  728. // Give pubsub time to deliver messages
  729. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  730. // Collect all state transition notifications
  731. let mut state_transitions: HashMap<String, Vec<State>> = HashMap::new();
  732. while let Some(msg) = listener.try_recv() {
  733. match msg.into_inner() {
  734. cashu::NotificationPayload::ProofState(cashu::ProofState { y, state, .. }) => {
  735. state_transitions
  736. .entry(y.to_string())
  737. .or_default()
  738. .push(state);
  739. }
  740. _ => panic!("Unexpected notification type"),
  741. }
  742. }
  743. // Verify each proof went through Pending -> Spent transition
  744. for y in proof_ys {
  745. let transitions = state_transitions
  746. .get(&y)
  747. .expect("Should have transitions for proof");
  748. assert_eq!(
  749. transitions,
  750. &vec![State::Pending, State::Spent],
  751. "Proof should transition from Pending to Spent"
  752. );
  753. }
  754. }
  755. /// Tests that swap fails gracefully when proof states cannot be updated:
  756. /// This would test the rollback path where proofs are added but state update fails.
  757. /// In the current implementation, this should trigger rollback of both proofs and blinded messages.
  758. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  759. async fn test_swap_proof_state_consistency() {
  760. setup_tracing();
  761. let mint = create_and_start_test_mint()
  762. .await
  763. .expect("Failed to create test mint");
  764. let wallet = create_test_wallet_for_mint(mint.clone())
  765. .await
  766. .expect("Failed to create test wallet");
  767. // Fund wallet
  768. fund_wallet(wallet.clone(), 100, None)
  769. .await
  770. .expect("Failed to fund wallet");
  771. let proofs = wallet
  772. .get_unspent_proofs()
  773. .await
  774. .expect("Could not get proofs");
  775. let keyset_id = get_keyset_id(&mint).await;
  776. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  777. // Execute successful swap
  778. let preswap = PreMintSecrets::random(
  779. keyset_id,
  780. 100.into(),
  781. &SplitTarget::default(),
  782. &fee_and_amounts,
  783. )
  784. .expect("Failed to create preswap");
  785. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  786. mint.process_swap_request(swap_request)
  787. .await
  788. .expect("Swap should succeed");
  789. // Verify all proofs have consistent state (Spent)
  790. let proof_ys: Vec<_> = proofs.iter().map(|p| p.y().unwrap()).collect();
  791. let states = mint
  792. .localstore()
  793. .get_proofs_states(&proof_ys)
  794. .await
  795. .expect("Failed to get proof states");
  796. // All states should be Some(Spent) - none should be None or Pending
  797. for (i, state) in states.iter().enumerate() {
  798. match state {
  799. Some(State::Spent) => {
  800. // Expected state
  801. }
  802. Some(other_state) => {
  803. panic!("Proof {} in unexpected state: {:?}", i, other_state)
  804. }
  805. None => {
  806. panic!("Proof {} has no state (should be Spent)", i)
  807. }
  808. }
  809. }
  810. }
  811. /// Tests that wallet correctly increments keyset counters when receiving proofs
  812. /// from multiple keysets and then performing operations with them.
  813. ///
  814. /// This test validates:
  815. /// 1. Wallet can receive proofs from multiple different keysets
  816. /// 2. Counter is correctly incremented for the target keyset during swap
  817. /// 3. Database maintains separate counters for each keyset
  818. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  819. async fn test_wallet_multi_keyset_counter_updates() {
  820. setup_tracing();
  821. let mint = create_and_start_test_mint()
  822. .await
  823. .expect("Failed to create test mint");
  824. let wallet = create_test_wallet_for_mint(mint.clone())
  825. .await
  826. .expect("Failed to create test wallet");
  827. // Fund wallet with initial 100 sats using first keyset
  828. fund_wallet(wallet.clone(), 100, None)
  829. .await
  830. .expect("Failed to fund wallet");
  831. let first_keyset_id = get_keyset_id(&mint).await;
  832. // Rotate to a second keyset
  833. mint.rotate_keyset(
  834. CurrencyUnit::Sat,
  835. cdk_integration_tests::standard_keyset_amounts(32),
  836. 0,
  837. )
  838. .await
  839. .expect("Failed to rotate keyset");
  840. // Wait for keyset rotation to propagate
  841. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  842. // Refresh wallet keysets to know about the new keyset
  843. wallet
  844. .refresh_keysets()
  845. .await
  846. .expect("Failed to refresh wallet keysets");
  847. // Fund wallet again with 100 sats using second keyset
  848. fund_wallet(wallet.clone(), 100, None)
  849. .await
  850. .expect("Failed to fund wallet with second keyset");
  851. let second_keyset_id = mint
  852. .pubkeys()
  853. .keysets
  854. .iter()
  855. .find(|k| k.id != first_keyset_id)
  856. .expect("Should have second keyset")
  857. .id;
  858. // Verify we now have proofs from two different keysets
  859. let all_proofs = wallet
  860. .get_unspent_proofs()
  861. .await
  862. .expect("Could not get proofs");
  863. let keysets_in_use: std::collections::HashSet<_> =
  864. all_proofs.iter().map(|p| p.keyset_id).collect();
  865. assert_eq!(
  866. keysets_in_use.len(),
  867. 2,
  868. "Should have proofs from 2 different keysets"
  869. );
  870. assert!(
  871. keysets_in_use.contains(&first_keyset_id),
  872. "Should have proofs from first keyset"
  873. );
  874. assert!(
  875. keysets_in_use.contains(&second_keyset_id),
  876. "Should have proofs from second keyset"
  877. );
  878. // Get initial total issued and redeemed for both keysets before swap
  879. let total_issued_before = mint.total_issued().await.unwrap();
  880. let total_redeemed_before = mint.total_redeemed().await.unwrap();
  881. let first_keyset_issued_before = total_issued_before
  882. .get(&first_keyset_id)
  883. .copied()
  884. .unwrap_or(Amount::ZERO);
  885. let first_keyset_redeemed_before = total_redeemed_before
  886. .get(&first_keyset_id)
  887. .copied()
  888. .unwrap_or(Amount::ZERO);
  889. let second_keyset_issued_before = total_issued_before
  890. .get(&second_keyset_id)
  891. .copied()
  892. .unwrap_or(Amount::ZERO);
  893. let second_keyset_redeemed_before = total_redeemed_before
  894. .get(&second_keyset_id)
  895. .copied()
  896. .unwrap_or(Amount::ZERO);
  897. tracing::info!(
  898. "Before swap - First keyset: issued={}, redeemed={}",
  899. first_keyset_issued_before,
  900. first_keyset_redeemed_before
  901. );
  902. tracing::info!(
  903. "Before swap - Second keyset: issued={}, redeemed={}",
  904. second_keyset_issued_before,
  905. second_keyset_redeemed_before
  906. );
  907. // Both keysets should have issued 100 sats
  908. assert_eq!(
  909. first_keyset_issued_before,
  910. Amount::from(100),
  911. "First keyset should have issued 100 sats"
  912. );
  913. assert_eq!(
  914. second_keyset_issued_before,
  915. Amount::from(100),
  916. "Second keyset should have issued 100 sats"
  917. );
  918. // Neither should have redeemed anything yet
  919. assert_eq!(
  920. first_keyset_redeemed_before,
  921. Amount::ZERO,
  922. "First keyset should have redeemed 0 sats before swap"
  923. );
  924. assert_eq!(
  925. second_keyset_redeemed_before,
  926. Amount::ZERO,
  927. "Second keyset should have redeemed 0 sats before swap"
  928. );
  929. // Now perform a swap with all proofs - this should only increment the counter
  930. // for the active (second) keyset, not for the first keyset
  931. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  932. let total_amount = all_proofs.total_amount().expect("Should get total amount");
  933. // Create swap using the active (second) keyset
  934. let preswap = PreMintSecrets::random(
  935. second_keyset_id,
  936. total_amount,
  937. &SplitTarget::default(),
  938. &fee_and_amounts,
  939. )
  940. .expect("Failed to create preswap");
  941. let swap_request = SwapRequest::new(all_proofs.clone(), preswap.blinded_messages());
  942. // Execute the swap
  943. let swap_response = mint
  944. .process_swap_request(swap_request)
  945. .await
  946. .expect("Swap should succeed");
  947. // Verify response
  948. assert_eq!(
  949. swap_response.signatures.len(),
  950. preswap.blinded_messages().len(),
  951. "Should receive signature for each blinded message"
  952. );
  953. // All the new proofs should be from the second (active) keyset
  954. let keys = mint
  955. .pubkeys()
  956. .keysets
  957. .iter()
  958. .find(|k| k.id == second_keyset_id)
  959. .expect("Should find second keyset")
  960. .keys
  961. .clone();
  962. let new_proofs = construct_proofs(
  963. swap_response.signatures,
  964. preswap.rs(),
  965. preswap.secrets(),
  966. &keys,
  967. )
  968. .expect("Failed to construct proofs");
  969. // Verify all new proofs use the second keyset
  970. for proof in &new_proofs {
  971. assert_eq!(
  972. proof.keyset_id, second_keyset_id,
  973. "All new proofs should use the active (second) keyset"
  974. );
  975. }
  976. // Verify total issued and redeemed after swap
  977. let total_issued_after = mint.total_issued().await.unwrap();
  978. let total_redeemed_after = mint.total_redeemed().await.unwrap();
  979. let first_keyset_issued_after = total_issued_after
  980. .get(&first_keyset_id)
  981. .copied()
  982. .unwrap_or(Amount::ZERO);
  983. let first_keyset_redeemed_after = total_redeemed_after
  984. .get(&first_keyset_id)
  985. .copied()
  986. .unwrap_or(Amount::ZERO);
  987. let second_keyset_issued_after = total_issued_after
  988. .get(&second_keyset_id)
  989. .copied()
  990. .unwrap_or(Amount::ZERO);
  991. let second_keyset_redeemed_after = total_redeemed_after
  992. .get(&second_keyset_id)
  993. .copied()
  994. .unwrap_or(Amount::ZERO);
  995. tracing::info!(
  996. "After swap - First keyset: issued={}, redeemed={}",
  997. first_keyset_issued_after,
  998. first_keyset_redeemed_after
  999. );
  1000. tracing::info!(
  1001. "After swap - Second keyset: issued={}, redeemed={}",
  1002. second_keyset_issued_after,
  1003. second_keyset_redeemed_after
  1004. );
  1005. // After swap:
  1006. // - First keyset: issued stays 100, redeemed increases by 100 (all its proofs were spent in swap)
  1007. // - Second keyset: issued increases by 200 (original 100 + new 100 from swap output),
  1008. // redeemed increases by 100 (its proofs from first funding were spent)
  1009. assert_eq!(
  1010. first_keyset_issued_after,
  1011. Amount::from(100),
  1012. "First keyset issued should stay 100 sats (no new issuance)"
  1013. );
  1014. assert_eq!(
  1015. first_keyset_redeemed_after,
  1016. Amount::from(100),
  1017. "First keyset should have redeemed 100 sats (all its proofs spent in swap)"
  1018. );
  1019. assert_eq!(
  1020. second_keyset_issued_after,
  1021. Amount::from(300),
  1022. "Second keyset should have issued 300 sats total (100 initial + 100 the second funding + 100 from swap output from the old keyset)"
  1023. );
  1024. assert_eq!(
  1025. second_keyset_redeemed_after,
  1026. Amount::from(100),
  1027. "Second keyset should have redeemed 100 sats (its proofs from initial funding spent in swap)"
  1028. );
  1029. // The test verifies that:
  1030. // 1. We can have proofs from multiple keysets in a wallet
  1031. // 2. Swap operation processes inputs from any keyset but creates outputs using active keyset
  1032. // 3. The keyset_counter table correctly handles counters for different keysets independently
  1033. // 4. The database upsert logic in increment_keyset_counter works for multiple keysets
  1034. // 5. Total issued and redeemed are tracked correctly per keyset during multi-keyset swaps
  1035. }