test_swap_flow.rs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950
  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_common::database::mint::{ProofsDatabase, SignaturesDatabase};
  20. use cdk_integration_tests::init_pure_tests::*;
  21. /// Helper to get the active keyset ID from a mint
  22. async fn get_keyset_id(mint: &Mint) -> Id {
  23. let keys = mint.pubkeys().keysets.first().unwrap().clone();
  24. keys.verify_id()
  25. .expect("Keyset ID generation is successful");
  26. keys.id
  27. }
  28. /// Tests the complete happy path of a swap operation:
  29. /// 1. Wallet is funded with tokens
  30. /// 2. Blinded messages are added to database
  31. /// 3. Outputs are signed by mint
  32. /// 4. Input proofs are verified
  33. /// 5. Transaction is balanced
  34. /// 6. Proofs are added and marked as spent
  35. /// 7. Blind signatures are saved
  36. /// All steps should succeed and database should be in consistent state.
  37. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  38. async fn test_swap_happy_path() {
  39. setup_tracing();
  40. let mint = create_and_start_test_mint()
  41. .await
  42. .expect("Failed to create test mint");
  43. let wallet = create_test_wallet_for_mint(mint.clone())
  44. .await
  45. .expect("Failed to create test wallet");
  46. // Fund wallet with 100 sats
  47. fund_wallet(wallet.clone(), 100, None)
  48. .await
  49. .expect("Failed to fund wallet");
  50. let proofs = wallet
  51. .get_unspent_proofs()
  52. .await
  53. .expect("Could not get proofs");
  54. let keyset_id = get_keyset_id(&mint).await;
  55. // Check initial amounts after minting
  56. let total_issued = mint.total_issued().await.unwrap();
  57. let total_redeemed = mint.total_redeemed().await.unwrap();
  58. let initial_issued = total_issued
  59. .get(&keyset_id)
  60. .copied()
  61. .unwrap_or(Amount::ZERO);
  62. let initial_redeemed = total_redeemed
  63. .get(&keyset_id)
  64. .copied()
  65. .unwrap_or(Amount::ZERO);
  66. assert_eq!(
  67. initial_issued,
  68. Amount::from(100),
  69. "Should have issued 100 sats"
  70. );
  71. assert_eq!(
  72. initial_redeemed,
  73. Amount::ZERO,
  74. "Should have redeemed 0 sats initially"
  75. );
  76. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  77. // Create swap request for same amount (100 sats)
  78. let preswap = PreMintSecrets::random(
  79. keyset_id,
  80. 100.into(),
  81. &SplitTarget::default(),
  82. &fee_and_amounts,
  83. )
  84. .expect("Failed to create preswap");
  85. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  86. // Execute swap
  87. let swap_response = mint
  88. .process_swap_request(swap_request)
  89. .await
  90. .expect("Swap should succeed");
  91. // Verify response contains correct number of signatures
  92. assert_eq!(
  93. swap_response.signatures.len(),
  94. preswap.blinded_messages().len(),
  95. "Should receive signature for each blinded message"
  96. );
  97. // Verify input proofs are marked as spent
  98. let states = mint
  99. .localstore()
  100. .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
  101. .await
  102. .expect("Failed to get proof states");
  103. for state in states {
  104. assert_eq!(
  105. State::Spent,
  106. state.expect("State should be known"),
  107. "All input proofs should be marked as spent"
  108. );
  109. }
  110. // Verify blind signatures were saved
  111. let saved_signatures = mint
  112. .localstore()
  113. .get_blind_signatures(
  114. &preswap
  115. .blinded_messages()
  116. .iter()
  117. .map(|bm| bm.blinded_secret)
  118. .collect::<Vec<_>>(),
  119. )
  120. .await
  121. .expect("Failed to get blind signatures");
  122. assert_eq!(
  123. saved_signatures.len(),
  124. swap_response.signatures.len(),
  125. "All signatures should be saved"
  126. );
  127. // Check keyset amounts after swap
  128. // Swap redeems old proofs (100 sats) and issues new proofs (100 sats)
  129. let total_issued = mint.total_issued().await.unwrap();
  130. let total_redeemed = mint.total_redeemed().await.unwrap();
  131. let after_issued = total_issued
  132. .get(&keyset_id)
  133. .copied()
  134. .unwrap_or(Amount::ZERO);
  135. let after_redeemed = total_redeemed
  136. .get(&keyset_id)
  137. .copied()
  138. .unwrap_or(Amount::ZERO);
  139. assert_eq!(
  140. after_issued,
  141. Amount::from(200),
  142. "Should have issued 200 sats total (initial 100 + swap 100)"
  143. );
  144. assert_eq!(
  145. after_redeemed,
  146. Amount::from(100),
  147. "Should have redeemed 100 sats from the swap"
  148. );
  149. }
  150. /// Tests that duplicate blinded messages are rejected:
  151. /// 1. First swap with blinded messages succeeds
  152. /// 2. Second swap attempt with same blinded messages fails
  153. /// 3. BlindedMessageWriter should prevent reuse
  154. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  155. async fn test_swap_duplicate_blinded_messages() {
  156. setup_tracing();
  157. let mint = create_and_start_test_mint()
  158. .await
  159. .expect("Failed to create test mint");
  160. let wallet = create_test_wallet_for_mint(mint.clone())
  161. .await
  162. .expect("Failed to create test wallet");
  163. // Fund wallet with 200 sats (enough for two swaps)
  164. fund_wallet(wallet.clone(), 200, None)
  165. .await
  166. .expect("Failed to fund wallet");
  167. let all_proofs = wallet
  168. .get_unspent_proofs()
  169. .await
  170. .expect("Could not get proofs");
  171. // Split proofs into two sets
  172. let mid = all_proofs.len() / 2;
  173. let proofs1: Vec<_> = all_proofs.iter().take(mid).cloned().collect();
  174. let proofs2: Vec<_> = all_proofs.iter().skip(mid).cloned().collect();
  175. let keyset_id = get_keyset_id(&mint).await;
  176. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  177. // Create blinded messages for first swap
  178. let preswap = PreMintSecrets::random(
  179. keyset_id,
  180. proofs1.total_amount().unwrap(),
  181. &SplitTarget::default(),
  182. &fee_and_amounts,
  183. )
  184. .expect("Failed to create preswap");
  185. let blinded_messages = preswap.blinded_messages();
  186. // First swap should succeed
  187. let swap_request1 = SwapRequest::new(proofs1, blinded_messages.clone());
  188. mint.process_swap_request(swap_request1)
  189. .await
  190. .expect("First swap should succeed");
  191. // Second swap with SAME blinded messages should fail
  192. let swap_request2 = SwapRequest::new(proofs2, blinded_messages.clone());
  193. let result = mint.process_swap_request(swap_request2).await;
  194. assert!(
  195. result.is_err(),
  196. "Second swap with duplicate blinded messages should fail"
  197. );
  198. }
  199. /// Tests that swap correctly rejects double-spending attempts:
  200. /// 1. First swap with proofs succeeds
  201. /// 2. Second swap with same proofs fails with TokenAlreadySpent
  202. /// 3. ProofWriter should detect already-spent proofs
  203. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  204. async fn test_swap_double_spend_detection() {
  205. setup_tracing();
  206. let mint = create_and_start_test_mint()
  207. .await
  208. .expect("Failed to create test mint");
  209. let wallet = create_test_wallet_for_mint(mint.clone())
  210. .await
  211. .expect("Failed to create test wallet");
  212. // Fund wallet with 100 sats
  213. fund_wallet(wallet.clone(), 100, None)
  214. .await
  215. .expect("Failed to fund wallet");
  216. let proofs = wallet
  217. .get_unspent_proofs()
  218. .await
  219. .expect("Could not get proofs");
  220. let keyset_id = get_keyset_id(&mint).await;
  221. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  222. // First swap
  223. let preswap1 = PreMintSecrets::random(
  224. keyset_id,
  225. 100.into(),
  226. &SplitTarget::default(),
  227. &fee_and_amounts,
  228. )
  229. .expect("Failed to create preswap");
  230. let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
  231. mint.process_swap_request(swap_request1)
  232. .await
  233. .expect("First swap should succeed");
  234. // Second swap with same proofs should fail
  235. let preswap2 = PreMintSecrets::random(
  236. keyset_id,
  237. 100.into(),
  238. &SplitTarget::default(),
  239. &fee_and_amounts,
  240. )
  241. .expect("Failed to create preswap");
  242. let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
  243. let result = mint.process_swap_request(swap_request2).await;
  244. match result {
  245. Err(cdk::Error::TokenAlreadySpent) => {
  246. // Expected error
  247. }
  248. Err(err) => panic!("Wrong error type: {:?}", err),
  249. Ok(_) => panic!("Double spend should not succeed"),
  250. }
  251. }
  252. /// Tests that unbalanced swap requests are rejected:
  253. /// Case 1: Output amount < Input amount (trying to steal from mint)
  254. /// Case 2: Output amount > Input amount (trying to create tokens)
  255. /// Both should fail with TransactionUnbalanced error.
  256. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  257. async fn test_swap_unbalanced_transaction_detection() {
  258. setup_tracing();
  259. let mint = create_and_start_test_mint()
  260. .await
  261. .expect("Failed to create test mint");
  262. let wallet = create_test_wallet_for_mint(mint.clone())
  263. .await
  264. .expect("Failed to create test wallet");
  265. // Fund wallet with 100 sats
  266. fund_wallet(wallet.clone(), 100, None)
  267. .await
  268. .expect("Failed to fund wallet");
  269. let proofs = wallet
  270. .get_unspent_proofs()
  271. .await
  272. .expect("Could not get proofs");
  273. let keyset_id = get_keyset_id(&mint).await;
  274. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  275. // Case 1: Try to swap for LESS (95 < 100) - underpaying
  276. let preswap_less = PreMintSecrets::random(
  277. keyset_id,
  278. 95.into(),
  279. &SplitTarget::default(),
  280. &fee_and_amounts,
  281. )
  282. .expect("Failed to create preswap");
  283. let swap_request_less = SwapRequest::new(proofs.clone(), preswap_less.blinded_messages());
  284. match mint.process_swap_request(swap_request_less).await {
  285. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  286. // Expected error
  287. }
  288. Err(err) => panic!("Wrong error type for underpay: {:?}", err),
  289. Ok(_) => panic!("Unbalanced swap (underpay) should not succeed"),
  290. }
  291. // Case 2: Try to swap for MORE (105 > 100) - overpaying/creating tokens
  292. let preswap_more = PreMintSecrets::random(
  293. keyset_id,
  294. 105.into(),
  295. &SplitTarget::default(),
  296. &fee_and_amounts,
  297. )
  298. .expect("Failed to create preswap");
  299. let swap_request_more = SwapRequest::new(proofs.clone(), preswap_more.blinded_messages());
  300. match mint.process_swap_request(swap_request_more).await {
  301. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  302. // Expected error
  303. }
  304. Err(err) => panic!("Wrong error type for overpay: {:?}", err),
  305. Ok(_) => panic!("Unbalanced swap (overpay) should not succeed"),
  306. }
  307. }
  308. /// Tests P2PK (Pay-to-Public-Key) spending conditions:
  309. /// 1. Create proofs locked to a public key
  310. /// 2. Attempt swap without signature - should fail
  311. /// 3. Attempt swap with valid signature - should succeed
  312. /// Validates NUT-11 signature enforcement.
  313. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  314. async fn test_swap_p2pk_signature_validation() {
  315. setup_tracing();
  316. let mint = create_and_start_test_mint()
  317. .await
  318. .expect("Failed to create test mint");
  319. let wallet = create_test_wallet_for_mint(mint.clone())
  320. .await
  321. .expect("Failed to create test wallet");
  322. // Fund wallet with 100 sats
  323. fund_wallet(wallet.clone(), 100, None)
  324. .await
  325. .expect("Failed to fund wallet");
  326. let input_proofs = wallet
  327. .get_unspent_proofs()
  328. .await
  329. .expect("Could not get proofs");
  330. let keyset_id = get_keyset_id(&mint).await;
  331. let secret_key = SecretKey::generate();
  332. // Create P2PK locked outputs
  333. let spending_conditions = SpendingConditions::new_p2pk(secret_key.public_key(), None);
  334. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  335. let pre_swap = PreMintSecrets::with_conditions(
  336. keyset_id,
  337. 100.into(),
  338. &SplitTarget::default(),
  339. &spending_conditions,
  340. &fee_and_amounts,
  341. )
  342. .expect("Failed to create P2PK preswap");
  343. let swap_request = SwapRequest::new(input_proofs.clone(), pre_swap.blinded_messages());
  344. // First swap to get P2PK locked proofs
  345. let keys = mint.pubkeys().keysets.first().cloned().unwrap().keys;
  346. let post_swap = mint
  347. .process_swap_request(swap_request)
  348. .await
  349. .expect("Initial swap should succeed");
  350. // Construct proofs from swap response
  351. let mut p2pk_proofs = construct_proofs(
  352. post_swap.signatures,
  353. pre_swap.rs(),
  354. pre_swap.secrets(),
  355. &keys,
  356. )
  357. .expect("Failed to construct proofs");
  358. // Try to spend P2PK proofs WITHOUT signature - should fail
  359. let preswap_unsigned = PreMintSecrets::random(
  360. keyset_id,
  361. 100.into(),
  362. &SplitTarget::default(),
  363. &fee_and_amounts,
  364. )
  365. .expect("Failed to create preswap");
  366. let swap_request_unsigned =
  367. SwapRequest::new(p2pk_proofs.clone(), preswap_unsigned.blinded_messages());
  368. match mint.process_swap_request(swap_request_unsigned).await {
  369. Err(cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided)) => {
  370. // Expected error
  371. }
  372. Err(err) => panic!("Wrong error type: {:?}", err),
  373. Ok(_) => panic!("Unsigned P2PK spend should fail"),
  374. }
  375. // Sign the proofs with correct key
  376. for proof in &mut p2pk_proofs {
  377. proof
  378. .sign_p2pk(secret_key.clone())
  379. .expect("Failed to sign proof");
  380. }
  381. // Try again WITH signature - should succeed
  382. let preswap_signed = PreMintSecrets::random(
  383. keyset_id,
  384. 100.into(),
  385. &SplitTarget::default(),
  386. &fee_and_amounts,
  387. )
  388. .expect("Failed to create preswap");
  389. let swap_request_signed = SwapRequest::new(p2pk_proofs, preswap_signed.blinded_messages());
  390. mint.process_swap_request(swap_request_signed)
  391. .await
  392. .expect("Signed P2PK spend should succeed");
  393. }
  394. /// Tests rollback behavior when duplicate blinded messages are used:
  395. /// This validates that the BlindedMessageWriter prevents reuse of blinded messages.
  396. /// 1. First swap with blinded messages succeeds
  397. /// 2. Second swap with same blinded messages fails
  398. /// 3. The failure should happen early (during blinded message addition)
  399. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  400. async fn test_swap_rollback_on_duplicate_blinded_message() {
  401. setup_tracing();
  402. let mint = create_and_start_test_mint()
  403. .await
  404. .expect("Failed to create test mint");
  405. let wallet = create_test_wallet_for_mint(mint.clone())
  406. .await
  407. .expect("Failed to create test wallet");
  408. // Fund with enough for multiple swaps
  409. fund_wallet(wallet.clone(), 200, None)
  410. .await
  411. .expect("Failed to fund wallet");
  412. let all_proofs = wallet
  413. .get_unspent_proofs()
  414. .await
  415. .expect("Could not get proofs");
  416. let mid = all_proofs.len() / 2;
  417. let proofs1: Vec<_> = all_proofs.iter().take(mid).cloned().collect();
  418. let proofs2: Vec<_> = all_proofs.iter().skip(mid).cloned().collect();
  419. let keyset_id = get_keyset_id(&mint).await;
  420. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  421. // Create shared blinded messages
  422. let preswap = PreMintSecrets::random(
  423. keyset_id,
  424. proofs1.total_amount().unwrap(),
  425. &SplitTarget::default(),
  426. &fee_and_amounts,
  427. )
  428. .expect("Failed to create preswap");
  429. let blinded_messages = preswap.blinded_messages();
  430. // Extract proof2 ys before moving proofs2
  431. let proof2_ys: Vec<_> = proofs2.iter().map(|p| p.y().unwrap()).collect();
  432. // First swap succeeds
  433. let swap1 = SwapRequest::new(proofs1, blinded_messages.clone());
  434. mint.process_swap_request(swap1)
  435. .await
  436. .expect("First swap should succeed");
  437. // Second swap with duplicate blinded messages should fail early
  438. // The BlindedMessageWriter should detect duplicate and prevent the swap
  439. let swap2 = SwapRequest::new(proofs2, blinded_messages.clone());
  440. let result = mint.process_swap_request(swap2).await;
  441. assert!(
  442. result.is_err(),
  443. "Duplicate blinded messages should cause failure"
  444. );
  445. // Verify the second set of proofs are NOT marked as spent
  446. // (since the swap failed before processing them)
  447. let states = mint
  448. .localstore()
  449. .get_proofs_states(&proof2_ys)
  450. .await
  451. .expect("Failed to get proof states");
  452. for state in states {
  453. assert!(
  454. state.is_none(),
  455. "Proofs from failed swap should not be marked as spent"
  456. );
  457. }
  458. }
  459. /// Tests concurrent swap attempts with same proofs:
  460. /// Spawns 3 concurrent tasks trying to swap the same proofs.
  461. /// Only one should succeed, others should fail with TokenAlreadySpent or TokenPending.
  462. /// Validates that concurrent access is properly handled.
  463. #[tokio::test(flavor = "multi_thread", worker_threads = 3)]
  464. async fn test_swap_concurrent_double_spend_prevention() {
  465. setup_tracing();
  466. let mint = create_and_start_test_mint()
  467. .await
  468. .expect("Failed to create test mint");
  469. let wallet = create_test_wallet_for_mint(mint.clone())
  470. .await
  471. .expect("Failed to create test wallet");
  472. // Fund wallet
  473. fund_wallet(wallet.clone(), 100, None)
  474. .await
  475. .expect("Failed to fund wallet");
  476. let proofs = wallet
  477. .get_unspent_proofs()
  478. .await
  479. .expect("Could not get proofs");
  480. let keyset_id = get_keyset_id(&mint).await;
  481. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  482. // Create 3 different swap requests with SAME proofs but different outputs
  483. let preswap1 = PreMintSecrets::random(
  484. keyset_id,
  485. 100.into(),
  486. &SplitTarget::default(),
  487. &fee_and_amounts,
  488. )
  489. .expect("Failed to create preswap 1");
  490. let preswap2 = PreMintSecrets::random(
  491. keyset_id,
  492. 100.into(),
  493. &SplitTarget::default(),
  494. &fee_and_amounts,
  495. )
  496. .expect("Failed to create preswap 2");
  497. let preswap3 = PreMintSecrets::random(
  498. keyset_id,
  499. 100.into(),
  500. &SplitTarget::default(),
  501. &fee_and_amounts,
  502. )
  503. .expect("Failed to create preswap 3");
  504. let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
  505. let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
  506. let swap_request3 = SwapRequest::new(proofs.clone(), preswap3.blinded_messages());
  507. // Spawn concurrent tasks
  508. let mint1 = mint.clone();
  509. let mint2 = mint.clone();
  510. let mint3 = mint.clone();
  511. let task1 = tokio::spawn(async move { mint1.process_swap_request(swap_request1).await });
  512. let task2 = tokio::spawn(async move { mint2.process_swap_request(swap_request2).await });
  513. let task3 = tokio::spawn(async move { mint3.process_swap_request(swap_request3).await });
  514. // Wait for all tasks
  515. let results = tokio::try_join!(task1, task2, task3).expect("Tasks should complete");
  516. // Count successes and failures
  517. let mut success_count = 0;
  518. let mut failure_count = 0;
  519. for result in [results.0, results.1, results.2] {
  520. match result {
  521. Ok(_) => success_count += 1,
  522. Err(cdk::Error::TokenAlreadySpent) | Err(cdk::Error::TokenPending) => {
  523. failure_count += 1
  524. }
  525. Err(err) => panic!("Unexpected error: {:?}", err),
  526. }
  527. }
  528. assert_eq!(
  529. success_count, 1,
  530. "Exactly one swap should succeed in concurrent scenario"
  531. );
  532. assert_eq!(
  533. failure_count, 2,
  534. "Exactly two swaps should fail in concurrent scenario"
  535. );
  536. // Verify all proofs are marked as spent
  537. let states = mint
  538. .localstore()
  539. .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
  540. .await
  541. .expect("Failed to get proof states");
  542. for state in states {
  543. assert_eq!(
  544. State::Spent,
  545. state.expect("State should be known"),
  546. "All proofs should be marked as spent after concurrent attempts"
  547. );
  548. }
  549. }
  550. /// Tests swap with fees enabled:
  551. /// 1. Create mint with keyset that has fees (1 sat per proof)
  552. /// 2. Fund wallet with many small proofs
  553. /// 3. Attempt swap without paying fee - should fail
  554. /// 4. Attempt swap with correct fee deduction - should succeed
  555. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  556. async fn test_swap_with_fees() {
  557. setup_tracing();
  558. let mint = create_and_start_test_mint()
  559. .await
  560. .expect("Failed to create test mint");
  561. let wallet = create_test_wallet_for_mint(mint.clone())
  562. .await
  563. .expect("Failed to create test wallet");
  564. // Rotate to keyset with 1 sat per proof fee
  565. mint.rotate_keyset(CurrencyUnit::Sat, 32, 1)
  566. .await
  567. .expect("Failed to rotate keyset");
  568. // Fund with 1000 sats as individual 1-sat proofs using the fee-based keyset
  569. // Wait a bit for keyset to be available
  570. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  571. fund_wallet(wallet.clone(), 1000, Some(SplitTarget::Value(Amount::ONE)))
  572. .await
  573. .expect("Failed to fund wallet");
  574. let proofs = wallet
  575. .get_unspent_proofs()
  576. .await
  577. .expect("Could not get proofs");
  578. // Take 100 proofs (100 sats total, will need to pay fee)
  579. let hundred_proofs: Vec<_> = proofs.iter().take(100).cloned().collect();
  580. // Get the keyset ID from the proofs (which will be the fee-based keyset)
  581. let keyset_id = hundred_proofs[0].keyset_id;
  582. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  583. // Try to swap for 100 outputs (same as input) - should fail due to unpaid fee
  584. let preswap_no_fee = PreMintSecrets::random(
  585. keyset_id,
  586. 100.into(),
  587. &SplitTarget::default(),
  588. &fee_and_amounts,
  589. )
  590. .expect("Failed to create preswap");
  591. let swap_no_fee = SwapRequest::new(hundred_proofs.clone(), preswap_no_fee.blinded_messages());
  592. match mint.process_swap_request(swap_no_fee).await {
  593. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  594. // Expected - didn't pay the fee
  595. }
  596. Err(err) => panic!("Wrong error type: {:?}", err),
  597. Ok(_) => panic!("Should fail when fee not paid"),
  598. }
  599. // Calculate correct fee (1 sat per input proof in this keyset)
  600. let fee = hundred_proofs.len() as u64; // 1 sat per proof = 100 sats fee
  601. let output_amount = 100 - fee;
  602. // Swap with correct fee deduction - should succeed if output_amount > 0
  603. if output_amount > 0 {
  604. let preswap_with_fee = PreMintSecrets::random(
  605. keyset_id,
  606. output_amount.into(),
  607. &SplitTarget::default(),
  608. &fee_and_amounts,
  609. )
  610. .expect("Failed to create preswap with fee");
  611. let swap_with_fee =
  612. SwapRequest::new(hundred_proofs.clone(), preswap_with_fee.blinded_messages());
  613. mint.process_swap_request(swap_with_fee)
  614. .await
  615. .expect("Swap with correct fee should succeed");
  616. }
  617. }
  618. /// Tests that swap correctly handles amount overflow:
  619. /// Attempts to create outputs that would overflow u64 when summed.
  620. /// This should be rejected before any database operations occur.
  621. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  622. async fn test_swap_amount_overflow_protection() {
  623. setup_tracing();
  624. let mint = create_and_start_test_mint()
  625. .await
  626. .expect("Failed to create test mint");
  627. let wallet = create_test_wallet_for_mint(mint.clone())
  628. .await
  629. .expect("Failed to create test wallet");
  630. // Fund wallet
  631. fund_wallet(wallet.clone(), 100, None)
  632. .await
  633. .expect("Failed to fund wallet");
  634. let proofs = wallet
  635. .get_unspent_proofs()
  636. .await
  637. .expect("Could not get proofs");
  638. let keyset_id = get_keyset_id(&mint).await;
  639. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  640. // Try to create outputs that would overflow
  641. // 2^63 + 2^63 + small amount would overflow u64
  642. let large_amount = 2_u64.pow(63);
  643. let pre_mint1 = PreMintSecrets::random(
  644. keyset_id,
  645. large_amount.into(),
  646. &SplitTarget::default(),
  647. &fee_and_amounts,
  648. )
  649. .expect("Failed to create pre_mint1");
  650. let pre_mint2 = PreMintSecrets::random(
  651. keyset_id,
  652. large_amount.into(),
  653. &SplitTarget::default(),
  654. &fee_and_amounts,
  655. )
  656. .expect("Failed to create pre_mint2");
  657. let mut combined_pre_mint = PreMintSecrets::random(
  658. keyset_id,
  659. 1.into(),
  660. &SplitTarget::default(),
  661. &fee_and_amounts,
  662. )
  663. .expect("Failed to create combined_pre_mint");
  664. combined_pre_mint.combine(pre_mint1);
  665. combined_pre_mint.combine(pre_mint2);
  666. let swap_request = SwapRequest::new(proofs, combined_pre_mint.blinded_messages());
  667. // Should fail with overflow/amount error
  668. match mint.process_swap_request(swap_request).await {
  669. Err(cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)))
  670. | Err(cdk::Error::AmountOverflow)
  671. | Err(cdk::Error::AmountError(_))
  672. | Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  673. // Any of these errors are acceptable for overflow
  674. }
  675. Err(err) => panic!("Unexpected error type: {:?}", err),
  676. Ok(_) => panic!("Overflow swap should not succeed"),
  677. }
  678. }
  679. /// Tests swap state transitions through pubsub notifications:
  680. /// 1. Subscribe to proof state changes
  681. /// 2. Execute swap
  682. /// 3. Verify Pending then Spent state transitions are received
  683. /// Validates NUT-17 notification behavior.
  684. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  685. async fn test_swap_state_transition_notifications() {
  686. setup_tracing();
  687. let mint = create_and_start_test_mint()
  688. .await
  689. .expect("Failed to create test mint");
  690. let wallet = create_test_wallet_for_mint(mint.clone())
  691. .await
  692. .expect("Failed to create test wallet");
  693. // Fund wallet
  694. fund_wallet(wallet.clone(), 100, None)
  695. .await
  696. .expect("Failed to fund wallet");
  697. let proofs = wallet
  698. .get_unspent_proofs()
  699. .await
  700. .expect("Could not get proofs");
  701. let keyset_id = get_keyset_id(&mint).await;
  702. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  703. let preswap = PreMintSecrets::random(
  704. keyset_id,
  705. 100.into(),
  706. &SplitTarget::default(),
  707. &fee_and_amounts,
  708. )
  709. .expect("Failed to create preswap");
  710. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  711. // Subscribe to proof state changes
  712. let proof_ys: Vec<String> = proofs.iter().map(|p| p.y().unwrap().to_string()).collect();
  713. let mut listener = mint
  714. .pubsub_manager()
  715. .subscribe(cdk::subscription::Params {
  716. kind: cdk::nuts::nut17::Kind::ProofState,
  717. filters: proof_ys.clone(),
  718. id: Arc::new("test_swap_notifications".into()),
  719. })
  720. .expect("Should subscribe successfully");
  721. // Execute swap
  722. mint.process_swap_request(swap_request)
  723. .await
  724. .expect("Swap should succeed");
  725. // Give pubsub time to deliver messages
  726. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  727. // Collect all state transition notifications
  728. let mut state_transitions: HashMap<String, Vec<State>> = HashMap::new();
  729. while let Some(msg) = listener.try_recv() {
  730. match msg.into_inner() {
  731. cashu::NotificationPayload::ProofState(cashu::ProofState { y, state, .. }) => {
  732. state_transitions
  733. .entry(y.to_string())
  734. .or_insert_with(Vec::new)
  735. .push(state);
  736. }
  737. _ => panic!("Unexpected notification type"),
  738. }
  739. }
  740. // Verify each proof went through Pending -> Spent transition
  741. for y in proof_ys {
  742. let transitions = state_transitions
  743. .get(&y)
  744. .expect("Should have transitions for proof");
  745. assert_eq!(
  746. transitions,
  747. &vec![State::Pending, State::Spent],
  748. "Proof should transition from Pending to Spent"
  749. );
  750. }
  751. }
  752. /// Tests that swap fails gracefully when proof states cannot be updated:
  753. /// This would test the rollback path where proofs are added but state update fails.
  754. /// In the current implementation, this should trigger rollback of both proofs and blinded messages.
  755. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  756. async fn test_swap_proof_state_consistency() {
  757. setup_tracing();
  758. let mint = create_and_start_test_mint()
  759. .await
  760. .expect("Failed to create test mint");
  761. let wallet = create_test_wallet_for_mint(mint.clone())
  762. .await
  763. .expect("Failed to create test wallet");
  764. // Fund wallet
  765. fund_wallet(wallet.clone(), 100, None)
  766. .await
  767. .expect("Failed to fund wallet");
  768. let proofs = wallet
  769. .get_unspent_proofs()
  770. .await
  771. .expect("Could not get proofs");
  772. let keyset_id = get_keyset_id(&mint).await;
  773. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  774. // Execute successful swap
  775. let preswap = PreMintSecrets::random(
  776. keyset_id,
  777. 100.into(),
  778. &SplitTarget::default(),
  779. &fee_and_amounts,
  780. )
  781. .expect("Failed to create preswap");
  782. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  783. mint.process_swap_request(swap_request)
  784. .await
  785. .expect("Swap should succeed");
  786. // Verify all proofs have consistent state (Spent)
  787. let proof_ys: Vec<_> = proofs.iter().map(|p| p.y().unwrap()).collect();
  788. let states = mint
  789. .localstore()
  790. .get_proofs_states(&proof_ys)
  791. .await
  792. .expect("Failed to get proof states");
  793. // All states should be Some(Spent) - none should be None or Pending
  794. for (i, state) in states.iter().enumerate() {
  795. match state {
  796. Some(State::Spent) => {
  797. // Expected state
  798. }
  799. Some(other_state) => {
  800. panic!("Proof {} in unexpected state: {:?}", i, other_state)
  801. }
  802. None => {
  803. panic!("Proof {} has no state (should be Spent)", i)
  804. }
  805. }
  806. }
  807. }