From a82e3eb3147348b4a096f2074b8f1e2857fed587 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 3 Mar 2025 14:10:47 +0000 Subject: [PATCH] fix: attempt to swap after a failed transaction (#622) * fix: attempt to swap after a failed transaction * fix: revert test change in https://github.com/cashubtc/cdk/pull/585 --- .../tests/fake_wallet.rs | 160 ++++++++++++++++++ crates/cdk-integration-tests/tests/mint.rs | 2 +- crates/cdk-redb/src/mint/mod.rs | 5 +- crates/cdk/src/mint/check_spendable.rs | 23 ++- crates/cdk/src/mint/swap.rs | 11 +- crates/cdk/src/mint/verification.rs | 9 + 6 files changed, 199 insertions(+), 11 deletions(-) diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index 99a782f8..e41149c4 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -955,6 +955,166 @@ async fn test_fake_mint_swap_inflated() -> Result<()> { Ok(()) } +/// Test swap after failure +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_swap_spend_after_fail() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let mint_quote = wallet.mint_quote(100.into(), None).await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?; + + let proofs = wallet.mint(&mint_quote.id, SplitTarget::None, None).await?; + let active_keyset_id = wallet.get_active_mint_keyset().await?.id; + + let pre_mint = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None)?; + + let swap_request = SwapRequest { + inputs: proofs.clone(), + outputs: pre_mint.blinded_messages(), + }; + + let http_client = HttpClient::new(MINT_URL.parse()?); + let response = http_client.post_swap(swap_request.clone()).await; + + assert!(response.is_ok()); + + let pre_mint = PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None)?; + + let swap_request = SwapRequest { + inputs: proofs.clone(), + outputs: pre_mint.blinded_messages(), + }; + + let http_client = HttpClient::new(MINT_URL.parse()?); + let response = http_client.post_swap(swap_request.clone()).await; + + match response { + Err(err) => match err { + cdk::Error::TokenAlreadySpent => (), + err => { + bail!( + "Wrong mint error returned expected already spent: {}", + err.to_string() + ); + } + }, + Ok(_) => { + bail!("Should not have allowed swap with unbalanced"); + } + } + + let pre_mint = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None)?; + + let swap_request = SwapRequest { + inputs: proofs, + outputs: pre_mint.blinded_messages(), + }; + + let http_client = HttpClient::new(MINT_URL.parse()?); + let response = http_client.post_swap(swap_request.clone()).await; + + match response { + Err(err) => match err { + cdk::Error::TokenAlreadySpent => (), + err => { + bail!("Wrong mint error returned: {}", err.to_string()); + } + }, + Ok(_) => { + bail!("Should not have allowed to mint with multiple units"); + } + } + + Ok(()) +} + +/// Test swap after failure +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_melt_spend_after_fail() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let mint_quote = wallet.mint_quote(100.into(), None).await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?; + + let proofs = wallet.mint(&mint_quote.id, SplitTarget::None, None).await?; + let active_keyset_id = wallet.get_active_mint_keyset().await?.id; + + let pre_mint = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None)?; + + let swap_request = SwapRequest { + inputs: proofs.clone(), + outputs: pre_mint.blinded_messages(), + }; + + let http_client = HttpClient::new(MINT_URL.parse()?); + let response = http_client.post_swap(swap_request.clone()).await; + + assert!(response.is_ok()); + + let pre_mint = PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None)?; + + let swap_request = SwapRequest { + inputs: proofs.clone(), + outputs: pre_mint.blinded_messages(), + }; + + let http_client = HttpClient::new(MINT_URL.parse()?); + let response = http_client.post_swap(swap_request.clone()).await; + + match response { + Err(err) => match err { + cdk::Error::TokenAlreadySpent => (), + err => { + bail!("Wrong mint error returned: {}", err.to_string()); + } + }, + Ok(_) => { + bail!("Should not have allowed to mint with multiple units"); + } + } + + let input_amount: u64 = proofs.total_amount()?.into(); + let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string()); + let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; + + let melt_request = MeltBolt11Request { + quote: melt_quote.id, + inputs: proofs, + outputs: None, + }; + + let http_client = HttpClient::new(MINT_URL.parse()?); + let response = http_client.post_melt(melt_request.clone()).await; + + match response { + Err(err) => match err { + cdk::Error::TokenAlreadySpent => (), + err => { + bail!("Wrong mint error returned: {}", err.to_string()); + } + }, + Ok(_) => { + bail!("Should not have allowed to melt with multiple units"); + } + } + + Ok(()) +} + /// Test swap where input unit != output unit #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> { diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 54c623a3..6e6131b0 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -290,7 +290,7 @@ pub async fn test_p2pk_swap() -> Result<()> { for keys in public_keys_to_listen { let statuses = msgs.remove(&keys).expect("some events"); - assert_eq!(statuses, vec![State::Pending, State::Spent]); + assert_eq!(statuses, vec![State::Pending, State::Pending, State::Spent]); } assert!(listener.try_recv().is_err(), "no other event is happening"); diff --git a/crates/cdk-redb/src/mint/mod.rs b/crates/cdk-redb/src/mint/mod.rs index e261fffe..ef1cdf92 100644 --- a/crates/cdk-redb/src/mint/mod.rs +++ b/crates/cdk-redb/src/mint/mod.rs @@ -694,7 +694,6 @@ impl MintDatabase for MintRedbDatabase { for y in ys { let current_state; - { match table.get(y.to_bytes()).map_err(Error::from)? { Some(state) => { @@ -705,8 +704,10 @@ impl MintDatabase for MintRedbDatabase { } } states.push(current_state); + } - if current_state != Some(State::Spent) { + for (y, current_state) in ys.iter().zip(&states) { + if current_state != &Some(State::Spent) { table .insert(y.to_bytes(), state_str.as_str()) .map_err(Error::from)?; diff --git a/crates/cdk/src/mint/check_spendable.rs b/crates/cdk/src/mint/check_spendable.rs index 4077c4b8..d93d8759 100644 --- a/crates/cdk/src/mint/check_spendable.rs +++ b/crates/cdk/src/mint/check_spendable.rs @@ -41,18 +41,37 @@ impl Mint { ys: &[PublicKey], proof_state: State, ) -> Result<(), Error> { - let proofs_state = self + let original_proofs_state = self .localstore .update_proofs_states(ys, proof_state) .await?; - let proofs_state = proofs_state.iter().flatten().collect::>(); + let proofs_state = original_proofs_state + .iter() + .flatten() + .collect::>(); if proofs_state.contains(&State::Pending) { + // Reset states before returning error + for (y, state) in ys.iter().zip(original_proofs_state.iter()) { + if let Some(original_state) = state { + self.localstore + .update_proofs_states(&[*y], *original_state) + .await?; + } + } return Err(Error::TokenPending); } if proofs_state.contains(&State::Spent) { + // Reset states before returning error + for (y, state) in ys.iter().zip(original_proofs_state.iter()) { + if let Some(original_state) = state { + self.localstore + .update_proofs_states(&[*y], *original_state) + .await?; + } + } return Err(Error::TokenAlreadySpent); } diff --git a/crates/cdk/src/mint/swap.rs b/crates/cdk/src/mint/swap.rs index 89261dc7..eda93223 100644 --- a/crates/cdk/src/mint/swap.rs +++ b/crates/cdk/src/mint/swap.rs @@ -14,6 +14,11 @@ impl Mint { ) -> Result { let input_ys = swap_request.inputs.ys()?; + self.localstore + .add_proofs(swap_request.inputs.clone(), None) + .await?; + self.check_ys_spendable(&input_ys, State::Pending).await?; + if let Err(err) = self .verify_transaction_balanced(&swap_request.inputs, &swap_request.outputs) .await @@ -23,12 +28,6 @@ impl Mint { return Err(err); }; - self.localstore - .add_proofs(swap_request.inputs.clone(), None) - .await?; - - self.check_ys_spendable(&input_ys, State::Pending).await?; - let EnforceSigFlag { sig_flag, pubkeys, diff --git a/crates/cdk/src/mint/verification.rs b/crates/cdk/src/mint/verification.rs index 6da93e5a..8dca8755 100644 --- a/crates/cdk/src/mint/verification.rs +++ b/crates/cdk/src/mint/verification.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use cdk_common::{Amount, BlindedMessage, CurrencyUnit, Id, Proofs, ProofsMethods, PublicKey}; +use tracing::instrument; use super::{Error, Mint}; @@ -12,6 +13,7 @@ pub struct Verification { impl Mint { /// Verify that the inputs to the transaction are unique + #[instrument(skip_all)] pub fn check_inputs_unique(inputs: &Proofs) -> Result<(), Error> { let proof_count = inputs.len(); @@ -29,6 +31,7 @@ impl Mint { } /// Verify that the outputs to are unique + #[instrument(skip_all)] pub fn check_outputs_unique(outputs: &[BlindedMessage]) -> Result<(), Error> { let output_count = outputs.len(); @@ -48,6 +51,7 @@ impl Mint { /// Verify output keyset /// /// Checks that the outputs are all of the same unit and the keyset is active + #[instrument(skip_all)] pub async fn verify_outputs_keyset( &self, outputs: &[BlindedMessage], @@ -88,6 +92,7 @@ impl Mint { /// Verify input keyset /// /// Checks that the inputs are all of the same unit + #[instrument(skip_all)] pub async fn verify_inputs_keyset(&self, inputs: &Proofs) -> Result { let mut keyset_units = HashSet::new(); @@ -120,6 +125,7 @@ impl Mint { } /// Verifies that the outputs have not already been signed + #[instrument(skip_all)] pub async fn check_output_already_signed( &self, outputs: &[BlindedMessage], @@ -145,6 +151,7 @@ impl Mint { /// Verifies outputs /// Checks outputs are unique, of the same unit and not signed before + #[instrument(skip_all)] pub async fn verify_outputs(&self, outputs: &[BlindedMessage]) -> Result { Mint::check_outputs_unique(outputs)?; self.check_output_already_signed(outputs).await?; @@ -159,6 +166,7 @@ impl Mint { /// Verifies inputs /// Checks that inputs are unique and of the same unit /// **NOTE: This does not check if inputs have been spent + #[instrument(skip_all)] pub async fn verify_inputs(&self, inputs: &Proofs) -> Result { Mint::check_inputs_unique(inputs)?; let unit = self.verify_inputs_keyset(inputs).await?; @@ -172,6 +180,7 @@ impl Mint { } /// Verify that inputs and outputs are valid and balanced + #[instrument(skip_all)] pub async fn verify_transaction_balanced( &self, inputs: &Proofs,