浏览代码

Return TransactionUnbalanced error for empty swap inputs/outputs

Fixes #1319 and probably #1356

When a swap request has empty inputs or empty outputs, the mint now correctly
returns error code 11002 (TransactionUnbalanced) instead of 11010
(UnitMismatch).

The issue occurred because when inputs or outputs are empty, the unit
verification returns `None`, and comparing `Some(unit) != None` would trigger a
UnitMismatch error before the balance check could detect the actual problem.

Changes:
- Add early validation in process_swap_request to catch empty inputs and return
  TransactionUnbalanced with proper amounts (0 input vs actual output amount)
- Update verify_transaction_balanced to return TransactionUnbalanced when
  either input or output unit is None, instead of attempting unit comparison
- Add test case documenting both scenarios:
  - Swap with inputs but empty outputs (attempting to destroy tokens)
  - Swap with empty inputs but outputs (attempting to create tokens)

This makes error handling more intuitive for API consumers, as the error code
now correctly indicates the transaction is unbalanced (trying to create or
destroy value) rather than suggesting a unit mismatch issue.
Cesar Rodas 2 月之前
父节点
当前提交
29681e55b0

+ 61 - 0
crates/cdk-integration-tests/tests/test_swap_flow.rs

@@ -352,6 +352,67 @@ async fn test_swap_unbalanced_transaction_detection() {
     }
 }
 
+/// Tests that swap requests with empty inputs or outputs are rejected:
+/// Case 1: Empty outputs (inputs without outputs)
+/// Case 2: Empty inputs (outputs without inputs)
+/// Both should fail. Currently returns UnitMismatch (11010) instead of
+/// TransactionUnbalanced (11002) because there are no keyset IDs to determine units.
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_swap_empty_inputs_or_outputs() {
+    setup_tracing();
+    let mint = create_and_start_test_mint()
+        .await
+        .expect("Failed to create test mint");
+    let wallet = create_test_wallet_for_mint(mint.clone())
+        .await
+        .expect("Failed to create test wallet");
+
+    // Fund wallet with 100 sats
+    fund_wallet(wallet.clone(), 100, None)
+        .await
+        .expect("Failed to fund wallet");
+
+    let proofs = wallet
+        .get_unspent_proofs()
+        .await
+        .expect("Could not get proofs");
+
+    // Case 1: Swap request with inputs but empty outputs
+    // This represents trying to destroy tokens (inputs with no outputs)
+    let swap_request_empty_outputs = SwapRequest::new(proofs.clone(), vec![]);
+
+    match mint.process_swap_request(swap_request_empty_outputs).await {
+        Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
+            // This would be the more appropriate error
+        }
+        Err(err) => panic!("Wrong error type for empty outputs: {:?}", err),
+        Ok(_) => panic!("Swap with empty outputs should not succeed"),
+    }
+
+    // Case 2: Swap request with empty inputs but with outputs
+    // This represents trying to create tokens from nothing
+    let keyset_id = get_keyset_id(&mint).await;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
+
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        100.into(),
+        &SplitTarget::default(),
+        &fee_and_amounts,
+    )
+    .expect("Failed to create preswap");
+
+    let swap_request_empty_inputs = SwapRequest::new(vec![], preswap.blinded_messages());
+
+    match mint.process_swap_request(swap_request_empty_inputs).await {
+        Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
+            // This would be the more appropriate error
+        }
+        Err(err) => panic!("Wrong error type for empty inputs: {:?}", err),
+        Ok(_) => panic!("Swap with empty inputs should not succeed"),
+    }
+}
+
 /// Tests P2PK (Pay-to-Public-Key) spending conditions:
 /// 1. Create proofs locked to a public key
 /// 2. Attempt swap without signature - should fail

+ 17 - 10
crates/cdk/src/mint/swap/mod.rs

@@ -29,20 +29,27 @@ impl Mint {
         // and HTLC (including SIGALL)
         swap_request.verify_spending_conditions()?;
 
+        let input_proofs = swap_request.inputs();
+
+        if input_proofs.is_empty() {
+            return Err(Error::TransactionUnbalanced(
+                0,
+                swap_request.output_amount()?.to_u64(),
+                0,
+            ));
+        }
+
         // We don't need to check P2PK or HTLC again. It has all been checked above
         // and the code doesn't reach here unless such verifications were satisfactory
 
         // Verify inputs (cryptographic verification, no DB needed)
-        let input_verification =
-            self.verify_inputs(swap_request.inputs())
-                .await
-                .map_err(|err| {
-                    #[cfg(feature = "prometheus")]
-                    self.record_swap_failure("process_swap_request");
-
-                    tracing::debug!("Input verification failed: {:?}", err);
-                    err
-                })?;
+        let input_verification = self.verify_inputs(input_proofs).await.map_err(|err| {
+            #[cfg(feature = "prometheus")]
+            self.record_swap_failure("process_swap_request");
+
+            tracing::debug!("Input verification failed: {:?}", err);
+            err
+        })?;
 
         // Step 1: Initialize the swap saga
         let init_saga = SwapSaga::new(self, self.localstore.clone(), self.pubsub_manager.clone());

+ 19 - 3
crates/cdk/src/mint/verification.rs

@@ -227,7 +227,25 @@ impl Mint {
             err
         })?;
 
-        if output_verification.unit != input_verification.unit {
+        let fees = self.get_proofs_fee(inputs).await?;
+
+        if output_verification
+            .unit
+            .as_ref()
+            .ok_or(Error::TransactionUnbalanced(
+                input_verification.amount.to_u64(),
+                output_verification.amount.to_u64(),
+                fees.into(),
+            ))?
+            != input_verification
+                .unit
+                .as_ref()
+                .ok_or(Error::TransactionUnbalanced(
+                    input_verification.amount.to_u64(),
+                    output_verification.amount.to_u64(),
+                    0,
+                ))?
+        {
             tracing::debug!(
                 "Output unit {:?} does not match input unit {:?}",
                 output_verification.unit,
@@ -236,8 +254,6 @@ impl Mint {
             return Err(Error::UnitMismatch);
         }
 
-        let fees = self.get_proofs_fee(inputs).await?;
-
         if output_verification.amount
             != input_verification
                 .amount