Skip to main content

cashu/nuts/nut14/
mod.rs

1//! NUT-14: Hashed Time Lock Contacts (HTLC)
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/14.md>
4
5use std::fmt;
6use std::str::FromStr;
7
8use bitcoin::hashes::sha256::Hash as Sha256Hash;
9use bitcoin::hashes::Hash;
10use bitcoin::secp256k1::schnorr::Signature;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use super::nut00::Witness;
15use super::nut10::Secret;
16use super::nut11::valid_signatures;
17use super::{Conditions, Proof};
18use crate::nut10::get_pubkeys_and_required_sigs;
19use crate::nut11::extract_signatures_from_witness;
20use crate::util::{hex, unix_time};
21use crate::SpendingConditions;
22
23pub mod serde_htlc_witness;
24
25/// NUT14 Errors
26#[derive(Debug, Error)]
27pub enum Error {
28    /// Incorrect secret kind
29    #[error("Secret is not a HTLC secret")]
30    IncorrectSecretKind,
31    /// HTLC locktime has already passed
32    #[error("Locktime in past")]
33    LocktimeInPast,
34    /// Witness signature is not valid
35    #[error("Invalid signature")]
36    InvalidSignature,
37    /// Hash Required
38    #[error("Hash required")]
39    HashRequired,
40    /// Hash is not valid
41    #[error("Hash is not valid")]
42    InvalidHash,
43    /// Preimage does not match
44    #[error("Preimage does not match")]
45    Preimage,
46    /// HTLC preimage must be valid hex encoding
47    #[error("Preimage must be valid hex encoding")]
48    InvalidHexPreimage,
49    /// HTLC preimage must be exactly 32 bytes
50    #[error("Preimage must be exactly 32 bytes (64 hex characters)")]
51    PreimageInvalidSize,
52    /// Witness Signatures not provided
53    #[error("Witness did not provide signatures")]
54    SignaturesNotProvided,
55    /// SIG_ALL not supported in this context
56    #[error("SIG_ALL proofs must be verified using a different method")]
57    SigAllNotSupportedHere,
58    /// HTLC Spend conditions not met
59    #[error("HTLC spend conditions are not met")]
60    SpendConditionsNotMet,
61    /// From hex error
62    #[error(transparent)]
63    HexError(#[from] hex::Error),
64    /// Secp256k1 error
65    #[error(transparent)]
66    Secp256k1(#[from] bitcoin::secp256k1::Error),
67    /// NUT11 Error
68    #[error(transparent)]
69    NUT11(#[from] super::nut11::Error),
70    #[error(transparent)]
71    /// Serde Error
72    Serde(#[from] serde_json::Error),
73}
74
75/// HTLC Witness
76#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub struct HTLCWitness {
78    /// Preimage
79    pub preimage: String,
80    /// Signatures
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub signatures: Option<Vec<String>>,
83}
84
85impl fmt::Debug for HTLCWitness {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.debug_struct("HTLCWitness")
88            .field("preimage", &"[REDACTED]")
89            .field("signatures", &self.signatures)
90            .finish()
91    }
92}
93
94impl HTLCWitness {
95    /// Decode the preimage from hex and verify it's exactly 32 bytes
96    ///
97    /// Returns the 32-byte preimage data if valid, or an error if:
98    /// - The hex decoding fails
99    /// - The decoded data is not exactly 32 bytes
100    pub fn preimage_data(&self) -> Result<[u8; 32], Error> {
101        const REQUIRED_PREIMAGE_BYTES: usize = 32;
102
103        // Decode the 64-character hex string to bytes
104        let preimage_bytes = hex::decode(&self.preimage).map_err(|_| Error::InvalidHexPreimage)?;
105
106        // Verify the preimage is exactly 32 bytes
107        if preimage_bytes.len() != REQUIRED_PREIMAGE_BYTES {
108            return Err(Error::PreimageInvalidSize);
109        }
110
111        // Convert to fixed-size array
112        let mut array = [0u8; 32];
113        array.copy_from_slice(&preimage_bytes);
114        Ok(array)
115    }
116}
117
118impl Proof {
119    /// Verify HTLC
120    ///
121    /// Per NUT-14, there are two spending pathways:
122    /// 1. Receiver path (preimage + pubkeys): ALWAYS available
123    /// 2. Sender/Refund path (refund keys, no preimage): available AFTER locktime
124    ///
125    /// The verification tries to determine which path is being used based on
126    /// the witness provided, then validates accordingly.
127    pub fn verify_htlc(&self) -> Result<(), Error> {
128        let secret: Secret = self.secret.clone().try_into()?;
129        let spending_conditions: Conditions = secret
130            .secret_data()
131            .tags()
132            .cloned()
133            .unwrap_or_default()
134            .try_into()
135            .map_err(|_| Error::SpendConditionsNotMet)?;
136
137        if spending_conditions.sig_flag == super::SigFlag::SigAll {
138            return Err(Error::SigAllNotSupportedHere);
139        }
140
141        if secret.kind() != super::Kind::HTLC {
142            return Err(Error::IncorrectSecretKind);
143        }
144
145        // Get the spending requirements (includes both receiver and refund paths)
146        let now = unix_time();
147        let requirements =
148            super::nut10::get_pubkeys_and_required_sigs(&secret, now).map_err(|err| match err {
149                super::nut10::Error::NUT14(nut14_err) => nut14_err,
150                _ => Error::SpendConditionsNotMet,
151            })?;
152
153        // Try to extract HTLC witness - must be correct type
154        let htlc_witness = match &self.witness {
155            Some(Witness::HTLCWitness(witness)) => witness,
156            _ => {
157                // Wrong witness type or no witness
158                // If refund path is available with 0 required sigs, anyone can spend
159                if let Some(refund_path) = &requirements.refund_path {
160                    if refund_path.required_sigs == 0 {
161                        return Ok(());
162                    }
163                }
164                return Err(Error::IncorrectSecretKind);
165            }
166        };
167
168        // Try to verify the preimage and capture the specific error if it fails
169        let preimage_result = verify_htlc_preimage(htlc_witness, &secret);
170
171        // Determine which path to use:
172        // - If preimage is valid → use receiver path (always available)
173        // - If preimage is invalid/missing → try refund path (if available)
174        if preimage_result.is_ok() {
175            // Receiver path: preimage valid, now check signatures against pubkeys
176            if requirements.required_sigs == 0 {
177                return Ok(());
178            }
179
180            let witness_signatures = htlc_witness
181                .signatures
182                .as_ref()
183                .ok_or(Error::SignaturesNotProvided)?;
184
185            let signatures: Vec<Signature> = witness_signatures
186                .iter()
187                .map(|s| Signature::from_str(s))
188                .collect::<Result<Vec<_>, _>>()?;
189
190            let msg: &[u8] = self.secret.as_bytes();
191            let valid_sig_count = valid_signatures(msg, &requirements.pubkeys, &signatures)?;
192
193            if valid_sig_count >= requirements.required_sigs {
194                Ok(())
195            } else {
196                Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
197            }
198        } else if let Some(refund_path) = &requirements.refund_path {
199            // Refund path: preimage not valid/provided, but locktime has passed
200            // Check signatures against refund keys
201            if refund_path.required_sigs == 0 {
202                // Anyone can spend (locktime passed, no refund keys)
203                return Ok(());
204            }
205
206            let witness_signatures = htlc_witness
207                .signatures
208                .as_ref()
209                .ok_or(Error::SignaturesNotProvided)?;
210
211            let signatures: Vec<Signature> = witness_signatures
212                .iter()
213                .map(|s| Signature::from_str(s))
214                .collect::<Result<Vec<_>, _>>()?;
215
216            let msg: &[u8] = self.secret.as_bytes();
217            let valid_sig_count = valid_signatures(msg, &refund_path.pubkeys, &signatures)?;
218
219            if valid_sig_count >= refund_path.required_sigs {
220                Ok(())
221            } else {
222                Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
223            }
224        } else {
225            // No valid preimage and refund path not available (locktime not passed)
226            // Return the specific error from preimage verification
227            preimage_result
228        }
229    }
230
231    /// Add Preimage
232    #[inline]
233    pub fn add_preimage(&mut self, preimage: String) {
234        let signatures = self
235            .witness
236            .as_ref()
237            .map(super::nut00::Witness::signatures)
238            .unwrap_or_default();
239
240        self.witness = Some(Witness::HTLCWitness(HTLCWitness {
241            preimage,
242            signatures,
243        }))
244    }
245}
246
247impl SpendingConditions {
248    /// New HTLC [SpendingConditions]
249    pub fn new_htlc(preimage: String, conditions: Option<Conditions>) -> Result<Self, Error> {
250        const MAX_PREIMAGE_BYTES: usize = 32;
251
252        let preimage_bytes = hex::decode(preimage)?;
253
254        if preimage_bytes.len() != MAX_PREIMAGE_BYTES {
255            return Err(Error::PreimageInvalidSize);
256        }
257
258        let htlc = Sha256Hash::hash(&preimage_bytes);
259
260        Ok(Self::HTLCConditions {
261            data: htlc,
262            conditions,
263        })
264    }
265
266    /// New HTLC [SpendingConditions] from a hash directly instead of preimage
267    pub fn new_htlc_hash(hash: &str, conditions: Option<Conditions>) -> Result<Self, Error> {
268        let hash = Sha256Hash::from_str(hash).map_err(|_| Error::InvalidHash)?;
269
270        Ok(Self::HTLCConditions {
271            data: hash,
272            conditions,
273        })
274    }
275}
276
277/// Verify that a preimage matches the hash in the secret data
278///
279/// The preimage should be a 64-character hex string representing 32 bytes.
280/// We decode it from hex, hash it with SHA256, and compare to the hash in secret.data
281fn verify_htlc_preimage(witness: &HTLCWitness, secret: &Secret) -> Result<(), Error> {
282    use bitcoin::hashes::sha256::Hash as Sha256Hash;
283    use bitcoin::hashes::Hash;
284
285    // Get the hash lock from the secret data
286    let hash_lock =
287        Sha256Hash::from_str(secret.secret_data().data()).map_err(|_| Error::InvalidHash)?;
288
289    // Decode and validate the preimage (returns [u8; 32])
290    let preimage_bytes = witness.preimage_data()?;
291
292    // Hash the 32-byte preimage
293    let preimage_hash = Sha256Hash::hash(&preimage_bytes);
294
295    // Compare with the hash lock
296    if hash_lock.ne(&preimage_hash) {
297        return Err(Error::Preimage);
298    }
299
300    Ok(())
301}
302
303/// Verify HTLC SIG_ALL signatures
304///
305/// Do NOT call this directly. This is called only from 'verify_full_sig_all_check',
306/// which has already done many important SIG_ALL checks. This performs the final
307/// signature verification for SIG_ALL+HTLC transactions.
308///
309/// Per NUT-14, there are two spending pathways:
310/// 1. Receiver path (preimage + pubkeys): ALWAYS available
311/// 2. Sender/Refund path (refund keys, no preimage): available AFTER locktime
312pub(crate) fn verify_sig_all_htlc(first_input: &Proof, msg_to_sign: String) -> Result<(), Error> {
313    // Get the first input, as it's the one with the signatures
314    let first_secret =
315        Secret::try_from(&first_input.secret).map_err(|_| Error::IncorrectSecretKind)?;
316
317    // Record current time for locktime evaluation
318    let current_time = crate::util::unix_time();
319
320    // Get the spending requirements (includes both receiver and refund paths)
321    let requirements = get_pubkeys_and_required_sigs(&first_secret, current_time)
322        .map_err(|_| Error::SpendConditionsNotMet)?;
323
324    // Try to extract HTLC witness and check if preimage is valid
325    let htlc_witness = match first_input.witness.as_ref() {
326        Some(super::Witness::HTLCWitness(witness)) => Some(witness),
327        _ => None,
328    };
329
330    // Check if a valid preimage is provided
331    let preimage_valid = htlc_witness
332        .map(|w| verify_htlc_preimage(w, &first_secret).is_ok())
333        .unwrap_or(false);
334
335    // Check for "anyone can spend" case first (preimage invalid, locktime passed, no refund keys)
336    // This doesn't require any signatures
337    if !preimage_valid {
338        if let Some(refund_path) = &requirements.refund_path {
339            if refund_path.required_sigs == 0 {
340                return Ok(());
341            }
342        }
343    }
344
345    // Get the witness (needed for signature extraction)
346    let first_witness = first_input
347        .witness
348        .as_ref()
349        .ok_or(Error::SignaturesNotProvided)?;
350
351    // Determine which path to use:
352    // - If preimage is valid → use receiver path (always available)
353    // - If preimage is invalid/missing → try refund path (if available)
354    if preimage_valid {
355        // Receiver path: preimage valid, now check SIG_ALL signatures against pubkeys
356        if requirements.required_sigs == 0 {
357            return Ok(());
358        }
359
360        let signatures = extract_signatures_from_witness(first_witness)?;
361        let valid_sig_count = super::nut11::valid_signatures(
362            msg_to_sign.as_bytes(),
363            &requirements.pubkeys,
364            &signatures,
365        )
366        .map_err(|_| Error::InvalidSignature)?;
367
368        if valid_sig_count >= requirements.required_sigs {
369            Ok(())
370        } else {
371            Err(Error::SpendConditionsNotMet)
372        }
373    } else if let Some(refund_path) = &requirements.refund_path {
374        // Refund path: preimage not valid/provided, but locktime has passed
375        // Check SIG_ALL signatures against refund keys
376        let signatures = extract_signatures_from_witness(first_witness)?;
377        let valid_sig_count = super::nut11::valid_signatures(
378            msg_to_sign.as_bytes(),
379            &refund_path.pubkeys,
380            &signatures,
381        )
382        .map_err(|_| Error::InvalidSignature)?;
383
384        if valid_sig_count >= refund_path.required_sigs {
385            Ok(())
386        } else {
387            Err(Error::SpendConditionsNotMet)
388        }
389    } else {
390        // No valid preimage and refund path not available (locktime not passed)
391        Err(Error::SpendConditionsNotMet)
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use bitcoin::hashes::sha256::Hash as Sha256Hash;
398    use bitcoin::hashes::Hash;
399
400    use super::*;
401    use crate::nuts::nut00::Witness;
402    use crate::nuts::nut10::Kind;
403    use crate::nuts::Nut10Secret;
404    use crate::secret::Secret as SecretString;
405    use crate::{SecretData, SecretKey};
406
407    #[allow(clippy::use_debug)]
408    #[test]
409    fn htlc_witness_debug_redacts_preimage() {
410        let preimage = "known-htlc-preimage";
411        let signature = "public-signature";
412        let witness = HTLCWitness {
413            preimage: preimage.to_string(),
414            signatures: Some(vec![signature.to_string()]),
415        };
416
417        let debug = format!("{witness:?}");
418
419        assert!(!debug.contains(preimage));
420        assert!(debug.contains("preimage: \"[REDACTED]\""));
421        assert!(debug.contains(signature));
422    }
423
424    fn htlc_proof(
425        preimage_bytes: [u8; 32],
426        conditions: Option<Conditions>,
427        witness: Option<Witness>,
428    ) -> Proof {
429        let hash = Sha256Hash::hash(&preimage_bytes);
430        let nut10_secret =
431            Nut10Secret::new(Kind::HTLC, SecretData::new(hash.to_string(), conditions));
432        let secret: SecretString = nut10_secret.try_into().unwrap();
433
434        Proof {
435            amount: crate::Amount::ONE,
436            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
437            secret,
438            c: crate::nuts::nut01::PublicKey::from_hex(
439                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
440            )
441            .unwrap(),
442            witness,
443            dleq: None,
444            p2pk_e: None,
445        }
446    }
447
448    /// Tests that verify_htlc correctly accepts a valid HTLC with the correct preimage.
449    ///
450    /// This test ensures that a properly formed HTLC proof with the correct preimage
451    /// passes verification.
452    ///
453    /// Mutant testing: Combined with negative tests, this catches mutations that
454    /// replace verify_htlc with Ok(()) since the negative tests will fail.
455    #[test]
456    fn test_verify_htlc_valid() {
457        // Create a valid HTLC secret with a known preimage (32 bytes)
458        let preimage_bytes = [42u8; 32]; // 32-byte preimage
459        let hash = Sha256Hash::hash(&preimage_bytes);
460        let hash_str = hash.to_string();
461
462        let nut10_secret = Nut10Secret::new(
463            Kind::HTLC,
464            SecretData::new(hash_str, None::<Vec<Vec<String>>>),
465        );
466        let secret: SecretString = nut10_secret.try_into().unwrap();
467
468        let htlc_witness = HTLCWitness {
469            preimage: hex::encode(preimage_bytes),
470            signatures: None,
471        };
472
473        let proof = Proof {
474            amount: crate::Amount::from(1),
475            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
476            secret,
477            c: crate::nuts::nut01::PublicKey::from_hex(
478                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
479            )
480            .unwrap(),
481            witness: Some(Witness::HTLCWitness(htlc_witness)),
482            dleq: None,
483            p2pk_e: None,
484        };
485
486        // Valid HTLC should verify successfully
487        assert!(proof.verify_htlc().is_ok());
488    }
489
490    #[test]
491    fn test_htlc_preimage_size_boundaries() {
492        let valid_preimage = hex::encode([42u8; 32]);
493        let short_preimage = hex::encode([42u8; 31]);
494        let long_preimage = hex::encode([42u8; 33]);
495
496        assert!(SpendingConditions::new_htlc(valid_preimage.clone(), None).is_ok());
497        assert!(matches!(
498            SpendingConditions::new_htlc(short_preimage.clone(), None),
499            Err(Error::PreimageInvalidSize)
500        ));
501        assert!(matches!(
502            SpendingConditions::new_htlc(long_preimage.clone(), None),
503            Err(Error::PreimageInvalidSize)
504        ));
505
506        assert!(HTLCWitness {
507            preimage: valid_preimage,
508            signatures: None,
509        }
510        .preimage_data()
511        .is_ok());
512        assert!(matches!(
513            HTLCWitness {
514                preimage: short_preimage,
515                signatures: None,
516            }
517            .preimage_data(),
518            Err(Error::PreimageInvalidSize)
519        ));
520        assert!(matches!(
521            HTLCWitness {
522                preimage: long_preimage,
523                signatures: None,
524            }
525            .preimage_data(),
526            Err(Error::PreimageInvalidSize)
527        ));
528    }
529
530    /// Tests that verify_htlc correctly rejects an HTLC with a wrong preimage.
531    ///
532    /// This test is critical for security - if the verification function doesn't properly
533    /// check the preimage against the hash, an attacker could spend HTLC-locked funds
534    /// without knowing the correct preimage.
535    ///
536    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or remove
537    /// the preimage verification logic.
538    #[test]
539    fn test_verify_htlc_wrong_preimage() {
540        // Create an HTLC secret with a specific hash (32 bytes)
541        let correct_preimage_bytes = [42u8; 32];
542        let hash = Sha256Hash::hash(&correct_preimage_bytes);
543        let hash_str = hash.to_string();
544
545        let nut10_secret = Nut10Secret::new(
546            Kind::HTLC,
547            SecretData::new(hash_str, None::<Vec<Vec<String>>>),
548        );
549        let secret: SecretString = nut10_secret.try_into().unwrap();
550
551        // Use a different preimage in the witness
552        let wrong_preimage_bytes = [99u8; 32]; // Different from correct preimage
553        let htlc_witness = HTLCWitness {
554            preimage: hex::encode(wrong_preimage_bytes),
555            signatures: None,
556        };
557
558        let proof = Proof {
559            amount: crate::Amount::from(1),
560            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
561            secret,
562            c: crate::nuts::nut01::PublicKey::from_hex(
563                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
564            )
565            .unwrap(),
566            witness: Some(Witness::HTLCWitness(htlc_witness)),
567            dleq: None,
568            p2pk_e: None,
569        };
570
571        // Verification should fail with wrong preimage
572        let result = proof.verify_htlc();
573        assert!(result.is_err());
574        assert!(matches!(result.unwrap_err(), Error::Preimage));
575    }
576
577    #[test]
578    fn test_verify_htlc_requires_refund_signature_when_refund_path_is_not_anyone_can_spend() {
579        let refund_key = SecretKey::generate().public_key();
580        let proof = htlc_proof(
581            [42u8; 32],
582            Some(Conditions {
583                locktime: Some(1),
584                refund_keys: Some(vec![refund_key]),
585                num_sigs_refund: Some(1),
586                ..Default::default()
587            }),
588            None,
589        );
590
591        assert!(matches!(
592            proof.verify_htlc(),
593            Err(Error::IncorrectSecretKind)
594        ));
595    }
596
597    #[test]
598    fn test_verify_htlc_rejects_insufficient_receiver_signatures() {
599        let required_key = SecretKey::generate().public_key();
600        let wrong_key = SecretKey::generate();
601        let mut proof = htlc_proof(
602            [42u8; 32],
603            Some(Conditions {
604                pubkeys: Some(vec![required_key]),
605                num_sigs: Some(1),
606                ..Default::default()
607            }),
608            None,
609        );
610        let signature = wrong_key.sign(proof.secret.as_bytes()).unwrap();
611        proof.witness = Some(Witness::HTLCWitness(HTLCWitness {
612            preimage: hex::encode([42u8; 32]),
613            signatures: Some(vec![signature.to_string()]),
614        }));
615
616        assert!(matches!(
617            proof.verify_htlc(),
618            Err(Error::NUT11(
619                crate::nuts::nut11::Error::SpendConditionsNotMet
620            ))
621        ));
622    }
623
624    #[test]
625    fn test_verify_htlc_rejects_insufficient_refund_signatures() {
626        let refund_key = SecretKey::generate().public_key();
627        let wrong_key = SecretKey::generate();
628        let mut proof = htlc_proof(
629            [42u8; 32],
630            Some(Conditions {
631                locktime: Some(1),
632                refund_keys: Some(vec![refund_key]),
633                num_sigs_refund: Some(1),
634                ..Default::default()
635            }),
636            None,
637        );
638        let signature = wrong_key.sign(proof.secret.as_bytes()).unwrap();
639        proof.witness = Some(Witness::HTLCWitness(HTLCWitness {
640            preimage: hex::encode([99u8; 32]),
641            signatures: Some(vec![signature.to_string()]),
642        }));
643
644        assert!(matches!(
645            proof.verify_htlc(),
646            Err(Error::NUT11(
647                crate::nuts::nut11::Error::SpendConditionsNotMet
648            ))
649        ));
650    }
651
652    #[test]
653    fn test_verify_sig_all_htlc_allows_expired_anyone_can_spend_refund_path() {
654        let proof = htlc_proof(
655            [42u8; 32],
656            Some(Conditions {
657                locktime: Some(1),
658                sig_flag: crate::nuts::SigFlag::SigAll,
659                ..Default::default()
660            }),
661            Some(Witness::HTLCWitness(HTLCWitness {
662                preimage: hex::encode([99u8; 32]),
663                signatures: None,
664            })),
665        );
666
667        assert!(verify_sig_all_htlc(&proof, "sig-all message".to_string()).is_ok());
668    }
669
670    /// Tests that verify_htlc correctly rejects an HTLC with an invalid hash format.
671    ///
672    /// This test ensures that the verification function properly validates that the
673    /// hash in the secret data is a valid SHA256 hash.
674    ///
675    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
676    /// remove the hash validation logic.
677    #[test]
678    fn test_verify_htlc_invalid_hash() {
679        // Create an HTLC secret with an invalid hash (not a valid hex string)
680        let invalid_hash = "not_a_valid_hash";
681
682        let nut10_secret = Nut10Secret::new(
683            Kind::HTLC,
684            SecretData::new(invalid_hash.to_string(), None::<Vec<Vec<String>>>),
685        );
686        let secret: SecretString = nut10_secret.try_into().unwrap();
687
688        let preimage_bytes = [42u8; 32]; // Valid 32-byte preimage
689        let htlc_witness = HTLCWitness {
690            preimage: hex::encode(preimage_bytes),
691            signatures: None,
692        };
693
694        let proof = Proof {
695            amount: crate::Amount::from(1),
696            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
697            secret,
698            c: crate::nuts::nut01::PublicKey::from_hex(
699                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
700            )
701            .unwrap(),
702            witness: Some(Witness::HTLCWitness(htlc_witness)),
703            dleq: None,
704            p2pk_e: None,
705        };
706
707        // Verification should fail with invalid hash
708        let result = proof.verify_htlc();
709        assert!(result.is_err());
710        assert!(matches!(result.unwrap_err(), Error::InvalidHash));
711    }
712
713    #[test]
714    fn test_htlc_num_sigs_zero_bypasses_signature_requirement() {
715        let pubkey = crate::nuts::nut01::PublicKey::from_hex(
716            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
717        )
718        .unwrap();
719
720        let preimage_bytes = [42u8; 32];
721        let hash = Sha256Hash::hash(&preimage_bytes);
722        let hash_str = hash.to_string();
723
724        let tags = vec![
725            vec!["pubkeys".to_string(), pubkey.to_string()],
726            vec!["n_sigs".to_string(), "0".to_string()],
727        ];
728
729        let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
730        // Since we patched deserialization itself (TryFrom<Secret>), let's verify that
731        // extracting the conditions out of a constructed secret will error
732        let conditions_res = crate::nuts::nut10::Conditions::try_from(
733            nut10_secret.secret_data().tags().cloned().unwrap(),
734        );
735        assert!(
736            conditions_res.is_err(),
737            "Conditions should fail to parse due to n_sigs=0"
738        );
739    }
740
741    #[test]
742    fn test_verify_sig_all_htlc_nsigs_zero_bypasses_sig_check() {
743        let preimage_bytes = [42u8; 32];
744        let hash = Sha256Hash::hash(&preimage_bytes);
745        let hash_str = hash.to_string();
746
747        let required_pubkey = crate::nuts::nut01::PublicKey::from_hex(
748            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
749        )
750        .unwrap();
751
752        // SIG_ALL HTLC with pubkeys but n_sigs=0
753        let tags = vec![
754            vec!["pubkeys".to_string(), required_pubkey.to_string()],
755            vec!["n_sigs".to_string(), "0".to_string()],
756            vec!["sigflag".to_string(), "SIG_ALL".to_string()],
757        ];
758
759        let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
760
761        let conditions_res = crate::nuts::nut10::Conditions::try_from(
762            nut10_secret.secret_data().tags().cloned().unwrap(),
763        );
764        assert!(
765            conditions_res.is_err(),
766            "Conditions should fail to parse due to n_sigs=0"
767        );
768    }
769
770    /// Tests that verify_htlc correctly rejects an HTLC with the wrong witness type.
771    ///
772    /// This test ensures that the verification function checks that the witness is
773    /// of the correct type (HTLCWitness) and not some other witness type.
774    ///
775    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
776    /// remove the witness type check.
777    #[test]
778    fn test_verify_htlc_wrong_witness_type() {
779        // Create an HTLC secret
780        let preimage = "test_preimage";
781        let hash = Sha256Hash::hash(preimage.as_bytes());
782        let hash_str = hash.to_string();
783
784        let nut10_secret = Nut10Secret::new(
785            Kind::HTLC,
786            SecretData::new(hash_str, None::<Vec<Vec<String>>>),
787        );
788        let secret: SecretString = nut10_secret.try_into().unwrap();
789
790        // Create proof with wrong witness type (P2PKWitness instead of HTLCWitness)
791        let proof = Proof {
792            amount: crate::Amount::from(1),
793            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
794            secret,
795            c: crate::nuts::nut01::PublicKey::from_hex(
796                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
797            )
798            .unwrap(),
799            witness: Some(Witness::P2PKWitness(super::super::nut11::P2PKWitness {
800                signatures: vec![],
801            })),
802            dleq: None,
803            p2pk_e: None,
804        };
805
806        // Verification should fail with wrong witness type
807        let result = proof.verify_htlc();
808        assert!(result.is_err());
809        assert!(matches!(result.unwrap_err(), Error::IncorrectSecretKind));
810    }
811
812    /// Tests that add_preimage correctly adds a preimage to the proof.
813    ///
814    /// This test ensures that add_preimage actually modifies the witness and doesn't
815    /// just return without doing anything.
816    ///
817    /// Mutant testing: Catches mutations that replace add_preimage with () without
818    /// actually adding the preimage.
819    #[test]
820    fn test_add_preimage() {
821        let preimage_bytes = [42u8; 32]; // 32-byte preimage
822        let hash = Sha256Hash::hash(&preimage_bytes);
823        let hash_str = hash.to_string();
824
825        let nut10_secret = Nut10Secret::new(
826            Kind::HTLC,
827            SecretData::new(hash_str, None::<Vec<Vec<String>>>),
828        );
829        let secret: SecretString = nut10_secret.try_into().unwrap();
830
831        let mut proof = Proof {
832            amount: crate::Amount::from(1),
833            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
834            secret,
835            c: crate::nuts::nut01::PublicKey::from_hex(
836                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
837            )
838            .unwrap(),
839            witness: None,
840            dleq: None,
841            p2pk_e: None,
842        };
843
844        // Initially, witness should be None
845        assert!(proof.witness.is_none());
846
847        // Add preimage (hex-encoded)
848        let preimage_hex = hex::encode(preimage_bytes);
849        proof.add_preimage(preimage_hex.clone());
850
851        // After adding, witness should be Some with HTLCWitness
852        assert!(proof.witness.is_some());
853        if let Some(Witness::HTLCWitness(witness)) = &proof.witness {
854            assert_eq!(witness.preimage, preimage_hex);
855        } else {
856            panic!("Expected HTLCWitness");
857        }
858
859        // The proof with added preimage should verify successfully
860        assert!(proof.verify_htlc().is_ok());
861    }
862
863    /// Tests that verify_htlc requires BOTH locktime expired AND no refund keys for "anyone can spend".
864    ///
865    /// This test verifies that when locktime has passed and refund keys are present,
866    /// a signature from the refund keys is required (not anyone-can-spend).
867    ///
868    /// Per NUT-14: After locktime, the refund path requires signatures from refund keys.
869    /// The "anyone can spend" case only applies when locktime passed AND no refund keys.
870    #[test]
871    fn test_htlc_locktime_and_refund_keys_logic() {
872        use crate::nuts::nut01::PublicKey;
873        use crate::nuts::nut10::Conditions;
874
875        let correct_preimage_bytes = [42u8; 32]; // 32-byte preimage
876        let hash = Sha256Hash::hash(&correct_preimage_bytes);
877        let hash_str = hash.to_string();
878
879        // Use WRONG preimage to force using refund path (not receiver path)
880        let wrong_preimage_bytes = [99u8; 32];
881
882        // Test: Locktime has passed (locktime=1) but refund keys ARE present
883        // Since we provide wrong preimage, receiver path fails, so we try refund path.
884        // Refund path with refund keys present should require a signature.
885        let refund_pubkey = PublicKey::from_hex(
886            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
887        )
888        .unwrap();
889
890        let conditions_with_refund = Conditions {
891            locktime: Some(1), // Locktime in past (current time is much larger)
892            pubkeys: None,
893            refund_keys: Some(vec![refund_pubkey]), // Refund key present
894            num_sigs: None,
895            sig_flag: crate::nuts::nut11::SigFlag::default(),
896            num_sigs_refund: None,
897        };
898
899        let nut10_secret = Nut10Secret::new(
900            Kind::HTLC,
901            SecretData::new(hash_str, Some(conditions_with_refund)),
902        );
903        let secret: SecretString = nut10_secret.try_into().unwrap();
904
905        let htlc_witness = HTLCWitness {
906            preimage: hex::encode(wrong_preimage_bytes), // Wrong preimage!
907            signatures: None,                            // No signature provided
908        };
909
910        let proof = Proof {
911            amount: crate::Amount::from(1),
912            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
913            secret,
914            c: crate::nuts::nut01::PublicKey::from_hex(
915                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
916            )
917            .unwrap(),
918            witness: Some(Witness::HTLCWitness(htlc_witness)),
919            dleq: None,
920            p2pk_e: None,
921        };
922
923        // Should FAIL because:
924        // 1. Wrong preimage means receiver path fails
925        // 2. Falls back to refund path (locktime passed)
926        // 3. Refund keys are present, so signature is required
927        // 4. No signature provided
928        let result = proof.verify_htlc();
929        assert!(
930            result.is_err(),
931            "Should fail when using refund path with refund keys but no signature"
932        );
933    }
934
935    #[test]
936    fn test_htlc_generated_empty_refund_keys_are_omitted() {
937        use crate::nuts::nut10::Conditions;
938
939        let preimage_bytes = [42u8; 32];
940        let hash = Sha256Hash::hash(&preimage_bytes);
941        let hash_str = hash.to_string();
942
943        let conditions = Conditions {
944            locktime: Some(1),
945            pubkeys: None,
946            refund_keys: Some(vec![]),
947            num_sigs: None,
948            sig_flag: crate::nuts::nut11::SigFlag::default(),
949            num_sigs_refund: None,
950        };
951
952        let nut10_secret =
953            Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(conditions)));
954        let secret: SecretString = nut10_secret.try_into().unwrap();
955
956        let htlc_witness = HTLCWitness {
957            preimage: hex::encode([0xffu8; 32]),
958            signatures: None,
959        };
960
961        let proof = Proof {
962            amount: crate::Amount::from(1),
963            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
964            secret,
965            c: crate::nuts::nut01::PublicKey::from_hex(
966                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
967            )
968            .unwrap(),
969            witness: Some(Witness::HTLCWitness(htlc_witness)),
970            dleq: None,
971            p2pk_e: None,
972        };
973
974        assert!(proof.verify_htlc().is_ok());
975    }
976
977    #[test]
978    fn test_htlc_empty_refund_tag_is_rejected() {
979        let preimage_bytes = [42u8; 32];
980        let hash = Sha256Hash::hash(&preimage_bytes);
981        let hash_str = hash.to_string();
982
983        let tags = vec![
984            vec!["locktime".to_string(), "1".to_string()],
985            vec!["refund".to_string()],
986        ];
987
988        let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
989        let secret: SecretString = nut10_secret.try_into().unwrap();
990
991        let htlc_witness = HTLCWitness {
992            preimage: hex::encode([0xffu8; 32]),
993            signatures: None,
994        };
995
996        let proof = Proof {
997            amount: crate::Amount::from(1),
998            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
999            secret,
1000            c: crate::nuts::nut01::PublicKey::from_hex(
1001                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
1002            )
1003            .unwrap(),
1004            witness: Some(Witness::HTLCWitness(htlc_witness)),
1005            dleq: None,
1006            p2pk_e: None,
1007        };
1008
1009        assert!(matches!(
1010            proof.verify_htlc(),
1011            Err(Error::SpendConditionsNotMet)
1012        ));
1013    }
1014}