浏览代码

fix: require 0 signatures for HTLC with no pubkeys specified (#1275)

* fix: require 0 signatures for HTLC with no pubkeys specified

  Prior to this fix, HTLC verification incorrectly required 1 signature even
  when no pubkeys were specified in the spending conditions. This caused
  valid preimage-only HTLCs to fail verification.

  The issue occurred in get_pubkeys_and_required_sigs() for HTLC secrets
  before locktime:
  - Line 179: pubkeys defaults to empty vector [] when not specified
  - Line 180: required_sigs defaulted to 1 unconditionally
  - Result: impossible condition requiring 1 signature from 0 pubkeys

* refactor(cashu): replace tuple with SpendingRequirements struct in nut10

Introduce SpendingRequirements struct to replace tuple return type in
get_pubkeys_and_required_sigs function. This improves code clarity by
using named fields (preimage_needed, pubkeys, required_sigs) instead
of positional tuple elements across nut10, nut11, and nut14 modules
tsk 4 天之前
父节点
当前提交
836a50aaa3
共有 3 个文件被更改,包括 83 次插入36 次删除
  1. 70 25
      crates/cashu/src/nuts/nut10.rs
  2. 5 6
      crates/cashu/src/nuts/nut11/mod.rs
  3. 8 5
      crates/cashu/src/nuts/nut14/mod.rs

+ 70 - 25
crates/cashu/src/nuts/nut10.rs

@@ -13,6 +13,20 @@ use thiserror::Error;
 use super::nut01::PublicKey;
 use super::Conditions;
 
+/// Spending requirements for P2PK or HTLC verification
+///
+/// Returned by `get_pubkeys_and_required_sigs` to indicate what conditions
+/// must be met to spend a proof.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct SpendingRequirements {
+    /// Whether a preimage is required (HTLC only, before locktime)
+    pub preimage_needed: bool,
+    /// Public keys that can provide valid signatures
+    pub pubkeys: Vec<PublicKey>,
+    /// Minimum number of signatures required from the pubkeys
+    pub required_sigs: u64,
+}
+
 /// NUT13 Error
 #[derive(Debug, Error)]
 pub enum Error {
@@ -116,15 +130,17 @@ impl Secret {
 /// From NUT-11: "If the tag locktime is the unix time and the mint's local clock is greater than
 /// locktime, the Proof becomes spendable by anyone, except [... if refund keys are specified]"
 ///
-/// Returns (preimage_needed, pubkeys, required_sigs).
-/// For P2PK, preimage_needed is always false.
-/// For HTLC, preimage_needed is true before locktime; From NUT-14: "if the current system time
-/// is later than Secret.tag.locktime, the Proof can be spent if Proof.witness includes
-/// a signature from the key in Secret.tags.refund."
+/// Returns `SpendingRequirements` containing:
+/// - `preimage_needed`: For P2PK, always false. For HTLC, true before locktime.
+/// - `pubkeys`: The public keys that can provide valid signatures
+/// - `required_sigs`: The minimum number of signatures required
+///
+/// From NUT-14: "if the current system time is later than Secret.tag.locktime, the Proof can
+/// be spent if Proof.witness includes a signature from the key in Secret.tags.refund."
 pub(crate) fn get_pubkeys_and_required_sigs(
     secret: &Secret,
     current_time: u64,
-) -> Result<(bool, Vec<PublicKey>, u64), super::nut11::Error> {
+) -> Result<SpendingRequirements, super::nut11::Error> {
     debug_assert!(
         secret.kind() == Kind::P2PK || secret.kind() == Kind::HTLC,
         "get_pubkeys_and_required_sigs called with invalid kind - this is a bug"
@@ -149,10 +165,18 @@ pub(crate) fn get_pubkeys_and_required_sigs(
         if let Some(refund_keys) = &conditions.refund_keys {
             // Locktime has passed and refund keys exist - use refund keys
             let refund_sigs = conditions.num_sigs_refund.unwrap_or(1);
-            Ok((false, refund_keys.clone(), refund_sigs))
+            Ok(SpendingRequirements {
+                preimage_needed: false,
+                pubkeys: refund_keys.clone(),
+                required_sigs: refund_sigs,
+            })
         } else {
             // Locktime has passed with no refund keys - anyone can spend
-            Ok((false, vec![], 0))
+            Ok(SpendingRequirements {
+                preimage_needed: false,
+                pubkeys: vec![],
+                required_sigs: 0,
+            })
         }
     } else {
         // Before locktime: logic differs between P2PK and HTLC
@@ -171,14 +195,28 @@ pub(crate) fn get_pubkeys_and_required_sigs(
                 }
 
                 let primary_num_sigs_required = conditions.num_sigs.unwrap_or(1);
-                Ok((false, primary_keys, primary_num_sigs_required))
+                Ok(SpendingRequirements {
+                    preimage_needed: false,
+                    pubkeys: primary_keys,
+                    required_sigs: primary_num_sigs_required,
+                })
             }
             Kind::HTLC => {
                 // HTLC: needs preimage before locktime, pubkeys from conditions
                 // (data contains hash, not pubkey)
                 let pubkeys = conditions.pubkeys.clone().unwrap_or_default();
-                let required_sigs = conditions.num_sigs.unwrap_or(1);
-                Ok((true, pubkeys, required_sigs))
+                // If no pubkeys are specified, require 0 signatures (only preimage needed)
+                // Otherwise, default to requiring 1 signature
+                let required_sigs = if pubkeys.is_empty() {
+                    0
+                } else {
+                    conditions.num_sigs.unwrap_or(1)
+                };
+                Ok(SpendingRequirements {
+                    preimage_needed: true,
+                    pubkeys,
+                    required_sigs,
+                })
             }
         }
     }
@@ -417,13 +455,15 @@ pub trait SpendingConditionVerification {
         let current_time = crate::util::unix_time();
 
         // Get the relevant public keys and required signature count based on locktime
-        let (preimage_needed, pubkeys, required_sigs) =
-            get_pubkeys_and_required_sigs(&first_secret, current_time)?;
+        let requirements = get_pubkeys_and_required_sigs(&first_secret, current_time)?;
 
-        debug_assert!(!preimage_needed, "P2PK should never require preimage");
+        debug_assert!(
+            !requirements.preimage_needed,
+            "P2PK should never require preimage"
+        );
 
         // Handle "anyone can spend" case (locktime passed with no refund keys)
-        if required_sigs == 0 {
+        if requirements.required_sigs == 0 {
             return Ok(());
         }
 
@@ -449,11 +489,14 @@ pub trait SpendingConditionVerification {
             .map_err(|_| super::nut11::Error::InvalidSignature)?;
 
         // Verify signatures using the existing valid_signatures function
-        let valid_sig_count =
-            super::nut11::valid_signatures(msg_to_sign.as_bytes(), &pubkeys, &signatures)?;
+        let valid_sig_count = super::nut11::valid_signatures(
+            msg_to_sign.as_bytes(),
+            &requirements.pubkeys,
+            &signatures,
+        )?;
 
         // Check if we have enough valid signatures
-        if valid_sig_count < required_sigs {
+        if valid_sig_count < requirements.required_sigs {
             return Err(super::nut11::Error::SpendConditionsNotMet);
         }
 
@@ -478,11 +521,10 @@ pub trait SpendingConditionVerification {
         let current_time = crate::util::unix_time();
 
         // Get the relevant public keys, required signature count, and whether preimage is needed
-        let (preimage_needed, pubkeys, required_sigs) =
-            get_pubkeys_and_required_sigs(&first_secret, current_time)?;
+        let requirements = get_pubkeys_and_required_sigs(&first_secret, current_time)?;
 
         // If preimage is needed (before locktime), verify it
-        if preimage_needed {
+        if requirements.preimage_needed {
             // Extract HTLC witness
             let htlc_witness = match first_input.witness.as_ref() {
                 Some(super::Witness::HTLCWitness(witness)) => witness,
@@ -495,7 +537,7 @@ pub trait SpendingConditionVerification {
         }
 
         // Handle "anyone can spend" case (locktime passed with no refund keys)
-        if required_sigs == 0 {
+        if requirements.required_sigs == 0 {
             return Ok(());
         }
 
@@ -521,11 +563,14 @@ pub trait SpendingConditionVerification {
             .map_err(|_| super::nut11::Error::InvalidSignature)?;
 
         // Verify signatures using the existing valid_signatures function
-        let valid_sig_count =
-            super::nut11::valid_signatures(msg_to_sign.as_bytes(), &pubkeys, &signatures)?;
+        let valid_sig_count = super::nut11::valid_signatures(
+            msg_to_sign.as_bytes(),
+            &requirements.pubkeys,
+            &signatures,
+        )?;
 
         // Check if we have enough valid signatures
-        if valid_sig_count < required_sigs {
+        if valid_sig_count < requirements.required_sigs {
             return Err(super::nut11::Error::SpendConditionsNotMet);
         }
 

+ 5 - 6
crates/cashu/src/nuts/nut11/mod.rs

@@ -155,15 +155,14 @@ impl Proof {
 
         // Based on the current time, we must identify the relevant keys
         let now = unix_time();
-        let (preimage_needed, relevant_pubkeys, relevant_num_sigs_required) =
-            super::nut10::get_pubkeys_and_required_sigs(&secret, now)?;
+        let requirements = super::nut10::get_pubkeys_and_required_sigs(&secret, now)?;
 
-        if preimage_needed {
+        if requirements.preimage_needed {
             return Err(Error::PreimageNotSupportedInP2PK);
         }
 
         // Handle "anyone can spend" case (locktime passed with no refund keys)
-        if relevant_num_sigs_required == 0 {
+        if requirements.required_sigs == 0 {
             return Ok(());
         }
 
@@ -178,7 +177,7 @@ impl Proof {
         let msg: &[u8] = self.secret.as_bytes();
         let valid_sig_count = valid_signatures(
             msg,
-            &relevant_pubkeys,
+            &requirements.pubkeys,
             &witness_signatures
                 .iter()
                 .map(|s| Signature::from_str(s))
@@ -186,7 +185,7 @@ impl Proof {
         )?;
 
         // Check if we have enough valid signatures
-        if valid_sig_count >= relevant_num_sigs_required {
+        if valid_sig_count >= requirements.required_sigs {
             Ok(())
         } else {
             Err(Error::SpendConditionsNotMet)

+ 8 - 5
crates/cashu/src/nuts/nut14/mod.rs

@@ -113,7 +113,7 @@ impl Proof {
 
         // Get the appropriate spending conditions based on locktime
         let now = unix_time();
-        let (preimage_needed, relevant_pubkeys, relevant_num_sigs_required) =
+        let requirements =
             super::nut10::get_pubkeys_and_required_sigs(&secret, now).map_err(Error::NUT11)?;
 
         // While a Witness is usually needed in a P2PK or HTLC proof, it's not
@@ -126,7 +126,7 @@ impl Proof {
         // is needed to get a preimage or signatures.
 
         // If preimage is needed (before locktime), verify it
-        if preimage_needed {
+        if requirements.preimage_needed {
             // Extract HTLC witness
             let htlc_witness = match &self.witness {
                 Some(Witness::HTLCWitness(witness)) => witness,
@@ -137,7 +137,7 @@ impl Proof {
             super::nut10::verify_htlc_preimage(htlc_witness, &secret)?;
         }
 
-        if relevant_num_sigs_required == 0 {
+        if requirements.required_sigs == 0 {
             return Ok(());
         }
 
@@ -163,10 +163,10 @@ impl Proof {
 
         // Count valid signatures using relevant_pubkeys
         let msg: &[u8] = self.secret.as_bytes();
-        let valid_sig_count = valid_signatures(msg, &relevant_pubkeys, &signatures)?;
+        let valid_sig_count = valid_signatures(msg, &requirements.pubkeys, &signatures)?;
 
         // Check if we have enough valid signatures
-        if valid_sig_count >= relevant_num_sigs_required {
+        if valid_sig_count >= requirements.required_sigs {
             Ok(())
         } else {
             Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
@@ -191,6 +191,9 @@ impl Proof {
 
 #[cfg(test)]
 mod tests {
+    use bitcoin::hashes::sha256::Hash as Sha256Hash;
+    use bitcoin::hashes::Hash;
+
     use super::*;
     use crate::nuts::nut00::Witness;
     use crate::nuts::nut10::Kind;