Browse Source

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
thesimplekid 4 weeks ago
parent
commit
a82e3eb314

+ 160 - 0
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<()> {

+ 1 - 1
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");

+ 3 - 2
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)?;

+ 21 - 2
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::<HashSet<&State>>();
+        let proofs_state = original_proofs_state
+            .iter()
+            .flatten()
+            .collect::<HashSet<&State>>();
 
         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);
         }
 

+ 5 - 6
crates/cdk/src/mint/swap.rs

@@ -14,6 +14,11 @@ impl Mint {
     ) -> Result<SwapResponse, Error> {
         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,

+ 9 - 0
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<CurrencyUnit, Error> {
         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<Verification, Error> {
         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<Verification, Error> {
         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,