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
This commit is contained in:
thesimplekid
2025-03-03 14:10:47 +00:00
committed by GitHub
parent 63393056a0
commit a82e3eb314
6 changed files with 199 additions and 11 deletions

View File

@@ -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<()> {

View File

@@ -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");

View File

@@ -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)?;

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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,