fix: mint with outputs greater then total or multiple units

This commit is contained in:
thesimplekid
2025-02-04 13:04:59 +00:00
parent 8326d3aca7
commit 05259b99f4
6 changed files with 186 additions and 19 deletions

View File

@@ -157,9 +157,6 @@ pub enum Error {
/// Receive can only be used with tokens from single mint
#[error("Multiple mint tokens not supported by receive. Please deconstruct the token and use receive with_proof")]
MultiMintTokenNotSupported,
/// Unit Not supported
#[error("Unit not supported for method")]
UnitUnsupported,
/// Preimage not provided
#[error("Preimage not provided")]
PreimageNotProvided,
@@ -233,6 +230,9 @@ pub enum Error {
/// NUT03 error
#[error(transparent)]
NUT03(#[from] crate::nuts::nut03::Error),
/// NUT04 error
#[error(transparent)]
NUT04(#[from] crate::nuts::nut04::Error),
/// NUT05 error
#[error(transparent)]
NUT05(#[from] crate::nuts::nut05::Error),
@@ -329,7 +329,7 @@ impl From<Error> for ErrorResponse {
detail: None,
},
Error::UnsupportedUnit => ErrorResponse {
code: ErrorCode::UnitUnsupported,
code: ErrorCode::UnsupportedUnit,
error: Some(err.to_string()),
detail: None,
},
@@ -412,7 +412,7 @@ impl From<ErrorResponse> for Error {
ErrorCode::KeysetNotFound => Self::UnknownKeySet,
ErrorCode::KeysetInactive => Self::InactiveKeyset,
ErrorCode::BlindedMessageAlreadySigned => Self::BlindedMessageAlreadySigned,
ErrorCode::UnitUnsupported => Self::UnitUnsupported,
ErrorCode::UnsupportedUnit => Self::UnsupportedUnit,
ErrorCode::TransactionUnbalanced => Self::TransactionUnbalanced(0, 0, 0),
ErrorCode::MintingDisabled => Self::MintingDisabled,
ErrorCode::InvoiceAlreadyPaid => Self::RequestAlreadyPaid,
@@ -449,7 +449,7 @@ pub enum ErrorCode {
/// Blinded Message Already signed
BlindedMessageAlreadySigned,
/// Unsupported unit
UnitUnsupported,
UnsupportedUnit,
/// Token already issed for quote
TokensAlreadyIssued,
/// Minting Disabled
@@ -478,7 +478,7 @@ impl ErrorCode {
10003 => Self::TokenNotVerified,
11001 => Self::TokenAlreadySpent,
11002 => Self::TransactionUnbalanced,
11005 => Self::UnitUnsupported,
11005 => Self::UnsupportedUnit,
11006 => Self::AmountOutofLimitRange,
11007 => Self::TokenPending,
12001 => Self::KeysetNotFound,
@@ -502,7 +502,7 @@ impl ErrorCode {
Self::TokenNotVerified => 10003,
Self::TokenAlreadySpent => 11001,
Self::TransactionUnbalanced => 11002,
Self::UnitUnsupported => 11005,
Self::UnsupportedUnit => 11005,
Self::AmountOutofLimitRange => 11006,
Self::TokenPending => 11007,
Self::KeysetNotFound => 12001,

View File

@@ -46,6 +46,20 @@ where
Arc::new(fake_wallet),
);
let fee_reserve = FeeReserve {
min_fee_reserve: 1.into(),
percent_fee_reserve: 1.0,
};
let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0);
mint_builder = mint_builder.add_ln_backend(
CurrencyUnit::Usd,
PaymentMethod::Bolt11,
MintMeltLimits::new(1, 5_000),
Arc::new(fake_wallet),
);
let mnemonic = Mnemonic::generate(12)?;
mint_builder = mint_builder

View File

@@ -476,3 +476,125 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> {
Ok(_) => bail!("Minting should not have succeed without a witness"),
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_inflated() -> 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 active_keyset_id = wallet.get_active_mint_keyset().await?.id;
let pre_mint = PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None)?;
let quote_info = wallet
.localstore
.get_mint_quote(&mint_quote.id)
.await?
.expect("there is a quote");
let mut mint_request = MintBolt11Request {
quote: mint_quote.id,
outputs: pre_mint.blinded_messages(),
signature: None,
};
if let Some(secret_key) = quote_info.secret_key {
mint_request.sign(secret_key)?;
}
let http_client = HttpClient::new(MINT_URL.parse()?);
let response = http_client.post_mint(mint_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
err => {
bail!("Wrong mint error returned: {}", err.to_string());
}
},
Ok(_) => {
bail!("Should not have allowed second payment");
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_multiple_units() -> 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 active_keyset_id = wallet.get_active_mint_keyset().await?.id;
let pre_mint = PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None)?;
let wallet_usd = Wallet::new(
MINT_URL,
CurrencyUnit::Usd,
Arc::new(WalletMemoryDatabase::default()),
&Mnemonic::generate(12)?.to_seed_normalized(""),
None,
)?;
let active_keyset_id = wallet_usd.get_active_mint_keyset().await?.id;
let usd_pre_mint = PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None)?;
let quote_info = wallet
.localstore
.get_mint_quote(&mint_quote.id)
.await?
.expect("there is a quote");
let mut sat_outputs = pre_mint.blinded_messages();
let mut usd_outputs = usd_pre_mint.blinded_messages();
sat_outputs.append(&mut usd_outputs);
let mut mint_request = MintBolt11Request {
quote: mint_quote.id,
outputs: sat_outputs,
signature: None,
};
if let Some(secret_key) = quote_info.secret_key {
mint_request.sign(secret_key)?;
}
let http_client = HttpClient::new(MINT_URL.parse()?);
let response = http_client.post_mint(mint_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::UnsupportedUnit => (),
err => {
bail!("Wrong mint error returned: {}", err.to_string());
}
},
Ok(_) => {
bail!("Should not have allowed to mint with multiple units");
}
}
Ok(())
}

View File

@@ -41,7 +41,7 @@ impl Mint {
let settings = nut05
.get_settings(&unit, &method)
.ok_or(Error::UnitUnsupported)?;
.ok_or(Error::UnsupportedUnit)?;
if matches!(options, Some(MeltOptions::Mpp { mpp: _ })) {
// Verify there is no corresponding mint quote.
@@ -103,7 +103,7 @@ impl Mint {
.ok_or_else(|| {
tracing::info!("Could not get ln backend for {}, bolt11 ", unit);
Error::UnitUnsupported
Error::UnsupportedUnit
})?;
let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| {
@@ -113,7 +113,7 @@ impl Mint {
err
);
Error::UnitUnsupported
Error::UnsupportedUnit
})?;
// We only want to set the msats_to_pay of the melt quote if the invoice is amountless
@@ -224,7 +224,7 @@ impl Mint {
Some(
to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit)
.map_err(|_| Error::UnitUnsupported)?,
.map_err(|_| Error::UnsupportedUnit)?,
)
}
false => None,
@@ -233,7 +233,7 @@ impl Mint {
let amount_to_pay = match partial_amount {
Some(amount_to_pay) => amount_to_pay,
None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &melt_quote.unit)
.map_err(|_| Error::UnitUnsupported)?,
.map_err(|_| Error::UnsupportedUnit)?,
};
let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
@@ -502,7 +502,7 @@ impl Mint {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(Error::UnitUnsupported);
return Err(Error::UnsupportedUnit);
}
};

View File

@@ -1,3 +1,6 @@
use std::collections::HashSet;
use cdk_common::Id;
use tracing::instrument;
use uuid::Uuid;
@@ -49,7 +52,7 @@ impl Mint {
}
}
None => {
return Err(Error::UnitUnsupported);
return Err(Error::UnsupportedUnit);
}
}
@@ -77,7 +80,7 @@ impl Mint {
.ok_or_else(|| {
tracing::info!("Bolt11 mint request for unsupported unit");
Error::UnitUnsupported
Error::UnsupportedUnit
})?;
let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
@@ -292,6 +295,35 @@ impl Mint {
mint_request.verify_signature(pubkey)?;
}
// We check the the total value of blinded messages == mint quote
if mint_request.total_amount()? != mint_quote.amount {
return Err(Error::TransactionUnbalanced(
mint_quote.amount.into(),
mint_request.total_amount()?.into(),
0,
));
}
let keyset_ids: HashSet<Id> = mint_request.outputs.iter().map(|b| b.keyset_id).collect();
let mut keyset_units = HashSet::new();
for keyset_id in keyset_ids {
let keyset = self.keyset(&keyset_id).await?.ok_or(Error::UnknownKeySet)?;
keyset_units.insert(keyset.unit);
}
if keyset_units.len() != 1 {
tracing::debug!("Client attempted to mint with outputs of multiple units");
return Err(Error::UnsupportedUnit);
}
if keyset_units.iter().next().expect("Checked len above") != &mint_quote.unit {
tracing::debug!("Client attempted to mint with unit not in quote");
return Err(Error::UnsupportedUnit);
}
let blinded_messages: Vec<PublicKey> = mint_request
.outputs
.iter()
@@ -315,8 +347,7 @@ impl Mint {
self.localstore
.update_mint_quote_state(&mint_request.quote, MintQuoteState::Paid)
.await
.unwrap();
.await?;
return Err(Error::BlindedMessageAlreadySigned);
}

View File

@@ -195,7 +195,7 @@ impl Wallet {
let unit = token.unit().unwrap_or_default();
if unit != self.unit {
return Err(Error::UnitUnsupported);
return Err(Error::UnsupportedUnit);
}
let proofs = token.proofs();