test_swap_flow.rs 30 KB

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