Browse Source

fix: mint with outputs greater then total or multiple units

thesimplekid 1 month ago
parent
commit
05259b99f4

+ 8 - 8
crates/cdk-common/src/error.rs

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

+ 14 - 0
crates/cdk-integration-tests/src/init_fake_wallet.rs

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

+ 122 - 0
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -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(())
+}

+ 6 - 6
crates/cdk/src/mint/melt.rs

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

+ 35 - 4
crates/cdk/src/mint/mint_nut04.rs

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

+ 1 - 1
crates/cdk/src/wallet/receive.rs

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