mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-08 14:45:47 +01:00
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:
@@ -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<()> {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user