Skip to content

Commit

Permalink
fix: attempt to swap after a failed transaction (#622)
Browse files Browse the repository at this point in the history
* fix: attempt to swap after a failed transaction

* fix: revert test change in #585
  • Loading branch information
thesimplekid authored Mar 3, 2025
1 parent 6339305 commit a82e3eb
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 11 deletions.
160 changes: 160 additions & 0 deletions crates/cdk-integration-tests/tests/fake_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
2 changes: 1 addition & 1 deletion crates/cdk-integration-tests/tests/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
5 changes: 3 additions & 2 deletions crates/cdk-redb/src/mint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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)?;
Expand Down
23 changes: 21 additions & 2 deletions crates/cdk/src/mint/check_spendable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
11 changes: 5 additions & 6 deletions crates/cdk/src/mint/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions crates/cdk/src/mint/verification.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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();

Expand All @@ -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();

Expand All @@ -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],
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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],
Expand All @@ -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?;
Expand All @@ -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?;
Expand All @@ -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,
Expand Down

0 comments on commit a82e3eb

Please sign in to comment.