浏览代码

feat: update NUT-11 SIG_ALL message aggregation per spec

Implements changes from cashubtc/nuts#302 which updates the SIG_ALL message aggregation scheme for spending condition verification.

  SIG_ALL Message Changes:
  - Update message format to: secret || C || amount || B_
  - Initially added keyset_id to outputs (amount || keyset_id || B_)
  - Removed keyset_id per final spec (amount || B_)
  - Applied to both swap (NUT-03) and melt (NUT-05) operations

  Architecture Improvements:
  - Introduce NUT-10 SpendingConditionVerification trait for unified
    verification logic across swap and melt operations
  - Refactor NUT-11 (P2PK) and NUT-14 (HTLC) verification to share
    common code paths
  - Add helper functions for pubkey/signature extraction with proper
    locktime handling
  - Improve error handling with specific error types (PreimageNotSupportedInP2PK,
    SpendConditionsNotMet)
  - Deprecate old verify_sig_all methods in favor of unified trait

  Test Coverage:
  - Add comprehensive test vectors for P2PK + SIG_ALL combinations
  - Add comprehensive test vectors for HTLC + SIG_ALL combinations
  - Add test helpers for spending condition verification
  - Cover multisig, locktime, refund keys, and mixed scenarios
  - Test vectors aligned with other implementations (nutshell, cashu-ts)

---------

Co-authored-by: Sats And Sports <sats.and.sports@gmail.com>
Co-authored-by: thesimplekid <tsk@thesimplekid.com>
SatsAndSports 5 天之前
父节点
当前提交
9eaa6f1c02

+ 1 - 1
crates/cashu/src/nuts/mod.rs

@@ -54,7 +54,7 @@ pub use nut05::{
 pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts};
 pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
 pub use nut09::{RestoreRequest, RestoreResponse};
-pub use nut10::{Kind, Secret as Nut10Secret, SecretData};
+pub use nut10::{Kind, Secret as Nut10Secret, SecretData, SpendingConditionVerification};
 pub use nut11::{Conditions, P2PKWitness, SigFlag, SpendingConditions};
 pub use nut12::{BlindSignatureDleq, ProofDleq};
 pub use nut14::HTLCWitness;

+ 4 - 7
crates/cashu/src/nuts/nut00/mod.rs

@@ -306,13 +306,10 @@ impl Witness {
     pub fn add_signatures(&mut self, signatues: Vec<String>) {
         match self {
             Self::P2PKWitness(p2pk_witness) => p2pk_witness.signatures.extend(signatues),
-            Self::HTLCWitness(htlc_witness) => {
-                htlc_witness.signatures = htlc_witness.signatures.clone().map(|sigs| {
-                    let mut sigs = sigs;
-                    sigs.extend(signatues);
-                    sigs
-                });
-            }
+            Self::HTLCWitness(htlc_witness) => match &mut htlc_witness.signatures {
+                Some(sigs) => sigs.extend(signatues),
+                None => htlc_witness.signatures = Some(signatues),
+            },
         }
     }
 

+ 26 - 0
crates/cashu/src/nuts/nut03.rs

@@ -91,6 +91,32 @@ impl SwapRequest {
     }
 }
 
+impl super::nut10::SpendingConditionVerification for SwapRequest {
+    fn inputs(&self) -> &Proofs {
+        &self.inputs
+    }
+
+    fn sig_all_msg_to_sign(&self) -> String {
+        let mut msg = String::new();
+
+        // Add all input secrets and C values in order
+        // msg = secret_0 || C_0 || ... || secret_n || C_n
+        for proof in &self.inputs {
+            msg.push_str(&proof.secret.to_string());
+            msg.push_str(&proof.c.to_hex());
+        }
+
+        // Add all output amounts and B_ values in order
+        // msg = ... || amount_0 || B_0 || ... || amount_m || B_m
+        for output in &self.outputs {
+            msg.push_str(&output.amount.to_string());
+            msg.push_str(&output.blinded_secret.to_hex());
+        }
+
+        msg
+    }
+}
+
 /// Split Response [NUT-06]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]

+ 39 - 1
crates/cashu/src/nuts/nut05.rs

@@ -129,7 +129,10 @@ impl<Q> MeltRequest<Q> {
     }
 }
 
-impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
+impl<Q> MeltRequest<Q>
+where
+    Q: Serialize + DeserializeOwned,
+{
     /// Create new [`MeltRequest`]
     pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
         Self {
@@ -151,6 +154,41 @@ impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
     }
 }
 
+impl<Q> super::nut10::SpendingConditionVerification for MeltRequest<Q>
+where
+    Q: std::fmt::Display,
+{
+    fn inputs(&self) -> &Proofs {
+        &self.inputs
+    }
+
+    fn sig_all_msg_to_sign(&self) -> String {
+        let mut msg = String::new();
+
+        // Add all input secrets and C values in order
+        // msg = secret_0 || C_0 || ... || secret_n || C_n
+        for proof in &self.inputs {
+            msg.push_str(&proof.secret.to_string());
+            msg.push_str(&proof.c.to_hex());
+        }
+
+        // Add all output amounts and B_ values in order (if any)
+        // msg = ... || amount_0 || B_0 || ... || amount_m || B_m
+        if let Some(outputs) = &self.outputs {
+            for output in outputs {
+                msg.push_str(&output.amount.to_string());
+                msg.push_str(&output.blinded_secret.to_hex());
+            }
+        }
+
+        // Add quote ID
+        // msg = ... || quote_id
+        msg.push_str(&self.quote.to_string());
+
+        msg
+    }
+}
+
 /// Melt Method Settings
 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]

+ 428 - 0
crates/cashu/src/nuts/nut10.rs

@@ -10,6 +10,9 @@ use serde::ser::SerializeTuple;
 use serde::{Deserialize, Serialize, Serializer};
 use thiserror::Error;
 
+use super::nut01::PublicKey;
+use super::Conditions;
+
 /// NUT13 Error
 #[derive(Debug, Error)]
 pub enum Error {
@@ -105,6 +108,431 @@ impl Secret {
     }
 }
 
+/// Get the relevant public keys and required signature count for P2PK or HTLC verification
+/// This is for NUT-11(P2PK) and NUT-14(HTLC)
+///
+/// Takes into account locktime - if locktime has passed, returns refund keys,
+/// otherwise returns primary pubkeys/hash path.
+/// 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."
+pub(crate) fn get_pubkeys_and_required_sigs(
+    secret: &Secret,
+    current_time: u64,
+) -> Result<(bool, Vec<PublicKey>, u64), 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"
+    );
+
+    let conditions: Conditions = secret
+        .secret_data()
+        .tags()
+        .cloned()
+        .unwrap_or_default()
+        .try_into()?;
+
+    // Check if locktime has passed
+    let locktime_passed = conditions
+        .locktime
+        .map(|locktime| locktime < current_time)
+        .unwrap_or(false);
+
+    // Determine which keys and signature count to use
+    if locktime_passed {
+        // After locktime: use refund path (no preimage needed)
+        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))
+        } else {
+            // Locktime has passed with no refund keys - anyone can spend
+            Ok((false, vec![], 0))
+        }
+    } else {
+        // Before locktime: logic differs between P2PK and HTLC
+        match secret.kind() {
+            Kind::P2PK => {
+                // P2PK: never needs preimage, use primary pubkeys
+                let mut primary_keys = vec![];
+
+                // Add the pubkey from secret.data
+                let data_pubkey = PublicKey::from_str(secret.secret_data().data())?;
+                primary_keys.push(data_pubkey);
+
+                // Add any additional pubkeys from conditions
+                if let Some(additional_keys) = &conditions.pubkeys {
+                    primary_keys.extend(additional_keys.clone());
+                }
+
+                let primary_num_sigs_required = conditions.num_sigs.unwrap_or(1);
+                Ok((false, primary_keys, 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))
+            }
+        }
+    }
+}
+
+use super::Proofs;
+
+/// Verify that a preimage matches the hash in the secret data
+///
+/// The preimage should be a 64-character hex string representing 32 bytes.
+/// We decode it from hex, hash it with SHA256, and compare to the hash in secret.data
+pub fn verify_htlc_preimage(
+    witness: &super::nut14::HTLCWitness,
+    secret: &Secret,
+) -> Result<(), super::nut14::Error> {
+    use bitcoin::hashes::sha256::Hash as Sha256Hash;
+    use bitcoin::hashes::Hash;
+
+    // Get the hash lock from the secret data
+    let hash_lock = Sha256Hash::from_str(secret.secret_data().data())
+        .map_err(|_| super::nut14::Error::InvalidHash)?;
+
+    // Decode and validate the preimage (returns [u8; 32])
+    let preimage_bytes = witness.preimage_data()?;
+
+    // Hash the 32-byte preimage
+    let preimage_hash = Sha256Hash::hash(&preimage_bytes);
+
+    // Compare with the hash lock
+    if hash_lock.ne(&preimage_hash) {
+        return Err(super::nut14::Error::Preimage);
+    }
+
+    Ok(())
+}
+
+/// Trait for requests that spend proofs (SwapRequest, MeltRequest)
+pub trait SpendingConditionVerification {
+    /// Get the input proofs
+    fn inputs(&self) -> &Proofs;
+
+    /// Construct the message to sign for SIG_ALL verification
+    ///
+    /// This concatenates all relevant transaction data that must be signed.
+    /// For swap: input secrets + output blinded messages
+    /// For melt: input secrets + quote/payment request
+    fn sig_all_msg_to_sign(&self) -> String;
+
+    /// Check if at least one proof in the set has SIG_ALL flag set
+    ///
+    /// SIG_ALL requires all proofs in the transaction to be signed.
+    /// If any proof has this flag, we need to verify signatures on all proofs.
+    fn has_at_least_one_sig_all(&self) -> Result<bool, super::nut11::Error> {
+        for proof in self.inputs() {
+            // Try to extract spending conditions from the proof's secret
+            if let Ok(spending_conditions) = super::SpendingConditions::try_from(&proof.secret) {
+                // Check for SIG_ALL flag in either P2PK or HTLC conditions
+                let has_sig_all = match spending_conditions {
+                    super::SpendingConditions::P2PKConditions { conditions, .. } => conditions
+                        .map(|c| c.sig_flag == super::SigFlag::SigAll)
+                        .unwrap_or(false),
+                    super::SpendingConditions::HTLCConditions { conditions, .. } => conditions
+                        .map(|c| c.sig_flag == super::SigFlag::SigAll)
+                        .unwrap_or(false),
+                };
+
+                if has_sig_all {
+                    return Ok(true);
+                }
+            }
+        }
+
+        Ok(false)
+    }
+
+    /// Verify all inputs meet SIG_ALL requirements per NUT-11
+    ///
+    /// When any input has SIG_ALL, all inputs must have:
+    /// 1. Same kind (P2PK or HTLC)
+    /// 2. SIG_ALL flag set
+    /// 3. Same Secret.data
+    /// 4. Same Secret.tags
+    fn verify_all_inputs_match_for_sig_all(&self) -> Result<(), super::nut11::Error> {
+        let inputs = self.inputs();
+
+        if inputs.is_empty() {
+            return Err(super::nut11::Error::SpendConditionsNotMet);
+        }
+
+        // Get first input's properties
+        let first_input = inputs.first().unwrap();
+        let first_secret = Secret::try_from(&first_input.secret)
+            .map_err(|_| super::nut11::Error::IncorrectSecretKind)?;
+        let first_kind = first_secret.kind();
+        let first_data = first_secret.secret_data().data();
+        let first_tags = first_secret.secret_data().tags();
+
+        // Get first input's conditions to check SIG_ALL flag
+        let first_conditions =
+            super::Conditions::try_from(first_tags.cloned().unwrap_or_default())?;
+
+        // Verify first input has SIG_ALL (it should, since we only call this function when SIG_ALL is detected)
+        if first_conditions.sig_flag != super::SigFlag::SigAll {
+            return Err(super::nut11::Error::SpendConditionsNotMet);
+        }
+
+        // Verify all remaining inputs match
+        for proof in inputs.iter().skip(1) {
+            let secret = Secret::try_from(&proof.secret)
+                .map_err(|_| super::nut11::Error::IncorrectSecretKind)?;
+
+            // Check kind matches
+            if secret.kind() != first_kind {
+                return Err(super::nut11::Error::SpendConditionsNotMet);
+            }
+
+            // Check data matches
+            if secret.secret_data().data() != first_data {
+                return Err(super::nut11::Error::SpendConditionsNotMet);
+            }
+
+            // Check tags match (this also ensures SIG_ALL flag matches, since sig_flag is part of tags)
+            if secret.secret_data().tags() != first_tags {
+                return Err(super::nut11::Error::SpendConditionsNotMet);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Verify spending conditions for this transaction
+    ///
+    /// This is the main entry point for spending condition verification.
+    /// It checks if any input has SIG_ALL and dispatches to the appropriate verification path.
+    fn verify_spending_conditions(&self) -> Result<(), super::nut11::Error> {
+        // Check if any input has SIG_ALL flag
+        if self.has_at_least_one_sig_all()? {
+            // at least one input has SIG_ALL
+            self.verify_full_sig_all_check()
+        } else {
+            // none of the inputs are SIG_ALL, so we can simply check
+            // each independently and verify any spending conditions
+            // that may - or may not - be there.
+            self.verify_inputs_individually().map_err(|e| match e {
+                super::nut14::Error::NUT11(nut11_err) => nut11_err,
+                _ => super::nut11::Error::SpendConditionsNotMet,
+            })
+        }
+    }
+
+    /// Verify spending conditions when SIG_ALL is present
+    ///
+    /// When SIG_ALL is set, all proofs in the transaction must be signed together.
+    fn verify_full_sig_all_check(&self) -> Result<(), super::nut11::Error> {
+        debug_assert!(
+            self.has_at_least_one_sig_all()?,
+            "verify_full_sig_all_check() called on proofs without SIG_ALL. This shouldn't happen"
+        );
+        // Verify all inputs meet SIG_ALL requirements per NUT-11:
+        // All inputs must have: (1) same kind, (2) SIG_ALL flag, (3) same data, (4) same tags
+        self.verify_all_inputs_match_for_sig_all()?;
+
+        // Get the first input to determine the kind
+        let first_input = self
+            .inputs()
+            .first()
+            .ok_or(super::nut11::Error::SpendConditionsNotMet)?;
+        let first_secret = Secret::try_from(&first_input.secret)
+            .map_err(|_| super::nut11::Error::IncorrectSecretKind)?;
+
+        // Dispatch based on secret kind
+        match first_secret.kind() {
+            Kind::P2PK => {
+                self.verify_sig_all_p2pk()?;
+            }
+            Kind::HTLC => {
+                self.verify_sig_all_htlc()?;
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Verify spending conditions for each input individually
+    ///
+    /// Handles SIG_INPUTS mode, non-NUT-10 secrets, and any other case where inputs
+    /// are verified independently rather than as a group.
+    /// This function will NOT be called if any input has SIG_ALL.
+    fn verify_inputs_individually(&self) -> Result<(), super::nut14::Error> {
+        debug_assert!(
+            !(self.has_at_least_one_sig_all()?),
+            "verify_inputs_individually() called on SIG_ALL. This shouldn't happen"
+        );
+        for proof in self.inputs() {
+            // Check if secret is a nut10 secret with conditions
+            if let Ok(secret) = Secret::try_from(&proof.secret) {
+                // Verify this function isn't being called with SIG_ALL proofs (development check)
+                if let Ok(conditions) = super::Conditions::try_from(
+                    secret.secret_data().tags().cloned().unwrap_or_default(),
+                ) {
+                    debug_assert!(
+                        conditions.sig_flag != super::SigFlag::SigAll,
+                        "verify_inputs_individually called with SIG_ALL proof - this is a bug"
+                    );
+                }
+
+                match secret.kind() {
+                    Kind::P2PK => {
+                        proof.verify_p2pk()?;
+                    }
+                    Kind::HTLC => {
+                        proof.verify_htlc()?;
+                    }
+                }
+            }
+            // If not a nut10 secret, skip verification (plain secret)
+        }
+        Ok(())
+    }
+
+    /// Verify P2PK SIG_ALL signatures
+    ///
+    /// Do NOT call this directly. This is called only from 'verify_full_sig_all_check',
+    /// which has already done many important SIG_ALL checks. This performs the final
+    /// signature verification for SIG_ALL+P2PK transactions.
+    fn verify_sig_all_p2pk(&self) -> Result<(), super::nut11::Error> {
+        // Get the first input, as it's the one with the signatures
+        let first_input = self
+            .inputs()
+            .first()
+            .ok_or(super::nut11::Error::SpendConditionsNotMet)?;
+        let first_secret = Secret::try_from(&first_input.secret)
+            .map_err(|_| super::nut11::Error::IncorrectSecretKind)?;
+
+        // Record current time for locktime evaluation
+        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)?;
+
+        debug_assert!(!preimage_needed, "P2PK should never require preimage");
+
+        // Handle "anyone can spend" case (locktime passed with no refund keys)
+        if required_sigs == 0 {
+            return Ok(());
+        }
+
+        // Construct the message that should be signed
+        let msg_to_sign = self.sig_all_msg_to_sign();
+
+        // Extract signatures from the first input's witness
+        let first_witness = first_input
+            .witness
+            .as_ref()
+            .ok_or(super::nut11::Error::SignaturesNotProvided)?;
+
+        let witness_sigs = first_witness
+            .signatures()
+            .ok_or(super::nut11::Error::SignaturesNotProvided)?;
+
+        // Convert witness strings to Signature objects
+        use std::str::FromStr;
+        let signatures: Vec<bitcoin::secp256k1::schnorr::Signature> = witness_sigs
+            .iter()
+            .map(|s| bitcoin::secp256k1::schnorr::Signature::from_str(s))
+            .collect::<Result<Vec<_>, _>>()
+            .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)?;
+
+        // Check if we have enough valid signatures
+        if valid_sig_count < required_sigs {
+            return Err(super::nut11::Error::SpendConditionsNotMet);
+        }
+
+        Ok(())
+    }
+
+    /// Verify HTLC SIG_ALL signatures
+    ///
+    /// Do NOT call this directly. This is called only from 'verify_full_sig_all_check',
+    /// which has already done many important SIG_ALL checks. This performs the final
+    /// signature verification for SIG_ALL+HTLC transactions.
+    fn verify_sig_all_htlc(&self) -> Result<(), super::nut11::Error> {
+        // Get the first input, as it's the one with the signatures
+        let first_input = self
+            .inputs()
+            .first()
+            .ok_or(super::nut11::Error::SpendConditionsNotMet)?;
+        let first_secret = Secret::try_from(&first_input.secret)
+            .map_err(|_| super::nut11::Error::IncorrectSecretKind)?;
+
+        // Record current time for locktime evaluation
+        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)?;
+
+        // If preimage is needed (before locktime), verify it
+        if preimage_needed {
+            // Extract HTLC witness
+            let htlc_witness = match first_input.witness.as_ref() {
+                Some(super::Witness::HTLCWitness(witness)) => witness,
+                _ => return Err(super::nut11::Error::SignaturesNotProvided),
+            };
+
+            // Verify the preimage matches the hash in the secret
+            verify_htlc_preimage(htlc_witness, &first_secret)
+                .map_err(|_| super::nut11::Error::SpendConditionsNotMet)?;
+        }
+
+        // Handle "anyone can spend" case (locktime passed with no refund keys)
+        if required_sigs == 0 {
+            return Ok(());
+        }
+
+        // Construct the message that should be signed
+        let msg_to_sign = self.sig_all_msg_to_sign();
+
+        // Extract signatures from the first input's witness
+        let first_witness = first_input
+            .witness
+            .as_ref()
+            .ok_or(super::nut11::Error::SignaturesNotProvided)?;
+
+        let witness_sigs = first_witness
+            .signatures()
+            .ok_or(super::nut11::Error::SignaturesNotProvided)?;
+
+        // Convert witness strings to Signature objects
+        use std::str::FromStr;
+        let signatures: Vec<bitcoin::secp256k1::schnorr::Signature> = witness_sigs
+            .iter()
+            .map(|s| bitcoin::secp256k1::schnorr::Signature::from_str(s))
+            .collect::<Result<Vec<_>, _>>()
+            .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)?;
+
+        // Check if we have enough valid signatures
+        if valid_sig_count < required_sigs {
+            return Err(super::nut11::Error::SpendConditionsNotMet);
+        }
+
+        Ok(())
+    }
+}
+
 impl Serialize for Secret {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where

文件差异内容过多而无法显示
+ 557 - 433
crates/cashu/src/nuts/nut11/mod.rs


+ 89 - 67
crates/cashu/src/nuts/nut14/mod.rs

@@ -4,8 +4,6 @@
 
 use std::str::FromStr;
 
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
-use bitcoin::hashes::Hash;
 use bitcoin::secp256k1::schnorr::Signature;
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
@@ -14,7 +12,6 @@ use super::nut00::Witness;
 use super::nut10::Secret;
 use super::nut11::valid_signatures;
 use super::{Conditions, Proof};
-use crate::ensure_cdk;
 use crate::util::{hex, unix_time};
 
 pub mod serde_htlc_witness;
@@ -46,6 +43,9 @@ pub enum Error {
     /// Witness Signatures not provided
     #[error("Witness did not provide signatures")]
     SignaturesNotProvided,
+    /// SIG_ALL not supported in this context
+    #[error("SIG_ALL proofs must be verified using a different method")]
+    SigAllNotSupportedHere,
     /// Secp256k1 error
     #[error(transparent)]
     Secp256k1(#[from] bitcoin::secp256k1::Error),
@@ -68,87 +68,109 @@ pub struct HTLCWitness {
     pub signatures: Option<Vec<String>>,
 }
 
+impl HTLCWitness {
+    /// Decode the preimage from hex and verify it's exactly 32 bytes
+    ///
+    /// Returns the 32-byte preimage data if valid, or an error if:
+    /// - The hex decoding fails
+    /// - The decoded data is not exactly 32 bytes
+    pub fn preimage_data(&self) -> Result<[u8; 32], Error> {
+        const REQUIRED_PREIMAGE_BYTES: usize = 32;
+
+        // Decode the 64-character hex string to bytes
+        let preimage_bytes = hex::decode(&self.preimage).map_err(|_| Error::InvalidHexPreimage)?;
+
+        // Verify the preimage is exactly 32 bytes
+        if preimage_bytes.len() != REQUIRED_PREIMAGE_BYTES {
+            return Err(Error::PreimageInvalidSize);
+        }
+
+        // Convert to fixed-size array
+        let mut array = [0u8; 32];
+        array.copy_from_slice(&preimage_bytes);
+        Ok(array)
+    }
+}
+
 impl Proof {
     /// Verify HTLC
     pub fn verify_htlc(&self) -> Result<(), Error> {
         let secret: Secret = self.secret.clone().try_into()?;
-        let conditions: Option<Conditions> = secret
+        let spending_conditions: Conditions = secret
             .secret_data()
             .tags()
-            .and_then(|c| c.clone().try_into().ok());
-
-        let htlc_witness = match &self.witness {
-            Some(Witness::HTLCWitness(witness)) => witness,
-            _ => return Err(Error::IncorrectSecretKind),
-        };
+            .cloned()
+            .unwrap_or_default()
+            .try_into()?;
 
-        const REQUIRED_PREIMAGE_BYTES: usize = 32;
-
-        let preimage_bytes =
-            hex::decode(&htlc_witness.preimage).map_err(|_| Error::InvalidHexPreimage)?;
+        if spending_conditions.sig_flag == super::SigFlag::SigAll {
+            return Err(Error::SigAllNotSupportedHere);
+        }
 
-        if preimage_bytes.len() != REQUIRED_PREIMAGE_BYTES {
-            return Err(Error::PreimageInvalidSize);
+        if secret.kind() != super::Kind::HTLC {
+            return Err(Error::IncorrectSecretKind);
         }
 
-        if let Some(conditions) = conditions {
-            // Check locktime
-            if let Some(locktime) = conditions.locktime {
-                // If locktime is in passed and no refund keys provided anyone can spend
-                if locktime.lt(&unix_time()) && conditions.refund_keys.is_none() {
-                    return Ok(());
-                }
-
-                // If refund keys are provided verify p2pk signatures
-                if let (Some(refund_key), Some(signatures)) =
-                    (conditions.refund_keys, &self.witness)
-                {
-                    let signatures = signatures
-                        .signatures()
-                        .ok_or(Error::SignaturesNotProvided)?
-                        .iter()
-                        .map(|s| Signature::from_str(s))
-                        .collect::<Result<Vec<Signature>, _>>()?;
-
-                    // If secret includes refund keys check that there is a valid signature
-                    if valid_signatures(self.secret.as_bytes(), &refund_key, &signatures)?.ge(&1) {
-                        return Ok(());
-                    }
-                }
-            }
-            // If pubkeys are present check there is a valid signature
-            if let Some(pubkey) = conditions.pubkeys {
-                let req_sigs = conditions.num_sigs.unwrap_or(1);
-
-                let signatures = htlc_witness
-                    .signatures
-                    .as_ref()
-                    .ok_or(Error::SignaturesNotProvided)?;
-
-                let signatures = signatures
-                    .iter()
-                    .map(|s| Signature::from_str(s))
-                    .collect::<Result<Vec<Signature>, _>>()?;
-
-                let valid_sigs = valid_signatures(self.secret.as_bytes(), &pubkey, &signatures)?;
-                ensure_cdk!(valid_sigs >= req_sigs, Error::IncorrectSecretKind);
-            }
+        // Get the appropriate spending conditions based on locktime
+        let now = unix_time();
+        let (preimage_needed, relevant_pubkeys, relevant_num_sigs_required) =
+            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
+        // always needed. If we are past the locktime, and there are no refund
+        // keys, then the proofs are anyone-can-spend:
+        //     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 [there are no refund keys]"
+        // Therefore, this function should not extract any Witness unless it
+        // is needed to get a preimage or signatures.
+
+        // If preimage is needed (before locktime), verify it
+        if preimage_needed {
+            // Extract HTLC witness
+            let htlc_witness = match &self.witness {
+                Some(Witness::HTLCWitness(witness)) => witness,
+                _ => return Err(Error::IncorrectSecretKind),
+            };
+
+            // Verify preimage using shared function
+            super::nut10::verify_htlc_preimage(htlc_witness, &secret)?;
         }
 
-        if secret.kind().ne(&super::Kind::HTLC) {
-            return Err(Error::IncorrectSecretKind);
+        if relevant_num_sigs_required == 0 {
+            return Ok(());
         }
 
-        let hash_lock =
-            Sha256Hash::from_str(secret.secret_data().data()).map_err(|_| Error::InvalidHash)?;
+        // if we get here, the preimage check (if it was needed) has been done
+        // and we know that at least one signature is required. So, we extract
+        // the witness.signatures and count them:
+
+        // Extract witness signatures
+        let htlc_witness = match &self.witness {
+            Some(Witness::HTLCWitness(witness)) => witness,
+            _ => return Err(Error::IncorrectSecretKind),
+        };
+        let witness_signatures = htlc_witness
+            .signatures
+            .as_ref()
+            .ok_or(Error::SignaturesNotProvided)?;
 
-        let preimage_hash = Sha256Hash::hash(&preimage_bytes);
+        // Convert signatures from strings
+        let signatures: Vec<Signature> = witness_signatures
+            .iter()
+            .map(|s| Signature::from_str(s))
+            .collect::<Result<Vec<_>, _>>()?;
 
-        if hash_lock.ne(&preimage_hash) {
-            return Err(Error::Preimage);
-        }
+        // Count valid signatures using relevant_pubkeys
+        let msg: &[u8] = self.secret.as_bytes();
+        let valid_sig_count = valid_signatures(msg, &relevant_pubkeys, &signatures)?;
 
-        Ok(())
+        // Check if we have enough valid signatures
+        if valid_sig_count >= relevant_num_sigs_required {
+            Ok(())
+        } else {
+            Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
+        }
     }
 
     /// Add Preimage

+ 1 - 1
crates/cdk-signatory/src/signatory.rs

@@ -148,7 +148,7 @@ pub trait Signatory {
         blinded_messages: Vec<BlindedMessage>,
     ) -> Result<Vec<BlindSignature>, Error>;
 
-    /// Verify [`Proof`] meets conditions and is signed
+    /// Verify [`Proof`] meets conditions and is signed by the mint (ignores P2PK/HTLC signatures"
     async fn verify_proofs(&self, proofs: Vec<Proof>) -> Result<(), Error>;
 
     /// Retrieve the list of all mint keysets

+ 3 - 3
crates/cdk/src/lib.rs

@@ -26,6 +26,9 @@ pub mod mint;
 #[cfg(feature = "wallet")]
 pub mod wallet;
 
+#[cfg(test)]
+mod test_helpers;
+
 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 mod bip353;
 
@@ -49,9 +52,6 @@ pub use oidc_client::OidcClient;
 pub mod event;
 pub mod fees;
 
-#[cfg(test)]
-pub mod test_helpers;
-
 #[doc(hidden)]
 pub use bitcoin::secp256k1;
 #[cfg(feature = "mint")]

+ 13 - 3
crates/cdk/src/mint/melt.rs → crates/cdk/src/mint/melt/mod.rs

@@ -8,7 +8,7 @@ use cdk_common::payment::{
     Bolt11OutgoingPaymentOptions, Bolt12OutgoingPaymentOptions, OutgoingPaymentOptions,
 };
 use cdk_common::quote_id::QuoteId;
-use cdk_common::{MeltOptions, MeltQuoteBolt12Request};
+use cdk_common::{MeltOptions, MeltQuoteBolt12Request, SpendingConditionVerification};
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::METRICS;
 use lightning::offers::offer::Offer;
@@ -24,8 +24,11 @@ use crate::types::PaymentProcessorKey;
 use crate::util::unix_time;
 use crate::{ensure_cdk, Amount, Error};
 
-mod melt_saga;
-pub(super) mod shared;
+pub(crate) mod melt_saga;
+pub(crate) mod shared;
+
+#[cfg(test)]
+mod tests;
 
 use melt_saga::MeltSaga;
 
@@ -422,6 +425,13 @@ impl Mint {
         &self,
         melt_request: &MeltRequest<QuoteId>,
     ) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
+        // Verify spending conditions (NUT-10/NUT-11/NUT-14), i.e. P2PK
+        // and HTLC (including SIGALL)
+        melt_request.verify_spending_conditions()?;
+
+        // 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
+
         let verification = self.verify_inputs(melt_request.inputs()).await?;
 
         let init_saga = MeltSaga::new(

+ 181 - 0
crates/cdk/src/mint/melt/tests/htlc_sigall_spending_conditions_tests.rs

@@ -0,0 +1,181 @@
+//! HTLC SIG_ALL tests for melt functionality
+//!
+//! These tests verify that the mint correctly enforces SIG_ALL flag behavior for HTLC
+//! during melt operations.
+
+use std::str::FromStr;
+
+use cdk_common::dhke::construct_proofs;
+use cdk_common::melt::MeltQuoteRequest;
+use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
+use cdk_common::Amount;
+use cdk_common::SpendingConditionVerification;
+
+use crate::test_helpers::nut10::{
+    create_test_hash_and_preimage, create_test_keypair, unzip3, TestMintHelper,
+};
+
+/// Test: HTLC SIG_ALL requiring preimage and one signature
+///
+/// Creates HTLC-locked proofs with SIG_ALL flag and verifies:
+/// 1. Melting with only preimage fails (signature required)
+/// 2. Melting with only SIG_INPUTS signatures fails (SIG_ALL required)
+/// 3. Melting with both preimage and SIG_ALL signature succeeds
+#[tokio::test]
+async fn test_htlc_sig_all_requiring_preimage_and_one_signature() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for Alice
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+
+    // Create hash and preimage
+    let (hash, preimage) = create_test_hash_and_preimage();
+
+    println!("Alice pubkey: {}", alice_pubkey);
+    println!("Hash: {}", hash);
+    println!("Preimage: {}", preimage);
+
+    // Step 1: Mint regular proofs (enough to cover invoice + fees)
+    let input_amount = Amount::from(20);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create HTLC spending conditions with SIG_ALL flag (hash locked to Alice's key)
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![alice_pubkey]),
+            refund_keys: None,
+            num_sigs: None,            // Default (1)
+            sig_flag: SigFlag::SigAll, // <-- SIG_ALL flag
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+    println!("Created HTLC spending conditions with SIG_ALL flag");
+
+    // Step 3: Create HTLC blinded messages (outputs)
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+    println!(
+        "Created {} HTLC outputs locked to alice with hash",
+        htlc_outputs.len()
+    );
+
+    // Step 4: Swap regular proofs for HTLC proofs (no signature needed on inputs)
+    let swap_request = cdk_common::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for HTLC proofs");
+    println!("Swap successful! Got BlindSignatures for our HTLC outputs");
+
+    // Step 5: Construct the HTLC proofs
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = htlc_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} HTLC proof(s) [{}]",
+        htlc_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 6: Create a real melt quote that we'll use for all tests
+    let bolt11_str = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq";
+    let bolt11 = cdk_common::Bolt11Invoice::from_str(bolt11_str).unwrap();
+
+    let melt_quote_request = cdk_common::MeltQuoteBolt11Request {
+        request: bolt11,
+        unit: cdk_common::CurrencyUnit::Sat,
+        options: None,
+    };
+
+    let melt_quote = mint
+        .get_melt_quote(MeltQuoteRequest::Bolt11(melt_quote_request))
+        .await
+        .unwrap();
+    println!("Created melt quote: {}", melt_quote.quote);
+
+    // Step 7: Try to melt with only preimage (should fail - signature required)
+    let mut proofs_preimage_only = htlc_proofs.clone();
+    // Add only preimage to first proof (no signature)
+    proofs_preimage_only[0].add_preimage(preimage.clone());
+
+    let melt_request_preimage_only =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), proofs_preimage_only.into(), None);
+
+    let result = melt_request_preimage_only.verify_spending_conditions();
+    assert!(
+        result.is_err(),
+        "Should fail with only preimage (no signature)"
+    );
+    println!("✓ Melting with ONLY preimage failed verification as expected");
+
+    let melt_result = mint.melt(&melt_request_preimage_only).await;
+    assert!(
+        melt_result.is_err(),
+        "Actual melt should also fail with only preimage"
+    );
+    println!("✓ Actual melt with ONLY preimage also failed as expected");
+
+    // Step 8: Try to melt with SIG_INPUTS signatures (should fail - SIG_ALL required)
+    let mut melt_request_sig_inputs =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), htlc_proofs.clone().into(), None);
+
+    // Add preimage to first proof
+    melt_request_sig_inputs.inputs_mut()[0].add_preimage(preimage.clone());
+
+    // Sign each proof individually (SIG_INPUTS mode) - this should fail for SIG_ALL
+    for proof in melt_request_sig_inputs.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = melt_request_sig_inputs.verify_spending_conditions();
+    assert!(
+        result.is_err(),
+        "Should fail - SIG_INPUTS signatures not valid for SIG_ALL"
+    );
+    println!("✓ Melting with SIG_INPUTS signatures failed verification as expected");
+
+    let melt_result = mint.melt(&melt_request_sig_inputs).await;
+    assert!(
+        melt_result.is_err(),
+        "Actual melt should also fail with SIG_INPUTS signatures"
+    );
+    println!("✓ Actual melt with SIG_INPUTS signatures also failed as expected");
+
+    // Step 9: Now melt with correct preimage + SIG_ALL signature
+    let mut melt_request =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), htlc_proofs.clone().into(), None);
+
+    // Add preimage to first proof
+    melt_request.inputs_mut()[0].add_preimage(preimage.clone());
+
+    // Use sign_sig_all to sign the transaction (signature goes on first proof's witness)
+    melt_request.sign_sig_all(alice_secret.clone()).unwrap();
+
+    // Verify spending conditions pass
+    melt_request.verify_spending_conditions().unwrap();
+    println!("✓ HTLC SIG_ALL spending conditions verified successfully");
+
+    // Perform the actual melt - this also verifies spending conditions internally
+    let melt_response = mint.melt(&melt_request).await.unwrap();
+    println!("✓ Melt operation completed successfully!");
+    println!("  Quote state: {:?}", melt_response.state);
+    assert_eq!(melt_response.quote, melt_quote.quote);
+}

+ 190 - 0
crates/cdk/src/mint/melt/tests/htlc_spending_conditions_tests.rs

@@ -0,0 +1,190 @@
+//! HTLC (NUT-14) tests for melt functionality
+//!
+//! These tests verify that the mint correctly validates HTLC spending conditions
+//! during melt operations, including:
+//! - Hash preimage verification
+//! - Signature validation
+
+use cdk_common::dhke::construct_proofs;
+use cdk_common::melt::MeltQuoteRequest;
+use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
+use cdk_common::Amount;
+
+use crate::test_helpers::nut10::{
+    create_test_hash_and_preimage, create_test_keypair, unzip3, TestMintHelper,
+};
+
+/// Test: HTLC requiring preimage and one signature
+///
+/// Creates HTLC-locked proofs and verifies:
+/// 1. Melting with only preimage fails (signature required)
+/// 2. Melting with only signature fails (preimage required)
+/// 3. Melting with both preimage and signature succeeds
+#[tokio::test]
+async fn test_htlc_requiring_preimage_and_one_signature() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for Alice
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+
+    // Create hash and preimage
+    let (hash, preimage) = create_test_hash_and_preimage();
+
+    println!("Alice pubkey: {}", alice_pubkey);
+    println!("Hash: {}", hash);
+    println!("Preimage: {}", preimage);
+
+    // Step 1: Mint regular proofs (enough to cover the invoice amount + fees)
+    // Invoice is 10 sats, fee reserve is 100% (10 sats), so we need 20 sats total
+    let input_amount = Amount::from(20);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create HTLC spending conditions (hash locked to Alice's key)
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![alice_pubkey]),
+            refund_keys: None,
+            num_sigs: None, // Default (1)
+            sig_flag: SigFlag::default(),
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+    println!("Created HTLC spending conditions");
+
+    // Step 3: Create HTLC blinded messages (outputs)
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+    println!(
+        "Created {} HTLC outputs locked to alice with hash",
+        htlc_outputs.len()
+    );
+
+    // Step 4: Swap regular proofs for HTLC proofs (no signature needed on inputs)
+    let swap_request = cdk_common::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for HTLC proofs");
+    println!("Swap successful! Got BlindSignatures for our HTLC outputs");
+
+    // Step 5: Construct the HTLC proofs
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = htlc_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} HTLC proof(s) [{}]",
+        htlc_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 6: Create a real melt quote that we'll use for all tests
+    use cdk_common::SpendingConditionVerification;
+    use std::str::FromStr;
+    let bolt11_str = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq";
+    let bolt11 = cdk_common::Bolt11Invoice::from_str(bolt11_str).unwrap();
+
+    let melt_quote_request = cdk_common::MeltQuoteBolt11Request {
+        request: bolt11,
+        unit: cdk_common::CurrencyUnit::Sat,
+        options: None,
+    };
+
+    let melt_quote = mint
+        .get_melt_quote(MeltQuoteRequest::Bolt11(melt_quote_request))
+        .await
+        .unwrap();
+    println!("Created melt quote: {}", melt_quote.quote);
+
+    // Step 7: Try to melt with only preimage (should fail - signature required)
+
+    let mut proofs_preimage_only = htlc_proofs.clone();
+
+    // Add only preimage (no signature)
+    for proof in proofs_preimage_only.iter_mut() {
+        proof.add_preimage(preimage.clone());
+    }
+
+    let melt_request_preimage_only =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), proofs_preimage_only.into(), None);
+
+    let result = melt_request_preimage_only.verify_spending_conditions();
+    assert!(
+        result.is_err(),
+        "Should fail with only preimage (no signature)"
+    );
+    println!("✓ Melting with ONLY preimage failed verification as expected");
+
+    // Also verify the actual melt fails
+    let melt_result = mint.melt(&melt_request_preimage_only).await;
+    assert!(
+        melt_result.is_err(),
+        "Actual melt should also fail with only preimage"
+    );
+    println!("✓ Actual melt with ONLY preimage also failed as expected");
+
+    // Step 8: Try to melt with only signature (should fail - preimage required)
+    let mut proofs_signature_only = htlc_proofs.clone();
+
+    // Add only signature (no preimage)
+    for proof in proofs_signature_only.iter_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let melt_request_signature_only =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), proofs_signature_only.into(), None);
+
+    let result = melt_request_signature_only.verify_spending_conditions();
+    assert!(
+        result.is_err(),
+        "Should fail with only signature (no preimage)"
+    );
+    println!("✓ Melting with ONLY signature failed verification as expected");
+
+    // Also verify the actual melt fails
+    let melt_result = mint.melt(&melt_request_signature_only).await;
+    assert!(
+        melt_result.is_err(),
+        "Actual melt should also fail with only signature"
+    );
+    println!("✓ Actual melt with ONLY signature also failed as expected");
+
+    // Step 9: Now melt with correct preimage + signature
+    let mut proofs_both = htlc_proofs.clone();
+
+    // Add preimage and sign all proofs
+    for proof in proofs_both.iter_mut() {
+        proof.add_preimage(preimage.clone());
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let melt_request =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), proofs_both.into(), None);
+
+    // Verify spending conditions pass
+    melt_request.verify_spending_conditions().unwrap();
+    println!("✓ HTLC spending conditions verified successfully");
+
+    // Perform the actual melt - this also verifies spending conditions internally
+    let melt_response = mint.melt(&melt_request).await.unwrap();
+    println!("✓ Melt operation completed successfully!");
+    println!("  Quote state: {:?}", melt_response.state);
+    assert_eq!(melt_response.quote, melt_quote.quote);
+}

+ 277 - 0
crates/cdk/src/mint/melt/tests/locktime_spending_conditions_tests.rs

@@ -0,0 +1,277 @@
+//! Locktime tests for melt functionality
+//!
+//! These tests verify that the mint correctly validates locktime spending conditions
+//! during melt operations, including spending after locktime expiry.
+
+use std::str::FromStr;
+
+use cdk_common::dhke::construct_proofs;
+use cdk_common::melt::MeltQuoteRequest;
+use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
+use cdk_common::Amount;
+use cdk_common::SpendingConditionVerification;
+
+use crate::test_helpers::nut10::{create_test_keypair, unzip3, TestMintHelper};
+use crate::util::unix_time;
+
+/// Test: P2PK with locktime - spending after expiry
+///
+/// Creates P2PK proofs with locktime and verifies:
+/// 1. Melting before locktime with wrong key fails
+/// 2. Melting after locktime with any key succeeds (anyone-can-spend)
+#[tokio::test]
+async fn test_p2pk_post_locktime_anyone_can_spend() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypairs
+    let (_alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, _bob_pubkey) = create_test_keypair();
+
+    println!("Alice pubkey: {}", alice_pubkey);
+
+    // Step 1: Create regular unencumbered proofs
+    let input_amount = Amount::from(20);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create P2PK spending conditions with locktime in the past (already expired)
+    // Locktime is 1 hour ago - so it's already expired
+    let locktime = unix_time() - 3600;
+
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime),     // Locktime in the past (expired)
+            pubkeys: None,                // no additional pubkeys
+            refund_keys: None,            // NO refund keys - anyone can spend!
+            num_sigs: None,               // default (1)
+            sig_flag: SigFlag::SigInputs, // SIG_INPUTS flag
+            num_sigs_refund: None,        // default (1)
+        }),
+    );
+    println!(
+        "Created P2PK spending conditions with expired locktime: {}",
+        locktime
+    );
+
+    // Split the input amount
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    // Create blinded messages
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+    println!("Created {} P2PK outputs with locktime", p2pk_outputs.len());
+
+    // Step 3: Swap for P2PK proofs
+    let swap_request = cdk_common::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for P2PK proofs");
+    println!("Swap successful! Got BlindSignatures");
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = p2pk_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} P2PK proof(s) [{}]",
+        p2pk_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 5: Create a real melt quote
+    let bolt11_str = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq";
+    let bolt11 = cdk_common::Bolt11Invoice::from_str(bolt11_str).unwrap();
+
+    let melt_quote_request = cdk_common::MeltQuoteBolt11Request {
+        request: bolt11,
+        unit: cdk_common::CurrencyUnit::Sat,
+        options: None,
+    };
+
+    let melt_quote = mint
+        .get_melt_quote(MeltQuoteRequest::Bolt11(melt_quote_request))
+        .await
+        .unwrap();
+    println!("Created melt quote: {}", melt_quote.quote);
+
+    // Step 6: Try to melt with Bob's signature (wrong key, but locktime expired so should work)
+    let mut proofs_bob_signed = p2pk_proofs.clone();
+
+    // Sign with Bob's key (not Alice's)
+    for proof in proofs_bob_signed.iter_mut() {
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let melt_request_bob =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), proofs_bob_signed.into(), None);
+
+    // After locktime expiry, anyone can spend (signature verification is skipped)
+    melt_request_bob.verify_spending_conditions().unwrap();
+    println!("✓ Post-locktime spending conditions verified successfully (anyone-can-spend)");
+
+    // Perform the actual melt
+    let melt_response = mint.melt(&melt_request_bob).await.unwrap();
+    println!("✓ Melt operation completed successfully with Bob's key after locktime!");
+    println!("  Quote state: {:?}", melt_response.state);
+    assert_eq!(melt_response.quote, melt_quote.quote);
+}
+
+/// Test: P2PK with future locktime - must use correct key before expiry
+///
+/// Creates P2PK proofs with future locktime and verifies:
+/// 1. Melting with wrong key before locktime fails
+/// 2. Melting with correct key before locktime succeeds
+#[tokio::test]
+async fn test_p2pk_before_locktime_requires_correct_key() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypairs
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, _bob_pubkey) = create_test_keypair();
+
+    println!("Alice pubkey: {}", alice_pubkey);
+
+    // Step 1: Create regular unencumbered proofs
+    let input_amount = Amount::from(20);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create P2PK spending conditions with locktime FAR in the future
+    // Locktime is 1 year from now - definitely not expired yet
+    let locktime = unix_time() + 365 * 24 * 60 * 60;
+
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                Some(locktime),           // Locktime in the future
+                None,                     // no additional pubkeys
+                None,                     // no refund keys
+                None,                     // default num_sigs (1)
+                Some(SigFlag::SigInputs), // SIG_INPUTS flag
+                None,                     // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+    println!(
+        "Created P2PK spending conditions with future locktime: {}",
+        locktime
+    );
+
+    // Split the input amount
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    // Create blinded messages
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+    println!("Created {} P2PK outputs with locktime", p2pk_outputs.len());
+
+    // Step 3: Swap for P2PK proofs
+    let swap_request = cdk_common::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for P2PK proofs");
+    println!("Swap successful! Got BlindSignatures");
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = p2pk_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} P2PK proof(s) [{}]",
+        p2pk_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 5: Create a real melt quote
+    let bolt11_str = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq";
+    let bolt11 = cdk_common::Bolt11Invoice::from_str(bolt11_str).unwrap();
+
+    let melt_quote_request = cdk_common::MeltQuoteBolt11Request {
+        request: bolt11,
+        unit: cdk_common::CurrencyUnit::Sat,
+        options: None,
+    };
+
+    let melt_quote = mint
+        .get_melt_quote(MeltQuoteRequest::Bolt11(melt_quote_request))
+        .await
+        .unwrap();
+    println!("Created melt quote: {}", melt_quote.quote);
+
+    // Step 6: Try to melt with Bob's signature (wrong key, locktime not expired)
+    let mut proofs_bob_signed = p2pk_proofs.clone();
+
+    // Sign with Bob's key (not Alice's)
+    for proof in proofs_bob_signed.iter_mut() {
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let melt_request_bob =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), proofs_bob_signed.into(), None);
+
+    // Before locktime expiry, wrong key should fail
+    let result = melt_request_bob.verify_spending_conditions();
+    assert!(
+        result.is_err(),
+        "Should fail with wrong key before locktime"
+    );
+    println!("✓ Melting with Bob's key before locktime failed verification as expected");
+
+    // Also verify the actual melt fails
+    let melt_result = mint.melt(&melt_request_bob).await;
+    assert!(
+        melt_result.is_err(),
+        "Actual melt should also fail with wrong key"
+    );
+    println!("✓ Actual melt with Bob's key before locktime also failed as expected");
+
+    // Step 7: Now melt with Alice's signature (correct key)
+    let mut proofs_alice_signed = p2pk_proofs.clone();
+
+    // Sign with Alice's key (correct)
+    for proof in proofs_alice_signed.iter_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let melt_request_alice =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), proofs_alice_signed.into(), None);
+
+    // Verify spending conditions pass
+    melt_request_alice.verify_spending_conditions().unwrap();
+    println!("✓ Pre-locktime spending conditions verified successfully with Alice's key");
+
+    // Perform the actual melt
+    let melt_response = mint.melt(&melt_request_alice).await.unwrap();
+    println!("✓ Melt operation completed successfully with Alice's key before locktime!");
+    println!("  Quote state: {:?}", melt_response.state);
+    assert_eq!(melt_response.quote, melt_quote.quote);
+}

+ 5 - 0
crates/cdk/src/mint/melt/tests/mod.rs

@@ -0,0 +1,5 @@
+mod htlc_sigall_spending_conditions_tests;
+mod htlc_spending_conditions_tests;
+mod locktime_spending_conditions_tests;
+mod p2pk_sigall_spending_conditions_tests;
+mod p2pk_spending_conditions_tests;

+ 167 - 0
crates/cdk/src/mint/melt/tests/p2pk_sigall_spending_conditions_tests.rs

@@ -0,0 +1,167 @@
+//! P2PK SIG_ALL tests for melt functionality
+//!
+//! These tests verify that the mint correctly enforces SIG_ALL flag behavior
+//! during melt operations.
+
+use cdk_common::dhke::construct_proofs;
+use cdk_common::melt::MeltQuoteRequest;
+use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
+use cdk_common::Amount;
+
+use crate::test_helpers::nut10::{create_test_keypair, unzip3, TestMintHelper};
+
+/// Test: P2PK with SIG_ALL flag requires transaction signature
+///
+/// Creates P2PK proofs with SIG_ALL flag and verifies:
+/// 1. Melting without signature is rejected
+/// 2. Melting with SIG_INPUTS signatures (individual proof signatures) is rejected
+/// 3. Melting with SIG_ALL signature (transaction signature) succeeds
+#[tokio::test]
+async fn test_p2pk_sig_all_requires_transaction_signature() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for P2PK
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    println!("Alice pubkey: {}", alice_pubkey);
+
+    // Step 1: Create regular unencumbered proofs that we'll swap for P2PK proofs
+    // Invoice is 10 sats, fee reserve is 100% (10 sats), so we need 20 sats total
+    let input_amount = Amount::from(20);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create P2PK blinded messages (outputs locked to alice_pubkey) with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                None,                  // no locktime
+                None,                  // no additional pubkeys
+                None,                  // no refund keys
+                None,                  // default num_sigs (1)
+                Some(SigFlag::SigAll), // SIG_ALL flag
+                None,                  // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+    println!("Created P2PK spending conditions with SIG_ALL flag");
+
+    // Split the input amount into power-of-2 denominations
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    // Create blinded messages for each split amount
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    println!(
+        "Created {} P2PK outputs locked to alice",
+        p2pk_outputs.len()
+    );
+
+    // Step 3: Swap regular proofs for P2PK proofs (no signature needed on inputs)
+    let swap_request = cdk_common::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for P2PK proofs");
+    println!("Swap successful! Got BlindSignatures for our P2PK outputs");
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = p2pk_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} P2PK proof(s) [{}]",
+        p2pk_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 5: Create a real melt quote that we'll use for all tests
+    use cdk_common::SpendingConditionVerification;
+    use std::str::FromStr;
+    let bolt11_str = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq";
+    let bolt11 = cdk_common::Bolt11Invoice::from_str(bolt11_str).unwrap();
+
+    let melt_quote_request = cdk_common::MeltQuoteBolt11Request {
+        request: bolt11,
+        unit: cdk_common::CurrencyUnit::Sat,
+        options: None,
+    };
+
+    let melt_quote = mint
+        .get_melt_quote(MeltQuoteRequest::Bolt11(melt_quote_request))
+        .await
+        .unwrap();
+    println!("Created melt quote: {}", melt_quote.quote);
+
+    // Step 6: Try to melt P2PK proof WITHOUT signature (should fail)
+
+    let melt_request_no_sig =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), p2pk_proofs.clone().into(), None);
+
+    let result = melt_request_no_sig.verify_spending_conditions();
+    assert!(result.is_err(), "Should fail without signature");
+    println!("✓ Melting WITHOUT signature failed verification as expected");
+
+    // Also verify the actual melt fails
+    let melt_result = mint.melt(&melt_request_no_sig).await;
+    assert!(
+        melt_result.is_err(),
+        "Actual melt should also fail without signature"
+    );
+    println!("✓ Actual melt WITHOUT signature also failed as expected");
+
+    // Step 7: Sign all proofs individually (SIG_INPUTS way) - should fail for SIG_ALL
+    let mut melt_request_sig_inputs =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), p2pk_proofs.clone().into(), None);
+
+    // Sign each proof individually (SIG_INPUTS mode)
+    for proof in melt_request_sig_inputs.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = melt_request_sig_inputs.verify_spending_conditions();
+    assert!(
+        result.is_err(),
+        "Should fail - SIG_INPUTS signatures not valid for SIG_ALL"
+    );
+    println!("✓ Melting with SIG_INPUTS signatures failed verification as expected");
+
+    // Also verify the actual melt fails
+    let melt_result = mint.melt(&melt_request_sig_inputs).await;
+    assert!(
+        melt_result.is_err(),
+        "Actual melt should also fail with SIG_INPUTS signatures"
+    );
+    println!("✓ Actual melt with SIG_INPUTS signatures also failed as expected");
+
+    // Step 8: Sign the transaction with SIG_ALL and perform the melt
+    let mut melt_request =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), p2pk_proofs.clone().into(), None);
+
+    // Use sign_sig_all to sign the transaction (signature goes on first proof's witness)
+    melt_request.sign_sig_all(alice_secret.clone()).unwrap();
+
+    // Verify spending conditions pass
+    melt_request.verify_spending_conditions().unwrap();
+    println!("✓ P2PK SIG_ALL spending conditions verified successfully");
+
+    // Perform the actual melt - this also verifies spending conditions internally
+    let melt_response = mint.melt(&melt_request).await.unwrap();
+    println!("✓ Melt operation completed successfully!");
+    println!("  Quote state: {:?}", melt_response.state);
+    assert_eq!(melt_response.quote, melt_quote.quote);
+}

+ 135 - 0
crates/cdk/src/mint/melt/tests/p2pk_spending_conditions_tests.rs

@@ -0,0 +1,135 @@
+//! Basic P2PK tests for melt functionality (SIG_INPUTS mode)
+//!
+//! These tests verify that the mint correctly validates basic P2PK spending conditions
+//! during melt operations.
+
+use std::str::FromStr;
+
+use cdk_common::dhke::construct_proofs;
+use cdk_common::melt::MeltQuoteRequest;
+use cdk_common::nuts::SpendingConditions;
+use cdk_common::Amount;
+use cdk_common::SpendingConditionVerification;
+
+use crate::test_helpers::nut10::{create_test_keypair, unzip3, TestMintHelper};
+
+/// Test: Basic P2PK with SIG_INPUTS (default mode)
+///
+/// Creates P2PK proofs with default SIG_INPUTS flag and verifies:
+/// 1. Melting without signatures is rejected
+/// 2. Melting with signatures on all proofs succeeds
+#[tokio::test]
+async fn test_p2pk_basic_sig_inputs() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for P2PK
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    println!("Alice pubkey: {}", alice_pubkey);
+
+    // Step 1: Create regular unencumbered proofs that we'll swap for P2PK proofs
+    let input_amount = Amount::from(20);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create P2PK blinded messages (outputs locked to alice_pubkey) with default SIG_INPUTS
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        None, // No additional conditions - uses default SIG_INPUTS
+    );
+    println!("Created P2PK spending conditions with default SIG_INPUTS flag");
+
+    // Split the input amount into power-of-2 denominations
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    // Create blinded messages for each split amount
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    println!(
+        "Created {} P2PK outputs locked to alice",
+        p2pk_outputs.len()
+    );
+
+    // Step 3: Swap regular proofs for P2PK proofs (no signature needed on inputs)
+    let swap_request = cdk_common::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for P2PK proofs");
+    println!("Swap successful! Got BlindSignatures for our P2PK outputs");
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = p2pk_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} P2PK proof(s) [{}]",
+        p2pk_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 5: Create a real melt quote that we'll use for all tests
+    let bolt11_str = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq";
+    let bolt11 = cdk_common::Bolt11Invoice::from_str(bolt11_str).unwrap();
+
+    let melt_quote_request = cdk_common::MeltQuoteBolt11Request {
+        request: bolt11,
+        unit: cdk_common::CurrencyUnit::Sat,
+        options: None,
+    };
+
+    let melt_quote = mint
+        .get_melt_quote(MeltQuoteRequest::Bolt11(melt_quote_request))
+        .await
+        .unwrap();
+    println!("Created melt quote: {}", melt_quote.quote);
+
+    // Step 6: Try to melt P2PK proof WITHOUT signature (should fail)
+    let melt_request_no_sig =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), p2pk_proofs.clone().into(), None);
+
+    let result = melt_request_no_sig.verify_spending_conditions();
+    assert!(result.is_err(), "Should fail without signature");
+    println!("✓ Melting WITHOUT signature failed verification as expected");
+
+    // Also verify the actual melt fails
+    let melt_result = mint.melt(&melt_request_no_sig).await;
+    assert!(
+        melt_result.is_err(),
+        "Actual melt should also fail without signature"
+    );
+    println!("✓ Actual melt WITHOUT signature also failed as expected");
+
+    // Step 7: Sign all proofs individually (SIG_INPUTS mode) and perform the melt
+    let mut proofs_signed = p2pk_proofs.clone();
+
+    // Sign each proof individually (SIG_INPUTS mode)
+    for proof in proofs_signed.iter_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let melt_request =
+        cdk_common::MeltRequest::new(melt_quote.quote.clone(), proofs_signed.into(), None);
+
+    // Verify spending conditions pass
+    melt_request.verify_spending_conditions().unwrap();
+    println!("✓ P2PK SIG_INPUTS spending conditions verified successfully");
+
+    // Perform the actual melt - this also verifies spending conditions internally
+    let melt_response = mint.melt(&melt_request).await.unwrap();
+    println!("✓ Melt operation completed successfully!");
+    println!("  Quote state: {:?}", melt_response.state);
+    assert_eq!(melt_response.quote, melt_quote.quote);
+}

+ 4 - 32
crates/cdk/src/mint/mod.rs

@@ -10,10 +10,9 @@ use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
 #[cfg(feature = "auth")]
 use cdk_common::database::DynMintAuthDatabase;
 use cdk_common::database::{self, DynMintDatabase};
-use cdk_common::nuts::{self, BlindSignature, BlindedMessage, CurrencyUnit, Id, Kind};
+use cdk_common::nuts::{BlindSignature, BlindedMessage, CurrencyUnit, Id};
 use cdk_common::payment::{DynMintPayment, WaitPaymentResponse};
 pub use cdk_common::quote_id::QuoteId;
-use cdk_common::secret;
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::global;
 use cdk_signatory::signatory::{Signatory, SignatoryKeySet};
@@ -853,39 +852,12 @@ impl Mint {
     /// Verify [`Proof`] meets conditions and is signed
     #[tracing::instrument(skip_all)]
     pub async fn verify_proofs(&self, proofs: Proofs) -> Result<(), Error> {
+        // This ignore P2PK and HTLC, as all NUT-10 spending conditions are
+        // checked elsewhere.
         #[cfg(feature = "prometheus")]
         global::inc_in_flight_requests("verify_proofs");
 
-        let result = async {
-            proofs
-                .iter()
-                .map(|proof| {
-                    // Check if secret is a nut10 secret with conditions
-                    if let Ok(secret) =
-                        <&secret::Secret as TryInto<nuts::nut10::Secret>>::try_into(&proof.secret)
-                    {
-                        // Checks and verifies known secret kinds.
-                        // If it is an unknown secret kind it will be treated as a normal secret.
-                        // Spending conditions will **not** be check. It is up to the wallet to ensure
-                        // only supported secret kinds are used as there is no way for the mint to
-                        // enforce only signing supported secrets as they are blinded at
-                        // that point.
-                        match secret.kind() {
-                            Kind::P2PK => {
-                                proof.verify_p2pk()?;
-                            }
-                            Kind::HTLC => {
-                                proof.verify_htlc()?;
-                            }
-                        }
-                    }
-                    Ok(())
-                })
-                .collect::<Result<Vec<()>, Error>>()?;
-
-            self.signatory.verify_proofs(proofs).await
-        }
-        .await;
+        let result = self.signatory.verify_proofs(proofs).await;
 
         #[cfg(feature = "prometheus")]
         {

+ 12 - 15
crates/cdk/src/mint/swap/mod.rs

@@ -3,12 +3,15 @@ use cdk_prometheus::METRICS;
 use swap_saga::SwapSaga;
 use tracing::instrument;
 
-use super::nut11::{enforce_sig_flag, EnforceSigFlag};
-use super::{Mint, SigFlag, SwapRequest, SwapResponse};
+use super::{Mint, SwapRequest, SwapResponse};
 use crate::Error;
+use cdk_common::SpendingConditionVerification;
 
 pub mod swap_saga;
 
+#[cfg(test)]
+mod tests;
+
 impl Mint {
     /// Process Swap
     #[instrument(skip_all)]
@@ -22,6 +25,13 @@ impl Mint {
         swap_request.input_amount()?;
         swap_request.output_amount()?;
 
+        // Verify spending conditions (NUT-10/NUT-11/NUT-14), i.e. P2PK
+        // and HTLC (including SIGALL)
+        swap_request.verify_spending_conditions()?;
+
+        // 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())
@@ -34,9 +44,6 @@ impl Mint {
                     err
                 })?;
 
-        // Verify signature flag (no DB needed)
-        self.validate_sig_flag(&swap_request).await?;
-
         // Step 1: Initialize the swap saga
         let init_saga = SwapSaga::new(self, self.localstore.clone(), self.pubsub_manager.clone());
 
@@ -65,16 +72,6 @@ impl Mint {
         Ok(response)
     }
 
-    async fn validate_sig_flag(&self, swap_request: &SwapRequest) -> Result<(), Error> {
-        let EnforceSigFlag { sig_flag, .. } = enforce_sig_flag(swap_request.inputs().clone());
-
-        if sig_flag == SigFlag::SigAll {
-            swap_request.verify_sig_all()?;
-        }
-
-        Ok(())
-    }
-
     #[cfg(feature = "prometheus")]
     fn record_swap_failure(&self, operation: &str) {
         METRICS.dec_in_flight_requests(operation);

+ 389 - 0
crates/cdk/src/mint/swap/tests/htlc_sigall_spending_conditions_tests.rs

@@ -0,0 +1,389 @@
+//! HTLC SIG_ALL tests for swap functionality
+//!
+//! These tests verify that the mint correctly enforces SIG_ALL flag behavior for HTLC
+
+use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
+use cdk_common::Amount;
+
+use crate::test_helpers::nut10::{
+    create_test_hash_and_preimage, create_test_keypair, unzip3, TestMintHelper,
+};
+
+/// Test: HTLC SIG_ALL requiring preimage and one signature
+///
+/// Creates HTLC-locked proofs with SIG_ALL flag and verifies:
+/// 1. Spending with only preimage fails (signature required)
+/// 2. Spending with only signature fails (preimage required)
+/// 3. Spending with both preimage and SIG_ALL signature succeeds
+#[tokio::test]
+async fn test_htlc_sig_all_requiring_preimage_and_one_signature() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for Alice
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+
+    // Create hash and preimage
+    let (hash, preimage) = create_test_hash_and_preimage();
+
+    println!("Alice pubkey: {}", alice_pubkey);
+    println!("Hash: {}", hash);
+    println!("Preimage: {}", preimage);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create HTLC spending conditions with SIG_ALL flag (hash locked to Alice's key)
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![alice_pubkey]),
+            refund_keys: None,
+            num_sigs: None,            // Default (1)
+            sig_flag: SigFlag::SigAll, // <-- SIG_ALL flag
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+    println!("Created HTLC spending conditions with SIG_ALL flag");
+
+    // Step 3: Create HTLC blinded messages (outputs)
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+    println!(
+        "Created {} HTLC outputs locked to alice with hash",
+        htlc_outputs.len()
+    );
+
+    // Step 4: Swap regular proofs for HTLC proofs (no signature needed on inputs)
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for HTLC proofs");
+    println!("Swap successful! Got BlindSignatures for our HTLC outputs");
+
+    // Step 5: Construct the HTLC proofs
+    use cdk_common::dhke::construct_proofs;
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = htlc_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} HTLC proof(s) [{}]",
+        htlc_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 6: Try to spend with only preimage (should fail - signature required)
+    use crate::test_helpers::mint::create_test_blinded_messages;
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_preimage_only =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    // Add only preimage to first proof (no signature)
+    swap_request_preimage_only.inputs_mut()[0].add_preimage(preimage.clone());
+
+    let result = mint.process_swap_request(swap_request_preimage_only).await;
+    assert!(
+        result.is_err(),
+        "Should fail with only preimage (no signature)"
+    );
+    println!(
+        "✓ Spending with ONLY preimage failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Try to spend with only signature (should fail - preimage required)
+    let mut swap_request_signature_only =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    // Add only SIG_ALL signature to first proof (no preimage)
+    // Note: Must create HTLCWitness first, otherwise sign_sig_all creates P2PKWitness
+    swap_request_signature_only.inputs_mut()[0].add_preimage(String::new()); // Empty preimage
+    swap_request_signature_only
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_signature_only).await;
+    assert!(
+        result.is_err(),
+        "Should fail with only signature (no preimage)"
+    );
+    println!(
+        "✓ Spending with ONLY signature failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 8: Now try to spend with both preimage and SIG_ALL signature
+    let mut swap_request_both =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    // Add preimage to first proof
+    swap_request_both.inputs_mut()[0].add_preimage(preimage.clone());
+    // Add SIG_ALL signature
+    swap_request_both
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_both).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed with correct preimage and SIG_ALL signature: {:?}",
+        result.err()
+    );
+    println!("✓ HTLC SIG_ALL spent successfully with correct preimage AND signature");
+}
+
+/// Test: HTLC SIG_ALL with wrong preimage
+///
+/// Verifies that providing an incorrect preimage fails even with correct SIG_ALL signature
+#[tokio::test]
+async fn test_htlc_sig_all_wrong_preimage() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (hash, _correct_preimage) = create_test_hash_and_preimage();
+
+    // Mint regular proofs and swap for HTLC SIG_ALL proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![alice_pubkey]),
+            refund_keys: None,
+            num_sigs: None,
+            sig_flag: SigFlag::SigAll,
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    use cdk_common::dhke::construct_proofs;
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Try to spend with WRONG preimage (but correct SIG_ALL signature)
+    use crate::test_helpers::mint::create_test_blinded_messages;
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    let wrong_preimage = "this_is_the_wrong_preimage";
+    swap_request.inputs_mut()[0].add_preimage(wrong_preimage.to_string());
+    swap_request.sign_sig_all(alice_secret.clone()).unwrap();
+
+    let result = mint.process_swap_request(swap_request).await;
+    assert!(result.is_err(), "Should fail with wrong preimage");
+    println!(
+        "✓ HTLC SIG_ALL with wrong preimage failed as expected: {:?}",
+        result.err()
+    );
+}
+
+/// Test: HTLC SIG_ALL locktime after expiry (refund path)
+///
+/// Verifies that after locktime expires, refund keys can spend without preimage using SIG_ALL
+#[tokio::test]
+async fn test_htlc_sig_all_locktime_after_expiry() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (_alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (hash, _preimage) = create_test_hash_and_preimage();
+
+    // Create HTLC with locktime in the PAST (already expired) and Bob as refund key
+    let past_locktime = cdk_common::util::unix_time() - 1000;
+
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: Some(past_locktime),
+            pubkeys: Some(vec![alice_pubkey]),
+            refund_keys: Some(vec![bob_pubkey]),
+            num_sigs: None,
+            sig_flag: SigFlag::SigAll,
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    use cdk_common::dhke::construct_proofs;
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // After locktime, Bob (refund key) can spend WITHOUT preimage using SIG_ALL
+    use crate::test_helpers::mint::create_test_blinded_messages;
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    // Bob signs with SIG_ALL (no preimage needed after locktime)
+    // Note: Must call add_preimage first (even with empty string) to create HTLC witness
+    swap_request.inputs_mut()[0].add_preimage(String::new());
+    swap_request.sign_sig_all(bob_secret.clone()).unwrap();
+
+    let result = mint.process_swap_request(swap_request).await;
+    assert!(
+        result.is_ok(),
+        "Bob should be able to spend after locktime without preimage: {:?}",
+        result.err()
+    );
+    println!("✓ HTLC SIG_ALL spent by refund key after locktime (no preimage needed)");
+}
+
+/// Test: HTLC SIG_ALL with multisig (preimage + 2-of-3 signatures)
+///
+/// Verifies that HTLC SIG_ALL can require preimage AND multiple signatures
+#[tokio::test]
+async fn test_htlc_sig_all_multisig_2of3() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_charlie_secret, charlie_pubkey) = create_test_keypair();
+    let (hash, preimage) = create_test_hash_and_preimage();
+
+    // Create HTLC requiring preimage + 2-of-3 signatures (Alice, Bob, Charlie) with SIG_ALL
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![alice_pubkey, bob_pubkey, charlie_pubkey]),
+            refund_keys: None,
+            num_sigs: Some(2), // Require 2 of 3
+            sig_flag: SigFlag::SigAll,
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    use cdk_common::dhke::construct_proofs;
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Try with preimage + only 1 SIG_ALL signature (should fail - need 2)
+    use crate::test_helpers::mint::create_test_blinded_messages;
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_one_sig =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    swap_request_one_sig.inputs_mut()[0].add_preimage(preimage.clone());
+    swap_request_one_sig
+        .sign_sig_all(alice_secret.clone())
+        .unwrap(); // Only Alice signs
+
+    let result = mint.process_swap_request(swap_request_one_sig).await;
+    assert!(
+        result.is_err(),
+        "Should fail with only 1 signature (need 2)"
+    );
+    println!("✓ HTLC SIG_ALL with 1-of-3 signatures failed as expected");
+
+    // Now with preimage + 2 SIG_ALL signatures (Alice and Bob) - should succeed
+    let mut swap_request_two_sigs =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    swap_request_two_sigs.inputs_mut()[0].add_preimage(preimage.clone());
+    swap_request_two_sigs
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+    swap_request_two_sigs
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_two_sigs).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed with preimage + 2-of-3 SIG_ALL signatures: {:?}",
+        result.err()
+    );
+    println!("✓ HTLC SIG_ALL spent with preimage + 2-of-3 signatures");
+}

+ 396 - 0
crates/cdk/src/mint/swap/tests/htlc_spending_conditions_tests.rs

@@ -0,0 +1,396 @@
+//! HTLC (NUT-14) tests for swap functionality
+//!
+//! These tests verify that the mint correctly validates HTLC spending conditions
+//! during swap operations, including:
+//! - Hash preimage verification
+//! - Locktime enforcement
+//! - Refund keys
+//! - Signature validation
+
+use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
+use cdk_common::Amount;
+
+use crate::test_helpers::nut10::{
+    create_test_hash_and_preimage, create_test_keypair, unzip3, TestMintHelper,
+};
+
+/// Test: HTLC requiring preimage and one signature
+///
+/// Creates HTLC-locked proofs and verifies:
+/// 1. Spending with only preimage fails (signature required)
+/// 2. Spending with only signature fails (preimage required)
+/// 3. Spending with both preimage and signature succeeds
+#[tokio::test]
+async fn test_htlc_requiring_preimage_and_one_signature() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for Alice
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+
+    // Create hash and preimage
+    let (hash, preimage) = create_test_hash_and_preimage();
+
+    println!("Alice pubkey: {}", alice_pubkey);
+    println!("Hash: {}", hash);
+    println!("Preimage: {}", preimage);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create HTLC spending conditions (hash locked to Alice's key)
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![alice_pubkey]),
+            refund_keys: None,
+            num_sigs: None, // Default (1)
+            sig_flag: SigFlag::default(),
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+    println!("Created HTLC spending conditions");
+
+    // Step 3: Create HTLC blinded messages (outputs)
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+    println!(
+        "Created {} HTLC outputs locked to alice with hash",
+        htlc_outputs.len()
+    );
+
+    // Step 4: Swap regular proofs for HTLC proofs (no signature needed on inputs)
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for HTLC proofs");
+    println!("Swap successful! Got BlindSignatures for our HTLC outputs");
+
+    // Step 5: Construct the HTLC proofs
+    use cdk_common::dhke::construct_proofs;
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = htlc_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} HTLC proof(s) [{}]",
+        htlc_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 6: Try to spend with only preimage (should fail - signature required)
+    use crate::test_helpers::mint::create_test_blinded_messages;
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_preimage_only =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    // Add only preimage (no signature)
+    for proof in swap_request_preimage_only.inputs_mut() {
+        proof.add_preimage(preimage.clone());
+    }
+
+    let result = mint.process_swap_request(swap_request_preimage_only).await;
+    assert!(
+        result.is_err(),
+        "Should fail with only preimage (no signature)"
+    );
+    println!(
+        "✓ Spending with ONLY preimage failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Try to spend with only signature (should fail - preimage required)
+    let mut swap_request_signature_only =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    // Add only signature (no preimage)
+    for proof in swap_request_signature_only.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_signature_only).await;
+    assert!(
+        result.is_err(),
+        "Should fail with only signature (no preimage)"
+    );
+    println!(
+        "✓ Spending with ONLY signature failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 8: Now try to spend the HTLC proofs with correct preimage + signature
+    let mut swap_request_both =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    // Add preimage and sign all proofs
+    for proof in swap_request_both.inputs_mut() {
+        proof.add_preimage(preimage.clone());
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_both).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed with correct preimage and signature: {:?}",
+        result.err()
+    );
+    println!("✓ HTLC spent successfully with correct preimage AND signature");
+}
+
+/// Test: HTLC with wrong preimage
+///
+/// Verifies that providing an incorrect preimage fails even with correct signature
+#[tokio::test]
+async fn test_htlc_wrong_preimage() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (hash, _correct_preimage) = create_test_hash_and_preimage();
+
+    // Mint regular proofs and swap for HTLC proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![alice_pubkey]),
+            refund_keys: None,
+            num_sigs: None,
+            sig_flag: SigFlag::default(),
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    use cdk_common::dhke::construct_proofs;
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Try to spend with WRONG preimage (but correct signature)
+    use crate::test_helpers::mint::create_test_blinded_messages;
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    let wrong_preimage = "this_is_the_wrong_preimage";
+    for proof in swap_request.inputs_mut() {
+        proof.add_preimage(wrong_preimage.to_string());
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request).await;
+    assert!(result.is_err(), "Should fail with wrong preimage");
+    println!(
+        "✓ HTLC with wrong preimage failed as expected: {:?}",
+        result.err()
+    );
+}
+
+/// Test: HTLC locktime after expiry (refund path)
+///
+/// Verifies that after locktime expires, refund keys can spend without preimage
+#[tokio::test]
+async fn test_htlc_locktime_after_expiry() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (_alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (hash, _preimage) = create_test_hash_and_preimage();
+
+    // Create HTLC with locktime in the PAST (already expired) and Bob as refund key
+    let past_locktime = cdk_common::util::unix_time() - 1000;
+
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: Some(past_locktime),
+            pubkeys: Some(vec![alice_pubkey]),
+            refund_keys: Some(vec![bob_pubkey]),
+            num_sigs: None,
+            sig_flag: SigFlag::default(),
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    use cdk_common::dhke::construct_proofs;
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // After locktime, Bob (refund key) can spend WITHOUT preimage
+    use crate::test_helpers::mint::create_test_blinded_messages;
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    // Bob signs (no preimage needed after locktime)
+    // Note: Must call add_preimage first (even with empty string) to create HTLC witness,
+    // otherwise sign_p2pk creates P2PK witness instead
+    for proof in swap_request.inputs_mut() {
+        proof.add_preimage(String::new()); // Empty preimage for refund path
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request).await;
+    assert!(
+        result.is_ok(),
+        "Bob should be able to spend after locktime without preimage: {:?}",
+        result.err()
+    );
+    println!("✓ HTLC spent by refund key after locktime (no preimage needed)");
+}
+
+/// Test: HTLC with multisig (preimage + 2-of-3 signatures)
+///
+/// Verifies that HTLC can require preimage AND multiple signatures
+#[tokio::test]
+async fn test_htlc_multisig_2of3() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_charlie_secret, charlie_pubkey) = create_test_keypair();
+    let (hash, preimage) = create_test_hash_and_preimage();
+
+    // Create HTLC requiring preimage + 2-of-3 signatures (Alice, Bob, Charlie)
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    let spending_conditions = SpendingConditions::new_htlc_hash(
+        &hash,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![alice_pubkey, bob_pubkey, charlie_pubkey]),
+            refund_keys: None,
+            num_sigs: Some(2), // Require 2 of 3
+            sig_flag: SigFlag::default(),
+            num_sigs_refund: None,
+        }),
+    )
+    .unwrap();
+
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (htlc_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), htlc_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    use cdk_common::dhke::construct_proofs;
+    let htlc_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Try with preimage + only 1 signature (should fail - need 2)
+    use crate::test_helpers::mint::create_test_blinded_messages;
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_one_sig =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    for proof in swap_request_one_sig.inputs_mut() {
+        proof.add_preimage(preimage.clone());
+        proof.sign_p2pk(alice_secret.clone()).unwrap(); // Only Alice signs
+    }
+
+    let result = mint.process_swap_request(swap_request_one_sig).await;
+    assert!(
+        result.is_err(),
+        "Should fail with only 1 signature (need 2)"
+    );
+    println!("✓ HTLC with 1-of-3 signatures failed as expected");
+
+    // Now with preimage + 2 signatures (Alice and Bob) - should succeed
+    let mut swap_request_two_sigs =
+        cdk_common::nuts::SwapRequest::new(htlc_proofs.clone(), new_outputs.clone());
+
+    for proof in swap_request_two_sigs.inputs_mut() {
+        proof.add_preimage(preimage.clone());
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_two_sigs).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed with preimage + 2-of-3 signatures: {:?}",
+        result.err()
+    );
+    println!("✓ HTLC spent with preimage + 2-of-3 signatures");
+}

+ 4 - 0
crates/cdk/src/mint/swap/tests/mod.rs

@@ -0,0 +1,4 @@
+mod htlc_sigall_spending_conditions_tests;
+mod htlc_spending_conditions_tests;
+mod p2pk_sigall_spending_conditions_tests;
+mod p2pk_spending_conditions_tests;

+ 1441 - 0
crates/cdk/src/mint/swap/tests/p2pk_sigall_spending_conditions_tests.rs

@@ -0,0 +1,1441 @@
+//! P2PK SIG_ALL tests for swap functionality
+//!
+//! These tests verify that the mint correctly enforces SIG_ALL flag behavior
+
+use crate::util::unix_time;
+use cdk_common::dhke::construct_proofs;
+use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
+use cdk_common::Amount;
+
+use crate::test_helpers::mint::create_test_blinded_messages;
+use crate::test_helpers::nut10::{create_test_keypair, unzip3, TestMintHelper};
+
+/// Test: P2PK with SIG_ALL flag requires transaction signature
+///
+/// Creates P2PK proofs with SIG_ALL flag and verifies:
+/// 1. Spending without signature is rejected
+/// 2. Spending with SIG_INPUTS signatures (individual proof signatures) is rejected
+/// 3. Spending with SIG_ALL signature (transaction signature) succeeds
+#[tokio::test]
+async fn test_p2pk_sig_all_requires_transaction_signature() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for P2PK
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    println!("Alice pubkey: {}", alice_pubkey);
+
+    // Step 1: Create regular unencumbered proofs that we'll swap for P2PK proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create P2PK blinded messages (outputs locked to alice_pubkey) with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                None,                  // no locktime
+                None,                  // no additional pubkeys
+                None,                  // no refund keys
+                None,                  // default num_sigs (1)
+                Some(SigFlag::SigAll), // SIG_ALL flag
+                None,                  // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+    println!("Created P2PK spending conditions with SIG_ALL flag");
+
+    // Split the input amount into power-of-2 denominations
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    // Create blinded messages for each split amount
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    println!(
+        "Created {} P2PK outputs locked to alice",
+        p2pk_outputs.len()
+    );
+
+    // Step 3: Swap regular proofs for P2PK proofs (no signature needed on inputs)
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for P2PK proofs");
+    println!("Swap successful! Got BlindSignatures for our P2PK outputs");
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = p2pk_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} P2PK proof(s) [{}]",
+        p2pk_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 5: Try to spend P2PK proof WITHOUT signature (should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let swap_request_no_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    let result = mint.process_swap_request(swap_request_no_sig).await;
+    assert!(result.is_err(), "Should fail without signature");
+    println!(
+        "✓ Spending WITHOUT signature failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 6: Sign all proofs individually (SIG_INPUTS way) - should fail for SIG_ALL
+    let mut swap_request_sig_inputs =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign each proof individually (SIG_INPUTS mode)
+    for proof in swap_request_sig_inputs.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_sig_inputs).await;
+    assert!(
+        result.is_err(),
+        "Should fail - SIG_INPUTS signatures not valid for SIG_ALL"
+    );
+    println!(
+        "✓ Spending with SIG_INPUTS signatures failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Sign the transaction with SIG_ALL (should succeed)
+    let mut swap_request_with_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Use sign_sig_all to sign the transaction (signature goes on first proof's witness)
+    swap_request_with_sig
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_with_sig).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed with valid signature: {:?}",
+        result.err()
+    );
+    println!("✓ Spending WITH ALL signatures (SIG_ALL) succeeded");
+}
+
+/// Test: P2PK multisig (2-of-3) with SIG_ALL
+///
+/// Creates proofs requiring 2 signatures from a set of 3 public keys with SIG_ALL flag and verifies:
+/// 1. Spending with only 1 signature fails (Alice only)
+/// 2. Spending with 2 invalid signatures fails (wrong keys)
+/// 3. Spending with 2 valid signatures succeeds (Alice + Bob)
+#[tokio::test]
+async fn test_p2pk_sig_all_multisig_2of3() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate 3 keypairs for the multisig
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_carol_secret, carol_pubkey) = create_test_keypair();
+
+    // Generate 2 wrong keypairs (not in the multisig set)
+    let (dave_secret, _dave_pubkey) = create_test_keypair();
+    let (eve_secret, _eve_pubkey) = create_test_keypair();
+
+    println!("Alice: {}", alice_pubkey);
+    println!("Bob: {}", bob_pubkey);
+    println!("Carol: {}", carol_pubkey);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create 2-of-3 multisig conditions with SIG_ALL
+    // Primary key: Alice
+    // Additional keys: Bob, Carol
+    // Requires 2 signatures total
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                None,                                 // no locktime
+                Some(vec![bob_pubkey, carol_pubkey]), // additional pubkeys
+                None,                                 // no refund keys
+                Some(2),                              // require 2 signatures
+                Some(SigFlag::SigAll),                // SIG_ALL flag
+                None,                                 // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+    println!("Created 2-of-3 multisig spending conditions with SIG_ALL (Alice, Bob, Carol)");
+
+    // Step 3: Create P2PK blinded messages with multisig conditions
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK multisig proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+    println!("Created P2PK multisig proofs (2-of-3) with SIG_ALL");
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with only 1 signature (Alice only - should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_one_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with only Alice (SIG_ALL mode)
+    swap_request_one_sig
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_one_sig).await;
+    assert!(
+        result.is_err(),
+        "Should fail with only 1 signature (need 2)"
+    );
+    println!(
+        "✓ Spending with only 1 signature (Alice) failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Try to spend with 2 invalid signatures (Dave + Eve - not in multisig set)
+    let mut swap_request_invalid_sigs =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Dave and Eve (wrong keys!) - add signatures one at a time
+    swap_request_invalid_sigs
+        .sign_sig_all(dave_secret.clone())
+        .unwrap();
+    swap_request_invalid_sigs
+        .sign_sig_all(eve_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_invalid_sigs).await;
+    assert!(result.is_err(), "Should fail with 2 invalid signatures");
+    println!(
+        "✓ Spending with 2 INVALID signatures (Dave + Eve) failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 8: Spend with 2 valid signatures (Alice + Bob - should succeed)
+    let mut swap_request_valid_sigs =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice and Bob - add signatures one at a time
+    swap_request_valid_sigs
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+    swap_request_valid_sigs
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+
+    // print the json serializiation of this final swap. It should succeed
+    // as it has sufficient signatures
+    println!(
+        "{}",
+        serde_json::to_string_pretty(&swap_request_valid_sigs.clone()).unwrap()
+    );
+
+    let result = mint.process_swap_request(swap_request_valid_sigs).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed with 2 valid signatures: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with 2 VALID signatures (Alice + Bob) succeeded");
+}
+
+/// Test: P2PK with SIG_ALL signed by wrong person is rejected
+///
+/// Creates proofs locked to Alice's public key with SIG_ALL flag and verifies that
+/// signing with Bob's key (wrong key) is rejected
+#[tokio::test]
+async fn test_p2pk_sig_all_signed_by_wrong_person() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypairs for Alice and Bob
+    let (_alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, _bob_pubkey) = create_test_keypair();
+    println!("Alice pubkey: {}", alice_pubkey);
+    println!("Bob will try to spend Alice's proofs");
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create P2PK blinded messages locked to Alice's pubkey with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                None,                  // no locktime
+                None,                  // no additional pubkeys
+                None,                  // no refund keys
+                None,                  // default num_sigs (1)
+                Some(SigFlag::SigAll), // SIG_ALL flag
+                None,                  // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 3: Swap for P2PK proofs locked to Alice
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+    println!("Created P2PK proofs locked to Alice with SIG_ALL");
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 5: Try to spend Alice's proofs by signing with Bob's key (wrong key!)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_wrong_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Bob's key instead of Alice's key (SIG_ALL mode)
+    swap_request_wrong_sig
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_wrong_sig).await;
+    assert!(result.is_err(), "Should fail when signed with wrong key");
+    println!(
+        "✓ Spending signed by wrong person failed as expected: {:?}",
+        result.err()
+    );
+}
+
+/// Test: Duplicate signatures are rejected (SIG_ALL)
+///
+/// Verifies that using the same signature twice doesn't count as multiple signers
+/// in a 2-of-2 multisig scenario with SIG_ALL flag
+#[tokio::test]
+async fn test_p2pk_sig_all_duplicate_signatures() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (_bob_secret, bob_pubkey) = create_test_keypair();
+
+    println!("Alice: {}", alice_pubkey);
+    println!("Bob: {}", bob_pubkey);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create 2-of-2 multisig (Alice and Bob, need both) with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                None,                   // no locktime
+                Some(vec![bob_pubkey]), // Bob is additional pubkey
+                None,                   // no refund keys
+                Some(2),                // require 2 signatures (Alice + Bob)
+                Some(SigFlag::SigAll),  // SIG_ALL flag
+                None,                   // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+    println!("Created 2-of-2 multisig (Alice, Bob) with SIG_ALL");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with Alice's signature TWICE (should fail - need Alice + Bob, not Alice + Alice)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_duplicate =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice twice instead of Alice + Bob (SIG_ALL mode)
+    swap_request_duplicate
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+    swap_request_duplicate
+        .sign_sig_all(alice_secret.clone())
+        .unwrap(); // Duplicate!
+
+    let result = mint.process_swap_request(swap_request_duplicate).await;
+    assert!(
+        result.is_err(),
+        "Should fail - duplicate signatures not allowed"
+    );
+    println!(
+        "✓ Spending with duplicate signatures (Alice + Alice) failed as expected: {:?}",
+        result.err()
+    );
+}
+
+/// Test: P2PK with locktime (before expiry) - SIG_ALL
+///
+/// Verifies that before locktime expires with SIG_ALL:
+/// 1. Spending with primary key (Alice) succeeds
+/// 2. Spending with refund key (Bob) fails
+#[tokio::test]
+async fn test_p2pk_sig_all_locktime_before_expiry() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+
+    // Set locktime 1 hour in the future
+    let locktime = unix_time() + 3600;
+
+    println!("Alice (primary): {}", alice_pubkey);
+    println!("Bob (refund): {}", bob_pubkey);
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expires in 1 hour)", locktime);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create conditions with Alice as primary and Bob as refund key with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                Some(locktime),         // locktime in the future
+                None,                   // no additional pubkeys
+                Some(vec![bob_pubkey]), // Bob is refund key
+                None,                   // default num_sigs (1)
+                Some(SigFlag::SigAll),  // SIG_ALL flag
+                None,                   // default num_sigs_refund (1)
+            )
+            .unwrap(),
+        ),
+    );
+    println!("Created P2PK with locktime and refund key with SIG_ALL");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with refund key (Bob) BEFORE locktime expires (should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_refund =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Bob (refund key) using SIG_ALL
+    swap_request_refund
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_refund).await;
+    assert!(
+        result.is_err(),
+        "Should fail - refund key cannot spend before locktime"
+    );
+    println!(
+        "✓ Spending with refund key (Bob) BEFORE locktime failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Spend with primary key (Alice) BEFORE locktime (should succeed)
+    let mut swap_request_primary =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice (primary key) using SIG_ALL
+    swap_request_primary
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_primary).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - primary key can spend before locktime: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with primary key (Alice) BEFORE locktime succeeded");
+}
+
+/// Test: P2PK with locktime (after expiry) - SIG_ALL
+///
+/// Verifies that after locktime expires with SIG_ALL:
+/// 1. Spending with primary key (Alice) fails
+/// 2. Spending with refund key (Bob) succeeds
+#[tokio::test]
+async fn test_p2pk_sig_all_locktime_after_expiry() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+
+    // Set locktime in the past (already expired)
+    let locktime = unix_time() - 3600;
+
+    println!("Alice (primary): {}", alice_pubkey);
+    println!("Bob (refund): {}", bob_pubkey);
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expired 1 hour ago)", locktime);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create conditions with Alice as primary and Bob as refund key with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime),            // locktime in the past (expired)
+            pubkeys: None,                       // no additional pubkeys
+            refund_keys: Some(vec![bob_pubkey]), // Bob is refund key
+            num_sigs: None,                      // default (1)
+            sig_flag: SigFlag::SigAll,           // SIG_ALL flag
+            num_sigs_refund: None,               // default (1)
+        }),
+    );
+    println!("Created P2PK with expired locktime and refund key with SIG_ALL");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with primary key (Alice) AFTER locktime expires (should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_primary =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice (primary key) using SIG_ALL
+    swap_request_primary
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_primary).await;
+    assert!(
+        result.is_err(),
+        "Should fail - primary key cannot spend after locktime expires"
+    );
+    println!(
+        "✓ Spending with primary key (Alice) AFTER locktime failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Spend with refund key (Bob) AFTER locktime (should succeed)
+    let mut swap_request_refund =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Bob (refund key) using SIG_ALL
+    swap_request_refund
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_refund).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - refund key can spend after locktime: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with refund key (Bob) AFTER locktime succeeded");
+}
+
+/// Test: P2PK with locktime after expiry, no refund keys (anyone can spend) - SIG_ALL
+///
+/// Verifies that after locktime expires with NO refund keys configured and SIG_ALL,
+/// anyone can spend the proofs without providing any signatures at all.
+#[tokio::test]
+async fn test_p2pk_sig_all_locktime_after_expiry_no_refund_anyone_can_spend() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (_alice_secret, alice_pubkey) = create_test_keypair();
+
+    // Set locktime in the past (already expired)
+    let locktime = unix_time() - 3600;
+
+    println!("Alice (primary): {}", alice_pubkey);
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expired 1 hour ago)", locktime);
+    println!("No refund keys configured - anyone can spend after locktime");
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create conditions with Alice as primary, NO refund keys, with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime),  // locktime in the past (expired)
+            pubkeys: None,             // no additional pubkeys
+            refund_keys: None,         // NO refund keys - anyone can spend!
+            num_sigs: None,            // default (1)
+            sig_flag: SigFlag::SigAll, // SIG_ALL flag
+            num_sigs_refund: None,     // default (1)
+        }),
+    );
+    println!("Created P2PK with expired locktime, NO refund keys, and SIG_ALL");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Spend WITHOUT any signatures (should succeed - anyone can spend!)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let swap_request_no_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // No signatures added at all!
+
+    let result = mint.process_swap_request(swap_request_no_sig).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - anyone can spend after locktime with no refund keys: {:?}",
+        result.err()
+    );
+    println!("✓ Spending WITHOUT any signatures succeeded (anyone can spend)");
+}
+
+/// Test: P2PK multisig with locktime (2-of-3 before, 1-of-2 after) - SIG_ALL
+///
+/// Complex scenario with SIG_ALL: Different multisig requirements before and after locktime
+/// Before locktime: Need 2-of-3 from (Alice, Bob, Carol)
+/// After locktime: Need 1-of-2 from (Dave, Eve) as refund keys
+#[tokio::test]
+async fn test_p2pk_sig_all_multisig_locktime() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Before locktime: Need 2-of-3 from (Alice, Bob, Carol)
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_carol_secret, carol_pubkey) = create_test_keypair();
+
+    // After locktime: Need 1-of-2 from (Dave, Eve) as refund keys
+    let (dave_secret, dave_pubkey) = create_test_keypair();
+    let (_eve_secret, eve_pubkey) = create_test_keypair();
+
+    let locktime = unix_time() - 100; // Already expired
+
+    println!("Primary multisig: Alice, Bob, Carol (need 2-of-3)");
+    println!("Refund multisig: Dave, Eve (need 1-of-2)");
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expired)", locktime);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create complex conditions with SIG_ALL
+    // Before locktime: 2-of-3 (Alice, Bob, Carol)
+    // After locktime: 1-of-2 (Dave, Eve)
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime),                         // Already expired
+            pubkeys: Some(vec![bob_pubkey, carol_pubkey]), // Bob and Carol (with Alice = 3 total)
+            refund_keys: Some(vec![dave_pubkey, eve_pubkey]), // Dave and Eve for refund
+            num_sigs: Some(2),                             // Need 2 signatures before locktime
+            sig_flag: SigFlag::SigAll,                     // SIG_ALL flag
+            num_sigs_refund: Some(1),                      // Need 1 signature after locktime
+        }),
+    );
+    println!("Created complex P2PK with SIG_ALL: 2-of-3 before locktime, 1-of-2 after locktime");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with primary keys (Alice + Bob) AFTER locktime (should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_primary =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice + Bob (primary multisig) using SIG_ALL
+    swap_request_primary
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+    swap_request_primary
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_primary).await;
+    assert!(
+        result.is_err(),
+        "Should fail - locktime expired, only refund keys valid"
+    );
+    println!(
+        "✓ Spending with primary keys (Alice + Bob) AFTER locktime failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Spend with refund key (Dave) AFTER locktime (should succeed - only need 1-of-2)
+    let mut swap_request_refund =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Dave only (refund key, need 1-of-2) using SIG_ALL
+    swap_request_refund
+        .sign_sig_all(dave_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_refund).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - refund key can spend after locktime: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with refund key (Dave, 1-of-2) AFTER locktime succeeded");
+}
+
+/// Test: SIG_ALL with mixed proofs (different data) should fail
+///
+/// Per NUT-11, when any proof has SIG_ALL, all proofs must have:
+/// 1. Same kind, 2. SIG_ALL flag, 3. Same data, 4. Same tags
+/// This test verifies that mixing proofs with different pubkeys (different data) is rejected.
+#[tokio::test]
+async fn test_p2pk_sig_all_mixed_proofs_different_data() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Create two different keypairs
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+
+    println!("Alice pubkey: {}", alice_pubkey);
+    println!("Bob pubkey: {}", bob_pubkey);
+
+    // Step 1: Mint regular proofs for Alice
+    let alice_input_amount = Amount::from(10);
+    let alice_input_proofs = test_mint.mint_proofs(alice_input_amount).await.unwrap();
+
+    // Step 2: Create Alice's P2PK spending conditions with SIG_ALL
+    let alice_spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: None,
+            refund_keys: None,
+            num_sigs: None,
+            sig_flag: SigFlag::SigAll,
+            num_sigs_refund: None,
+        }),
+    );
+
+    // Step 3: Swap for Alice's P2PK proofs
+    let alice_split_amounts = test_mint.split_amount(alice_input_amount).unwrap();
+    let (alice_outputs, alice_blinding_factors, alice_secrets) = unzip3(
+        alice_split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &alice_spending_conditions))
+            .collect(),
+    );
+
+    let swap_request_alice =
+        cdk_common::nuts::SwapRequest::new(alice_input_proofs, alice_outputs.clone());
+    let swap_response_alice = mint.process_swap_request(swap_request_alice).await.unwrap();
+
+    let alice_proofs = construct_proofs(
+        swap_response_alice.signatures.clone(),
+        alice_blinding_factors.clone(),
+        alice_secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    println!(
+        "Created {} Alice proofs (locked to Alice with SIG_ALL)",
+        alice_proofs.len()
+    );
+
+    // Step 4: Mint regular proofs for Bob
+    let bob_input_amount = Amount::from(10);
+    let bob_input_proofs = test_mint.mint_proofs(bob_input_amount).await.unwrap();
+
+    // Step 5: Create Bob's P2PK spending conditions with SIG_ALL (different data!)
+    let bob_spending_conditions = SpendingConditions::new_p2pk(
+        bob_pubkey,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: None,
+            refund_keys: None,
+            num_sigs: None,
+            sig_flag: SigFlag::SigAll,
+            num_sigs_refund: None,
+        }),
+    );
+
+    // Step 6: Swap for Bob's P2PK proofs
+    let bob_split_amounts = test_mint.split_amount(bob_input_amount).unwrap();
+    let (bob_outputs, bob_blinding_factors, bob_secrets) = unzip3(
+        bob_split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &bob_spending_conditions))
+            .collect(),
+    );
+
+    let swap_request_bob =
+        cdk_common::nuts::SwapRequest::new(bob_input_proofs, bob_outputs.clone());
+    let swap_response_bob = mint.process_swap_request(swap_request_bob).await.unwrap();
+
+    let bob_proofs = construct_proofs(
+        swap_response_bob.signatures.clone(),
+        bob_blinding_factors.clone(),
+        bob_secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    println!(
+        "Created {} Bob proofs (locked to Bob with SIG_ALL)",
+        bob_proofs.len()
+    );
+
+    // Step 7: Try to spend Alice's and Bob's proofs together in one transaction (should FAIL!)
+    // This violates NUT-11 requirement that all SIG_ALL proofs must have same data
+    let total_amount = alice_input_amount + bob_input_amount;
+    let (new_outputs, _) = create_test_blinded_messages(mint, total_amount)
+        .await
+        .unwrap();
+
+    let mut mixed_proofs = alice_proofs.clone();
+    mixed_proofs.extend(bob_proofs.clone());
+
+    let mut swap_request_mixed =
+        cdk_common::nuts::SwapRequest::new(mixed_proofs, new_outputs.clone());
+
+    // Sign with both Alice's and Bob's keys (no client-side validation, so this succeeds)
+    swap_request_mixed
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+    swap_request_mixed.sign_sig_all(bob_secret.clone()).unwrap();
+
+    // But the mint should reject it due to mismatched data, even though both signed
+    let result = mint.process_swap_request(swap_request_mixed).await;
+    assert!(result.is_err(), "Should fail - cannot mix proofs with different data in SIG_ALL transaction, even with both signatures");
+
+    let error_msg = format!("{:?}", result.err().unwrap());
+    println!(
+        "✓ Mixing Alice and Bob proofs in SIG_ALL transaction failed at mint verification: {}",
+        error_msg
+    );
+
+    // Step 8: Alice should be able to spend her proofs alone (should succeed)
+    let (alice_new_outputs, _) = create_test_blinded_messages(mint, alice_input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_alice_only =
+        cdk_common::nuts::SwapRequest::new(alice_proofs.clone(), alice_new_outputs.clone());
+    swap_request_alice_only
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_alice_only).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - Alice spending her own proofs: {:?}",
+        result.err()
+    );
+    println!("✓ Alice successfully spent her own proofs separately");
+
+    // Step 9: Bob should be able to spend his proofs alone (should succeed)
+    let (bob_new_outputs, _) = create_test_blinded_messages(mint, bob_input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_bob_only =
+        cdk_common::nuts::SwapRequest::new(bob_proofs.clone(), bob_new_outputs.clone());
+    swap_request_bob_only
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_bob_only).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - Bob spending his own proofs: {:?}",
+        result.err()
+    );
+    println!("✓ Bob successfully spent his own proofs separately");
+}
+
+/// Test: P2PK multisig BEFORE locktime expires (2-of-3) - SIG_ALL
+///
+/// Tests that a 2-of-3 multisig with SIG_ALL works correctly BEFORE locktime expires.
+/// This complements the existing test that verifies refund keys work AFTER locktime.
+#[tokio::test]
+async fn test_p2pk_sig_all_multisig_before_locktime() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Create 3 keypairs for primary multisig (Alice, Bob, Carol)
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_carol_secret, carol_pubkey) = create_test_keypair();
+
+    // Create refund keys (Dave, Eve) - won't be used since we're before locktime
+    let (_dave_secret, dave_pubkey) = create_test_keypair();
+    let (_eve_secret, eve_pubkey) = create_test_keypair();
+
+    let locktime = unix_time() + 3600; // Locktime is 1 hour in the future
+
+    println!("Primary multisig: Alice, Bob, Carol (need 2-of-3)");
+    println!("Refund multisig: Dave, Eve (need 1-of-2)");
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expires in 1 hour)", locktime);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create complex conditions with SIG_ALL
+    // Before locktime: 2-of-3 (Alice, Bob, Carol)
+    // After locktime: 1-of-2 (Dave, Eve)
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime),                         // 1 hour in the future
+            pubkeys: Some(vec![bob_pubkey, carol_pubkey]), // Bob and Carol (with Alice = 3 total)
+            refund_keys: Some(vec![dave_pubkey, eve_pubkey]), // Dave and Eve for refund
+            num_sigs: Some(2),                             // Need 2 signatures before locktime
+            sig_flag: SigFlag::SigAll,                     // SIG_ALL flag
+            num_sigs_refund: Some(1),                      // Need 1 signature after locktime
+        }),
+    );
+    println!("Created complex P2PK with SIG_ALL: 2-of-3 before locktime, 1-of-2 after locktime");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with only 1 signature (Alice) BEFORE locktime (should fail - need 2-of-3)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_one_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice only (need 2-of-3)
+    swap_request_one_sig
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_one_sig).await;
+    assert!(
+        result.is_err(),
+        "Should fail - need 2-of-3 signatures before locktime"
+    );
+    println!(
+        "✓ Spending with only 1 signature (Alice) BEFORE locktime failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Spend with 2 signatures (Alice + Bob) BEFORE locktime (should succeed - 2-of-3)
+    let mut swap_request_two_sigs =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice + Bob (2-of-3, should succeed)
+    swap_request_two_sigs
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+    swap_request_two_sigs
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_two_sigs).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - 2-of-3 signatures before locktime: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with 2 signatures (Alice + Bob, 2-of-3) BEFORE locktime succeeded");
+}
+
+/// Test: P2PK with more signatures than required - SIG_ALL
+///
+/// Tests that providing MORE valid signatures than required succeeds.
+/// For example, 3 valid signatures for a 2-of-3 multisig should work fine.
+#[tokio::test]
+async fn test_p2pk_sig_all_more_signatures_than_required() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Create 3 keypairs for multisig (Alice, Bob, Carol)
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (carol_secret, carol_pubkey) = create_test_keypair();
+
+    println!("Multisig: Alice, Bob, Carol (need 2-of-3)");
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create 2-of-3 multisig conditions with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: None,
+            pubkeys: Some(vec![bob_pubkey, carol_pubkey]), // Bob and Carol (with Alice = 3 total)
+            refund_keys: None,
+            num_sigs: Some(2), // Need 2 signatures (but we'll provide 3)
+            sig_flag: SigFlag::SigAll,
+            num_sigs_refund: None,
+        }),
+    );
+    println!("Created 2-of-3 multisig with SIG_ALL");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Spend with ALL 3 signatures (Alice + Bob + Carol) even though only 2 required
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_all_sigs =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with all 3 keys (more than the required 2-of-3)
+    swap_request_all_sigs
+        .sign_sig_all(alice_secret.clone())
+        .unwrap();
+    swap_request_all_sigs
+        .sign_sig_all(bob_secret.clone())
+        .unwrap();
+    swap_request_all_sigs
+        .sign_sig_all(carol_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_all_sigs).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - 3 valid signatures when only 2-of-3 required: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with 3 signatures (all of Alice, Bob, Carol) when only 2-of-3 required succeeded");
+}
+
+/// Test: P2PK with 2-of-2 refund multisig after locktime - SIG_ALL
+///
+/// Tests that after locktime expires, BOTH refund signatures are required (2-of-2).
+/// Verifies that 1-of-2 fails and 2-of-2 succeeds.
+#[tokio::test]
+async fn test_p2pk_sig_all_refund_multisig_2of2() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Primary key (Alice)
+    let (_alice_secret, alice_pubkey) = create_test_keypair();
+
+    // Refund keys (Dave, Eve) - need both after locktime
+    let (dave_secret, dave_pubkey) = create_test_keypair();
+    let (eve_secret, eve_pubkey) = create_test_keypair();
+
+    let locktime = unix_time() - 3600; // Already expired (1 hour ago)
+
+    println!("Alice (primary)");
+    println!("Dave, Eve (refund, need 2-of-2)");
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expired 1 hour ago)", locktime);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create conditions with 2-of-2 refund multisig and SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime), // Already expired
+            pubkeys: None,
+            refund_keys: Some(vec![dave_pubkey, eve_pubkey]), // Dave and Eve for refund
+            num_sigs: None,                                   // Default (1) for primary
+            sig_flag: SigFlag::SigAll,
+            num_sigs_refund: Some(2), // Need BOTH refund signatures (2-of-2)
+        }),
+    );
+    println!("Created P2PK with SIG_ALL: 2-of-2 refund multisig after locktime");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with only Dave's signature (1-of-2, should fail - need 2-of-2)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_one_refund =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Dave only (need both Dave and Eve)
+    swap_request_one_refund
+        .sign_sig_all(dave_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_one_refund).await;
+    assert!(
+        result.is_err(),
+        "Should fail - need 2-of-2 refund signatures"
+    );
+    println!(
+        "✓ Spending with only 1 refund signature (Dave) AFTER locktime failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Spend with both Dave and Eve (2-of-2, should succeed)
+    let mut swap_request_both_refunds =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with both Dave and Eve (2-of-2 refund multisig)
+    swap_request_both_refunds
+        .sign_sig_all(dave_secret.clone())
+        .unwrap();
+    swap_request_both_refunds
+        .sign_sig_all(eve_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_both_refunds).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - 2-of-2 refund signatures after locktime: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with 2-of-2 refund signatures (Dave + Eve) AFTER locktime succeeded");
+}
+
+/// Test: SIG_ALL should reject if output amounts are swapped
+///
+/// Creates two P2PK proofs (8+2 sats) with SIG_ALL flag, swaps the output amounts
+/// after signing, and verifies that the mint should reject this (but currently doesn't).
+#[tokio::test]
+async fn test_sig_all_should_reject_if_the_output_amounts_are_swapped() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for Alice
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    println!("Alice pubkey: {}", alice_pubkey);
+
+    // Step 1: Mint regular proofs (10 sats = 8+2)
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+    println!("Minted {} sats", input_amount);
+
+    // Step 2: Create P2PK spending conditions with SIG_ALL
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                None,                  // no locktime
+                None,                  // no additional pubkeys
+                None,                  // no refund keys
+                None,                  // default num_sigs (1)
+                Some(SigFlag::SigAll), // SIG_ALL flag
+                None,                  // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+
+    // Step 3: Swap for P2PK proofs with SIG_ALL
+    let split_amounts = vec![Amount::from(8), Amount::from(2)];
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    let swap_request = cdk_common::nuts::SwapRequest::new(input_proofs, p2pk_outputs);
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures,
+        blinding_factors,
+        secrets,
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    println!("Created {} P2PK proofs with SIG_ALL", p2pk_proofs.len());
+    assert_eq!(p2pk_proofs.len(), 2, "Should have 2 proofs (8+2)");
+
+    // Step 5: Create new swap request and sign with SIG_ALL
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request = cdk_common::nuts::SwapRequest::new(p2pk_proofs, new_outputs);
+
+    // Inspect the outputs
+    println!("Outputs in swap request:");
+    for (i, output) in swap_request.outputs().iter().enumerate() {
+        println!(
+            "  Output {}: amount={}, blinded_secret={}",
+            i,
+            output.amount,
+            output.blinded_secret.to_hex()
+        );
+    }
+
+    // Sign the transaction with SIG_ALL
+    swap_request.sign_sig_all(alice_secret).unwrap();
+
+    // Swap the amounts of the two outputs
+    let outputs = swap_request.outputs_mut();
+    let temp_amount = outputs[0].amount;
+    outputs[0].amount = outputs[1].amount;
+    outputs[1].amount = temp_amount;
+
+    // Print outputs after swapping amounts
+    println!("Outputs after swapping amounts:");
+    for (i, output) in swap_request.outputs().iter().enumerate() {
+        println!(
+            "  Output {}: amount={}, blinded_secret={}",
+            i,
+            output.amount,
+            output.blinded_secret.to_hex()
+        );
+    }
+
+    // Step 6: Try to execute the swap - should now FAIL because the signature is invalid
+    let result = mint.process_swap_request(swap_request.clone()).await;
+    assert!(
+        result.is_err(),
+        "Swap should fail - amounts were tampered with after signing"
+    );
+    println!("✓ Swap correctly rejected after output amounts were swapped!");
+    println!("  Error: {:?}", result.err());
+
+    // Step 7: Swap the amounts back to original and verify it succeeds
+    let outputs = swap_request.outputs_mut();
+    let temp_amount = outputs[0].amount;
+    outputs[0].amount = outputs[1].amount;
+    outputs[1].amount = temp_amount;
+
+    println!("Outputs after swapping back to original:");
+    for (i, output) in swap_request.outputs().iter().enumerate() {
+        println!(
+            "  Output {}: amount={}, blinded_secret={}",
+            i,
+            output.amount,
+            output.blinded_secret.to_hex()
+        );
+    }
+
+    let result = mint.process_swap_request(swap_request).await;
+    assert!(
+        result.is_ok(),
+        "Swap should succeed with original amounts: {:?}",
+        result.err()
+    );
+    println!("✓ Swap succeeded after restoring original amounts!");
+}

+ 804 - 0
crates/cdk/src/mint/swap/tests/p2pk_spending_conditions_tests.rs

@@ -0,0 +1,804 @@
+//! P2PK (NUT-11) tests for swap functionality
+//!
+//! These tests verify that the mint correctly validates P2PK spending conditions
+//! during swap operations, including:
+//! - Single signature P2PK
+//! - Multisig (m-of-n)
+//! - Locktime enforcement
+//! - Refund keys
+//! - Signature validation
+
+use crate::util::unix_time;
+use cdk_common::dhke::construct_proofs;
+use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
+use cdk_common::Amount;
+
+use crate::test_helpers::mint::create_test_blinded_messages;
+use crate::test_helpers::nut10::{create_test_keypair, unzip3, TestMintHelper};
+
+/// Test: P2PK with single pubkey requires all proofs signed
+///
+/// Creates proofs locked to a single public key and verifies:
+/// 1. Spending without any signatures is rejected
+/// 2. Spending with partial signatures (only some proofs signed) is rejected
+/// 3. Spending with all proofs signed succeeds
+#[tokio::test]
+async fn test_p2pk_single_pubkey_requires_all_proofs_signed() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypair for P2PK
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    println!("Alice pubkey: {}", alice_pubkey);
+
+    // Step 1: Create regular unencumbered proofs that we'll swap for P2PK proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create P2PK blinded messages (outputs locked to alice_pubkey)
+    let spending_conditions = SpendingConditions::new_p2pk(alice_pubkey, None);
+
+    // Split the input amount into power-of-2 denominations
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+    println!("Split {} into [{}]", input_amount, split_display.join("+"));
+
+    // Create blinded messages for each split amount
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    println!(
+        "Created {} P2PK outputs locked to alice",
+        p2pk_outputs.len()
+    );
+
+    // Step 3: Swap regular proofs for P2PK proofs (no signature needed on inputs)
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint
+        .process_swap_request(swap_request)
+        .await
+        .expect("Failed to swap for P2PK proofs");
+    println!("Swap successful! Got BlindSignatures for our P2PK outputs");
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    let proof_amounts: Vec<String> = p2pk_proofs.iter().map(|p| p.amount.to_string()).collect();
+    println!(
+        "Constructed {} P2PK proof(s) [{}]",
+        p2pk_proofs.len(),
+        proof_amounts.join("+")
+    );
+
+    // Step 5: Try to spend P2PK proof WITHOUT signature (should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let swap_request_no_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    let result = mint.process_swap_request(swap_request_no_sig).await;
+    assert!(result.is_err(), "Should fail without signature");
+    println!(
+        "✓ Spending WITHOUT signature failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 6: Sign only ONE of the proofs and try (should fail - need all signatures)
+    let mut swap_request_partial_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign only the first proof
+    swap_request_partial_sig.inputs_mut()[0]
+        .sign_p2pk(alice_secret.clone())
+        .unwrap();
+
+    let result = mint.process_swap_request(swap_request_partial_sig).await;
+    assert!(result.is_err(), "Should fail with only partial signatures");
+    println!(
+        "✓ Spending with PARTIAL signatures failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Now sign ALL the proofs and try again (should succeed)
+    let mut swap_request_with_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign all the P2PK proofs with Alice's key
+    for proof in swap_request_with_sig.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_with_sig).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed with valid signature: {:?}",
+        result.err()
+    );
+    println!("✓ Spending WITH ALL signatures succeeded");
+}
+
+/// Test: P2PK multisig (2-of-3)
+///
+/// Creates proofs requiring 2 signatures from a set of 3 public keys and verifies:
+/// 1. Spending with only 1 valid signature fails (Alice only)
+/// 2. Spending with 2 invalid signatures fails (wrong keys)
+/// 3. Spending with 2 valid signatures succeeds (Alice + Bob)
+#[tokio::test]
+async fn test_p2pk_multisig_2of3() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate 3 keypairs for the multisig
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_carol_secret, carol_pubkey) = create_test_keypair();
+
+    // Generate 2 wrong keypairs (not in the multisig set)
+    let (dave_secret, _dave_pubkey) = create_test_keypair();
+    let (eve_secret, _eve_pubkey) = create_test_keypair();
+
+    println!("Alice: {}", alice_pubkey);
+    println!("Bob: {}", bob_pubkey);
+    println!("Carol: {}", carol_pubkey);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create 2-of-3 multisig conditions
+    // Primary key: Alice
+    // Additional keys: Bob, Carol
+    // Requires 2 signatures total
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                None,                                 // no locktime
+                Some(vec![bob_pubkey, carol_pubkey]), // additional pubkeys
+                None,                                 // no refund keys
+                Some(2),                              // require 2 signatures
+                None,                                 // default sig_flag
+                None,                                 // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+    println!("Created 2-of-3 multisig spending conditions (Alice, Bob, Carol)");
+
+    // Step 3: Create P2PK blinded messages with multisig conditions
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK multisig proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+    println!("Created P2PK multisig proofs (2-of-3)");
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with only 1 signature (Alice only - should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_one_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with only Alice
+    for proof in swap_request_one_sig.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_one_sig).await;
+    assert!(
+        result.is_err(),
+        "Should fail with only 1 signature (need 2)"
+    );
+    println!(
+        "✓ Spending with only 1 signature (Alice) failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Try to spend with 2 invalid signatures (Dave + Eve - not in multisig set)
+    let mut swap_request_invalid_sigs =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Dave and Eve (wrong keys!)
+    for proof in swap_request_invalid_sigs.inputs_mut() {
+        proof.sign_p2pk(dave_secret.clone()).unwrap();
+        proof.sign_p2pk(eve_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_invalid_sigs).await;
+    assert!(result.is_err(), "Should fail with 2 invalid signatures");
+    println!(
+        "✓ Spending with 2 INVALID signatures (Dave + Eve) failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 8: Spend with 2 valid signatures (Alice + Bob - should succeed)
+    let mut swap_request_valid_sigs =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice and Bob
+    for proof in swap_request_valid_sigs.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_valid_sigs).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed with 2 valid signatures: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with 2 VALID signatures (Alice + Bob) succeeded");
+}
+
+/// Test: P2PK with locktime (before expiry)
+///
+/// Verifies that before locktime expires:
+/// 1. Spending with primary key (Alice) succeeds
+/// 2. Spending with refund key (Bob) fails
+#[tokio::test]
+async fn test_p2pk_locktime_before_expiry() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+
+    // Set locktime 1 hour in the future
+    let locktime = unix_time() + 3600;
+
+    println!("Alice (primary): {}", alice_pubkey);
+    println!("Bob (refund): {}", bob_pubkey);
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expires in 1 hour)", locktime);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create conditions with Alice as primary and Bob as refund key
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                Some(locktime),         // locktime in the future
+                None,                   // no additional pubkeys
+                Some(vec![bob_pubkey]), // Bob is refund key
+                None,                   // default num_sigs (1)
+                None,                   // default sig_flag
+                None,                   // default num_sigs_refund (1)
+            )
+            .unwrap(),
+        ),
+    );
+    println!("Created P2PK with locktime and refund key");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with refund key (Bob) BEFORE locktime expires (should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_refund =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Bob (refund key)
+    for proof in swap_request_refund.inputs_mut() {
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_refund).await;
+    assert!(
+        result.is_err(),
+        "Should fail - refund key cannot spend before locktime"
+    );
+    println!(
+        "✓ Spending with refund key (Bob) BEFORE locktime failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Spend with primary key (Alice) BEFORE locktime (should succeed)
+    let mut swap_request_primary =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice (primary key)
+    for proof in swap_request_primary.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_primary).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - primary key can spend before locktime: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with primary key (Alice) BEFORE locktime succeeded");
+}
+
+/// Test: P2PK with locktime (after expiry)
+///
+/// Verifies that after locktime expires:
+/// 1. Spending with refund key (Bob) succeeds
+/// 2. Spending with primary key (Alice) fails
+#[tokio::test]
+async fn test_p2pk_locktime_after_expiry() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+
+    // Set locktime in the past (already expired)
+    let locktime = unix_time() - 3600;
+
+    println!("Alice (primary): {}", alice_pubkey);
+    println!("Bob (refund): {}", bob_pubkey);
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expired 1 hour ago)", locktime);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create conditions with Alice as primary and Bob as refund key
+    // Note: We create the Conditions struct directly to bypass the validation
+    // that rejects locktimes in the past (since we're testing the expired case)
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime),            // locktime in the past (expired)
+            pubkeys: None,                       // no additional pubkeys
+            refund_keys: Some(vec![bob_pubkey]), // Bob is refund key
+            num_sigs: None,                      // default (1)
+            sig_flag: SigFlag::default(),
+            num_sigs_refund: None, // default (1)
+        }),
+    );
+    println!("Created P2PK with expired locktime and refund key");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with primary key (Alice) AFTER locktime expires (should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_primary =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice (primary key)
+    for proof in swap_request_primary.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_primary).await;
+    assert!(
+        result.is_err(),
+        "Should fail - primary key cannot spend after locktime expires"
+    );
+    println!(
+        "✓ Spending with primary key (Alice) AFTER locktime failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Spend with refund key (Bob) AFTER locktime (should succeed)
+    let mut swap_request_refund =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Bob (refund key)
+    for proof in swap_request_refund.inputs_mut() {
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_refund).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - refund key can spend after locktime: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with refund key (Bob) AFTER locktime succeeded");
+}
+
+/// Test: P2PK with locktime after expiry, no refund keys (anyone can spend)
+///
+/// Verifies that after locktime expires with NO refund keys configured,
+/// anyone can spend the proofs without providing any signatures at all.
+#[tokio::test]
+async fn test_p2pk_locktime_after_expiry_no_refund_anyone_can_spend() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (_alice_secret, alice_pubkey) = create_test_keypair();
+
+    // Set locktime in the past (already expired)
+    let locktime = unix_time() - 3600;
+
+    println!("Alice (primary): {}", alice_pubkey);
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expired 1 hour ago)", locktime);
+    println!("No refund keys configured - anyone can spend after locktime");
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create conditions with Alice as primary, NO refund keys
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime), // locktime in the past (expired)
+            pubkeys: None,            // no additional pubkeys
+            refund_keys: None,        // NO refund keys - anyone can spend!
+            num_sigs: None,           // default (1)
+            sig_flag: SigFlag::default(),
+            num_sigs_refund: None, // default (1)
+        }),
+    );
+    println!("Created P2PK with expired locktime and NO refund keys");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Spend WITHOUT any signatures (should succeed - anyone can spend!)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let swap_request_no_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // No signatures added at all!
+
+    let result = mint.process_swap_request(swap_request_no_sig).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - anyone can spend after locktime with no refund keys: {:?}",
+        result.err()
+    );
+    println!("✓ Spending WITHOUT any signatures succeeded (anyone can spend)");
+}
+
+/// Test: P2PK multisig with locktime (2-of-3 before, 1-of-2 after)
+///
+/// Complex scenario: Different multisig requirements before and after locktime
+/// Before locktime: Need 2-of-3 from (Alice, Bob, Carol)
+/// After locktime: Need 1-of-2 from (Dave, Eve) as refund keys
+#[tokio::test]
+async fn test_p2pk_multisig_locktime() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Before locktime: Need 2-of-3 from (Alice, Bob, Carol)
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_carol_secret, carol_pubkey) = create_test_keypair();
+
+    // After locktime: Need 1-of-2 from (Dave, Eve) as refund keys
+    let (dave_secret, dave_pubkey) = create_test_keypair();
+    let (_eve_secret, eve_pubkey) = create_test_keypair();
+
+    let locktime = unix_time() - 100; // Already expired
+
+    println!("Primary multisig: Alice, Bob, Carol (need 2-of-3)");
+    println!("Refund multisig: Dave, Eve (need 1-of-2)");
+    println!("Current time: {}", unix_time());
+    println!("Locktime: {} (expired)", locktime);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create complex conditions
+    // Before locktime: 2-of-3 (Alice, Bob, Carol)
+    // After locktime: 1-of-2 (Dave, Eve)
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(Conditions {
+            locktime: Some(locktime),                         // Already expired
+            pubkeys: Some(vec![bob_pubkey, carol_pubkey]), // Bob and Carol (with Alice = 3 total)
+            refund_keys: Some(vec![dave_pubkey, eve_pubkey]), // Dave and Eve for refund
+            num_sigs: Some(2),                             // Need 2 signatures before locktime
+            sig_flag: SigFlag::default(),
+            num_sigs_refund: Some(1), // Need 1 signature after locktime
+        }),
+    );
+    println!("Created complex P2PK: 2-of-3 before locktime, 1-of-2 after locktime");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with primary keys (Alice + Bob) AFTER locktime (should fail)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_primary =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice + Bob (primary multisig)
+    for proof in swap_request_primary.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_primary).await;
+    assert!(
+        result.is_err(),
+        "Should fail - locktime expired, only refund keys valid"
+    );
+    println!(
+        "✓ Spending with primary keys (Alice + Bob) AFTER locktime failed as expected: {:?}",
+        result.err()
+    );
+
+    // Step 7: Spend with refund key (Dave) AFTER locktime (should succeed - only need 1-of-2)
+    let mut swap_request_refund =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Dave only (refund key, need 1-of-2)
+    for proof in swap_request_refund.inputs_mut() {
+        proof.sign_p2pk(dave_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_refund).await;
+    assert!(
+        result.is_ok(),
+        "Should succeed - refund key can spend after locktime: {:?}",
+        result.err()
+    );
+    println!("✓ Spending with refund key (Dave, 1-of-2) AFTER locktime succeeded");
+}
+
+/// Test: P2PK signed by wrong person is rejected
+///
+/// Creates proofs locked to Alice's public key and verifies that
+/// signing with Bob's key (wrong key) is rejected
+#[tokio::test]
+async fn test_p2pk_signed_by_wrong_person() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    // Generate keypairs for Alice and Bob
+    let (_alice_secret, alice_pubkey) = create_test_keypair();
+    let (bob_secret, _bob_pubkey) = create_test_keypair();
+    println!("Alice pubkey: {}", alice_pubkey);
+    println!("Bob will try to spend Alice's proofs");
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create P2PK blinded messages locked to Alice's pubkey
+    let spending_conditions = SpendingConditions::new_p2pk(alice_pubkey, None);
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 3: Swap for P2PK proofs locked to Alice
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+    println!("Created P2PK proofs locked to Alice");
+
+    // Step 4: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 5: Try to spend Alice's proofs by signing with Bob's key (wrong key!)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_wrong_sig =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Bob's key instead of Alice's key
+    for proof in swap_request_wrong_sig.inputs_mut() {
+        proof.sign_p2pk(bob_secret.clone()).unwrap();
+    }
+
+    let result = mint.process_swap_request(swap_request_wrong_sig).await;
+    assert!(result.is_err(), "Should fail when signed with wrong key");
+    println!(
+        "✓ Spending signed by wrong person failed as expected: {:?}",
+        result.err()
+    );
+}
+
+/// Test: Duplicate signatures are rejected
+///
+/// Verifies that using the same signature twice doesn't count as multiple signers
+/// in a 2-of-2 multisig scenario
+#[tokio::test]
+async fn test_p2pk_duplicate_signatures() {
+    let test_mint = TestMintHelper::new().await.unwrap();
+    let mint = test_mint.mint();
+
+    let (alice_secret, alice_pubkey) = create_test_keypair();
+    let (_bob_secret, bob_pubkey) = create_test_keypair();
+
+    println!("Alice: {}", alice_pubkey);
+    println!("Bob: {}", bob_pubkey);
+
+    // Step 1: Mint regular proofs
+    let input_amount = Amount::from(10);
+    let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
+
+    // Step 2: Create 2-of-2 multisig (Alice and Bob, need both)
+    let spending_conditions = SpendingConditions::new_p2pk(
+        alice_pubkey,
+        Some(
+            Conditions::new(
+                None,                   // no locktime
+                Some(vec![bob_pubkey]), // Bob is additional pubkey
+                None,                   // no refund keys
+                Some(2),                // require 2 signatures (Alice + Bob)
+                None,                   // default sig_flag
+                None,                   // no num_sigs_refund
+            )
+            .unwrap(),
+        ),
+    );
+    println!("Created 2-of-2 multisig (Alice, Bob)");
+
+    // Step 3: Create P2PK blinded messages
+    let split_amounts = test_mint.split_amount(input_amount).unwrap();
+    let (p2pk_outputs, blinding_factors, secrets) = unzip3(
+        split_amounts
+            .iter()
+            .map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
+            .collect(),
+    );
+
+    // Step 4: Swap for P2PK proofs
+    let swap_request =
+        cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
+    let swap_response = mint.process_swap_request(swap_request).await.unwrap();
+
+    // Step 5: Construct the P2PK proofs
+    let p2pk_proofs = construct_proofs(
+        swap_response.signatures.clone(),
+        blinding_factors.clone(),
+        secrets.clone(),
+        &test_mint.public_keys_of_the_active_sat_keyset,
+    )
+    .unwrap();
+
+    // Step 6: Try to spend with Alice's signature TWICE (should fail - need Alice + Bob, not Alice + Alice)
+    let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
+        .await
+        .unwrap();
+    let mut swap_request_duplicate =
+        cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
+
+    // Sign with Alice twice instead of Alice + Bob
+    for proof in swap_request_duplicate.inputs_mut() {
+        proof.sign_p2pk(alice_secret.clone()).unwrap();
+        proof.sign_p2pk(alice_secret.clone()).unwrap(); // Duplicate!
+    }
+
+    let result = mint.process_swap_request(swap_request_duplicate).await;
+    assert!(
+        result.is_err(),
+        "Should fail - duplicate signatures not allowed"
+    );
+    println!(
+        "✓ Spending with duplicate signatures (Alice + Alice) failed as expected: {:?}",
+        result.err()
+    );
+}

+ 2 - 0
crates/cdk/src/test_helpers/mod.rs

@@ -7,3 +7,5 @@
 
 #[cfg(feature = "mint")]
 pub mod mint;
+#[cfg(feature = "mint")]
+pub mod nut10;

+ 145 - 0
crates/cdk/src/test_helpers/nut10.rs

@@ -0,0 +1,145 @@
+#![cfg(test)]
+//! Shared test helpers for spending condition tests (P2PK, HTLC, etc.)
+
+use crate::mint::Mint;
+use crate::secret::Secret;
+use crate::Error;
+use cdk_common::dhke::blind_message;
+use cdk_common::nuts::nut10::Secret as Nut10Secret;
+use cdk_common::nuts::{
+    BlindedMessage, CurrencyUnit, Id, Keys, PublicKey, SecretKey, SpendingConditions,
+};
+use cdk_common::Amount;
+
+use crate::test_helpers::mint::{create_test_mint, mint_test_proofs};
+
+/// Test mint wrapper with convenient access to common keyset info
+pub struct TestMintHelper {
+    pub mint: Mint,
+    pub active_sat_keyset_id: Id,
+    pub public_keys_of_the_active_sat_keyset: Keys,
+    /// Available denominations sorted largest first (e.g., [2147483648, 1073741824, ..., 2, 1])
+    pub available_amounts_sorted: Vec<u64>,
+}
+
+impl TestMintHelper {
+    pub async fn new() -> Result<Self, Error> {
+        let mint = create_test_mint().await?;
+
+        // Get the active SAT keyset ID
+        let active_sat_keyset_id = mint
+            .get_active_keysets()
+            .get(&CurrencyUnit::Sat)
+            .cloned()
+            .ok_or(Error::Internal)?;
+
+        // Get the active SAT keyset keys
+        let lookup_by_that_id = mint.keyset_pubkeys(&active_sat_keyset_id)?;
+        let active_sat_keyset = lookup_by_that_id.keysets.first().ok_or(Error::Internal)?;
+        assert_eq!(
+            active_sat_keyset.id, active_sat_keyset_id,
+            "Keyset ID mismatch"
+        );
+        let public_keys_of_the_active_sat_keyset = active_sat_keyset.keys.clone();
+
+        // Get the available denominations from the keyset, sorted largest first
+        let mut available_amounts_sorted: Vec<u64> = public_keys_of_the_active_sat_keyset
+            .iter()
+            .map(|(amt, _)| amt.to_u64())
+            .collect();
+        available_amounts_sorted.sort_by(|a, b| b.cmp(a)); // Sort descending (largest first)
+
+        Ok(TestMintHelper {
+            mint,
+            active_sat_keyset_id,
+            public_keys_of_the_active_sat_keyset,
+            available_amounts_sorted,
+        })
+    }
+
+    /// Get a reference to the underlying mint
+    pub fn mint(&self) -> &Mint {
+        &self.mint
+    }
+
+    /// Split an amount into power-of-2 denominations
+    /// Returns the amounts that sum to the total (e.g., 10 -> [8, 2])
+    pub fn split_amount(&self, amount: Amount) -> Result<Vec<Amount>, Error> {
+        // Simple greedy algorithm: start from largest and work down
+        let mut result = Vec::new();
+        let mut remaining = amount.to_u64();
+
+        for &amt in &self.available_amounts_sorted {
+            if remaining >= amt {
+                result.push(Amount::from(amt));
+                remaining -= amt;
+            }
+        }
+
+        if remaining != 0 {
+            return Err(Error::Internal);
+        }
+
+        Ok(result)
+    }
+
+    /// Mint proofs for the given amount
+    /// Prints a message like "Minted 10 sats [8+2]"
+    pub async fn mint_proofs(&self, amount: Amount) -> Result<cdk_common::Proofs, Error> {
+        let proofs = mint_test_proofs(&self.mint, amount).await?;
+
+        // Build the split display string (e.g., "8+2")
+        let split_amounts = self.split_amount(amount)?;
+        let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
+        println!("Minted {} sats [{}]", amount, split_display.join("+"));
+
+        Ok(proofs)
+    }
+
+    /// Create a single blinded message with spending conditions for the given amount
+    /// Returns (blinded_message, blinding_factor, secret)
+    pub fn create_blinded_message(
+        &self,
+        amount: Amount,
+        spending_conditions: &SpendingConditions,
+    ) -> (BlindedMessage, SecretKey, Secret) {
+        let nut10_secret: Nut10Secret = spending_conditions.clone().into();
+        let secret: Secret = nut10_secret.try_into().unwrap();
+        let (blinded_point, blinding_factor) = blind_message(&secret.to_bytes(), None).unwrap();
+        let blinded_msg = BlindedMessage::new(amount, self.active_sat_keyset_id, blinded_point);
+        (blinded_msg, blinding_factor, secret)
+    }
+}
+
+/// Helper: Create a keypair for testing
+pub fn create_test_keypair() -> (SecretKey, PublicKey) {
+    let secret = SecretKey::generate();
+    let pubkey = secret.public_key();
+    (secret, pubkey)
+}
+
+/// Helper: Create a hash and preimage for testing
+/// Returns (hash_hex_string, preimage_hex_string)
+pub fn create_test_hash_and_preimage() -> (String, String) {
+    use bitcoin::hashes::sha256::Hash as Sha256Hash;
+    use bitcoin::hashes::Hash;
+
+    // Create a 32-byte preimage
+    let preimage_bytes = [0x42u8; 32];
+    let hash = Sha256Hash::hash(&preimage_bytes);
+    // Return hex-encoded hash and hex-encoded preimage
+    (hash.to_string(), crate::util::hex::encode(preimage_bytes))
+}
+
+/// Helper: Unzip a vector of 3-tuples into 3 separate vectors
+pub fn unzip3<A, B, C>(vec: Vec<(A, B, C)>) -> (Vec<A>, Vec<B>, Vec<C>) {
+    let mut vec_a = Vec::new();
+    let mut vec_b = Vec::new();
+    let mut vec_c = Vec::new();
+    for (a, b, c) in vec {
+        vec_a.push(a);
+        vec_b.push(b);
+        vec_c.push(c);
+    }
+    (vec_a, vec_b, vec_c)
+}

部分文件因为文件数量过多而无法显示