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