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)]
76#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
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 HTLCWitness {
86    /// Decode the preimage from hex and verify it's exactly 32 bytes
87    ///
88    /// Returns the 32-byte preimage data if valid, or an error if:
89    /// - The hex decoding fails
90    /// - The decoded data is not exactly 32 bytes
91    pub fn preimage_data(&self) -> Result<[u8; 32], Error> {
92        const REQUIRED_PREIMAGE_BYTES: usize = 32;
93
94        // Decode the 64-character hex string to bytes
95        let preimage_bytes = hex::decode(&self.preimage).map_err(|_| Error::InvalidHexPreimage)?;
96
97        // Verify the preimage is exactly 32 bytes
98        if preimage_bytes.len() != REQUIRED_PREIMAGE_BYTES {
99            return Err(Error::PreimageInvalidSize);
100        }
101
102        // Convert to fixed-size array
103        let mut array = [0u8; 32];
104        array.copy_from_slice(&preimage_bytes);
105        Ok(array)
106    }
107}
108
109impl Proof {
110    /// Verify HTLC
111    ///
112    /// Per NUT-14, there are two spending pathways:
113    /// 1. Receiver path (preimage + pubkeys): ALWAYS available
114    /// 2. Sender/Refund path (refund keys, no preimage): available AFTER locktime
115    ///
116    /// The verification tries to determine which path is being used based on
117    /// the witness provided, then validates accordingly.
118    pub fn verify_htlc(&self) -> Result<(), Error> {
119        let secret: Secret = self.secret.clone().try_into()?;
120        let spending_conditions: Conditions = secret
121            .secret_data()
122            .tags()
123            .cloned()
124            .unwrap_or_default()
125            .try_into()
126            .map_err(|_| Error::SpendConditionsNotMet)?;
127
128        if spending_conditions.sig_flag == super::SigFlag::SigAll {
129            return Err(Error::SigAllNotSupportedHere);
130        }
131
132        if secret.kind() != super::Kind::HTLC {
133            return Err(Error::IncorrectSecretKind);
134        }
135
136        // Get the spending requirements (includes both receiver and refund paths)
137        let now = unix_time();
138        let requirements =
139            super::nut10::get_pubkeys_and_required_sigs(&secret, now).map_err(|err| match err {
140                super::nut10::Error::NUT14(nut14_err) => nut14_err,
141                _ => Error::SpendConditionsNotMet,
142            })?;
143
144        // Try to extract HTLC witness - must be correct type
145        let htlc_witness = match &self.witness {
146            Some(Witness::HTLCWitness(witness)) => witness,
147            _ => {
148                // Wrong witness type or no witness
149                // If refund path is available with 0 required sigs, anyone can spend
150                if let Some(refund_path) = &requirements.refund_path {
151                    if refund_path.required_sigs == 0 {
152                        return Ok(());
153                    }
154                }
155                return Err(Error::IncorrectSecretKind);
156            }
157        };
158
159        // Try to verify the preimage and capture the specific error if it fails
160        let preimage_result = verify_htlc_preimage(htlc_witness, &secret);
161
162        // Determine which path to use:
163        // - If preimage is valid → use receiver path (always available)
164        // - If preimage is invalid/missing → try refund path (if available)
165        if preimage_result.is_ok() {
166            // Receiver path: preimage valid, now check signatures against pubkeys
167            if requirements.required_sigs == 0 {
168                return Ok(());
169            }
170
171            let witness_signatures = htlc_witness
172                .signatures
173                .as_ref()
174                .ok_or(Error::SignaturesNotProvided)?;
175
176            let signatures: Vec<Signature> = witness_signatures
177                .iter()
178                .map(|s| Signature::from_str(s))
179                .collect::<Result<Vec<_>, _>>()?;
180
181            let msg: &[u8] = self.secret.as_bytes();
182            let valid_sig_count = valid_signatures(msg, &requirements.pubkeys, &signatures)?;
183
184            if valid_sig_count >= requirements.required_sigs {
185                Ok(())
186            } else {
187                Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
188            }
189        } else if let Some(refund_path) = &requirements.refund_path {
190            // Refund path: preimage not valid/provided, but locktime has passed
191            // Check signatures against refund keys
192            if refund_path.required_sigs == 0 {
193                // Anyone can spend (locktime passed, no refund keys)
194                return Ok(());
195            }
196
197            let witness_signatures = htlc_witness
198                .signatures
199                .as_ref()
200                .ok_or(Error::SignaturesNotProvided)?;
201
202            let signatures: Vec<Signature> = witness_signatures
203                .iter()
204                .map(|s| Signature::from_str(s))
205                .collect::<Result<Vec<_>, _>>()?;
206
207            let msg: &[u8] = self.secret.as_bytes();
208            let valid_sig_count = valid_signatures(msg, &refund_path.pubkeys, &signatures)?;
209
210            if valid_sig_count >= refund_path.required_sigs {
211                Ok(())
212            } else {
213                Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
214            }
215        } else {
216            // No valid preimage and refund path not available (locktime not passed)
217            // Return the specific error from preimage verification
218            preimage_result
219        }
220    }
221
222    /// Add Preimage
223    #[inline]
224    pub fn add_preimage(&mut self, preimage: String) {
225        let signatures = self
226            .witness
227            .as_ref()
228            .map(super::nut00::Witness::signatures)
229            .unwrap_or_default();
230
231        self.witness = Some(Witness::HTLCWitness(HTLCWitness {
232            preimage,
233            signatures,
234        }))
235    }
236}
237
238impl SpendingConditions {
239    /// New HTLC [SpendingConditions]
240    pub fn new_htlc(preimage: String, conditions: Option<Conditions>) -> Result<Self, Error> {
241        const MAX_PREIMAGE_BYTES: usize = 32;
242
243        let preimage_bytes = hex::decode(preimage)?;
244
245        if preimage_bytes.len() != MAX_PREIMAGE_BYTES {
246            return Err(Error::PreimageInvalidSize);
247        }
248
249        let htlc = Sha256Hash::hash(&preimage_bytes);
250
251        Ok(Self::HTLCConditions {
252            data: htlc,
253            conditions,
254        })
255    }
256
257    /// New HTLC [SpendingConditions] from a hash directly instead of preimage
258    pub fn new_htlc_hash(hash: &str, conditions: Option<Conditions>) -> Result<Self, Error> {
259        let hash = Sha256Hash::from_str(hash).map_err(|_| Error::InvalidHash)?;
260
261        Ok(Self::HTLCConditions {
262            data: hash,
263            conditions,
264        })
265    }
266}
267
268/// Verify that a preimage matches the hash in the secret data
269///
270/// The preimage should be a 64-character hex string representing 32 bytes.
271/// We decode it from hex, hash it with SHA256, and compare to the hash in secret.data
272fn verify_htlc_preimage(witness: &HTLCWitness, secret: &Secret) -> Result<(), Error> {
273    use bitcoin::hashes::sha256::Hash as Sha256Hash;
274    use bitcoin::hashes::Hash;
275
276    // Get the hash lock from the secret data
277    let hash_lock =
278        Sha256Hash::from_str(secret.secret_data().data()).map_err(|_| Error::InvalidHash)?;
279
280    // Decode and validate the preimage (returns [u8; 32])
281    let preimage_bytes = witness.preimage_data()?;
282
283    // Hash the 32-byte preimage
284    let preimage_hash = Sha256Hash::hash(&preimage_bytes);
285
286    // Compare with the hash lock
287    if hash_lock.ne(&preimage_hash) {
288        return Err(Error::Preimage);
289    }
290
291    Ok(())
292}
293
294/// Verify HTLC SIG_ALL signatures
295///
296/// Do NOT call this directly. This is called only from 'verify_full_sig_all_check',
297/// which has already done many important SIG_ALL checks. This performs the final
298/// signature verification for SIG_ALL+HTLC transactions.
299///
300/// Per NUT-14, there are two spending pathways:
301/// 1. Receiver path (preimage + pubkeys): ALWAYS available
302/// 2. Sender/Refund path (refund keys, no preimage): available AFTER locktime
303pub(crate) fn verify_sig_all_htlc(first_input: &Proof, msg_to_sign: String) -> Result<(), Error> {
304    // Get the first input, as it's the one with the signatures
305    let first_secret =
306        Secret::try_from(&first_input.secret).map_err(|_| Error::IncorrectSecretKind)?;
307
308    // Record current time for locktime evaluation
309    let current_time = crate::util::unix_time();
310
311    // Get the spending requirements (includes both receiver and refund paths)
312    let requirements = get_pubkeys_and_required_sigs(&first_secret, current_time)
313        .map_err(|_| Error::SpendConditionsNotMet)?;
314
315    // Try to extract HTLC witness and check if preimage is valid
316    let htlc_witness = match first_input.witness.as_ref() {
317        Some(super::Witness::HTLCWitness(witness)) => Some(witness),
318        _ => None,
319    };
320
321    // Check if a valid preimage is provided
322    let preimage_valid = htlc_witness
323        .map(|w| verify_htlc_preimage(w, &first_secret).is_ok())
324        .unwrap_or(false);
325
326    // Check for "anyone can spend" case first (preimage invalid, locktime passed, no refund keys)
327    // This doesn't require any signatures
328    if !preimage_valid {
329        if let Some(refund_path) = &requirements.refund_path {
330            if refund_path.required_sigs == 0 {
331                return Ok(());
332            }
333        }
334    }
335
336    // Get the witness (needed for signature extraction)
337    let first_witness = first_input
338        .witness
339        .as_ref()
340        .ok_or(Error::SignaturesNotProvided)?;
341
342    // Determine which path to use:
343    // - If preimage is valid → use receiver path (always available)
344    // - If preimage is invalid/missing → try refund path (if available)
345    if preimage_valid {
346        // Receiver path: preimage valid, now check SIG_ALL signatures against pubkeys
347        if requirements.required_sigs == 0 {
348            return Ok(());
349        }
350
351        let signatures = extract_signatures_from_witness(first_witness)?;
352        let valid_sig_count = super::nut11::valid_signatures(
353            msg_to_sign.as_bytes(),
354            &requirements.pubkeys,
355            &signatures,
356        )
357        .map_err(|_| Error::InvalidSignature)?;
358
359        if valid_sig_count >= requirements.required_sigs {
360            Ok(())
361        } else {
362            Err(Error::SpendConditionsNotMet)
363        }
364    } else if let Some(refund_path) = &requirements.refund_path {
365        // Refund path: preimage not valid/provided, but locktime has passed
366        // Check SIG_ALL signatures against refund keys
367        let signatures = extract_signatures_from_witness(first_witness)?;
368        let valid_sig_count = super::nut11::valid_signatures(
369            msg_to_sign.as_bytes(),
370            &refund_path.pubkeys,
371            &signatures,
372        )
373        .map_err(|_| Error::InvalidSignature)?;
374
375        if valid_sig_count >= refund_path.required_sigs {
376            Ok(())
377        } else {
378            Err(Error::SpendConditionsNotMet)
379        }
380    } else {
381        // No valid preimage and refund path not available (locktime not passed)
382        Err(Error::SpendConditionsNotMet)
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use bitcoin::hashes::sha256::Hash as Sha256Hash;
389    use bitcoin::hashes::Hash;
390
391    use super::*;
392    use crate::nuts::nut00::Witness;
393    use crate::nuts::nut10::Kind;
394    use crate::nuts::Nut10Secret;
395    use crate::secret::Secret as SecretString;
396    use crate::SecretData;
397
398    /// Tests that verify_htlc correctly accepts a valid HTLC with the correct preimage.
399    ///
400    /// This test ensures that a properly formed HTLC proof with the correct preimage
401    /// passes verification.
402    ///
403    /// Mutant testing: Combined with negative tests, this catches mutations that
404    /// replace verify_htlc with Ok(()) since the negative tests will fail.
405    #[test]
406    fn test_verify_htlc_valid() {
407        // Create a valid HTLC secret with a known preimage (32 bytes)
408        let preimage_bytes = [42u8; 32]; // 32-byte preimage
409        let hash = Sha256Hash::hash(&preimage_bytes);
410        let hash_str = hash.to_string();
411
412        let nut10_secret = Nut10Secret::new(
413            Kind::HTLC,
414            SecretData::new(hash_str, None::<Vec<Vec<String>>>),
415        );
416        let secret: SecretString = nut10_secret.try_into().unwrap();
417
418        let htlc_witness = HTLCWitness {
419            preimage: hex::encode(preimage_bytes),
420            signatures: None,
421        };
422
423        let proof = Proof {
424            amount: crate::Amount::from(1),
425            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
426            secret,
427            c: crate::nuts::nut01::PublicKey::from_hex(
428                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
429            )
430            .unwrap(),
431            witness: Some(Witness::HTLCWitness(htlc_witness)),
432            dleq: None,
433            p2pk_e: None,
434        };
435
436        // Valid HTLC should verify successfully
437        assert!(proof.verify_htlc().is_ok());
438    }
439
440    /// Tests that verify_htlc correctly rejects an HTLC with a wrong preimage.
441    ///
442    /// This test is critical for security - if the verification function doesn't properly
443    /// check the preimage against the hash, an attacker could spend HTLC-locked funds
444    /// without knowing the correct preimage.
445    ///
446    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or remove
447    /// the preimage verification logic.
448    #[test]
449    fn test_verify_htlc_wrong_preimage() {
450        // Create an HTLC secret with a specific hash (32 bytes)
451        let correct_preimage_bytes = [42u8; 32];
452        let hash = Sha256Hash::hash(&correct_preimage_bytes);
453        let hash_str = hash.to_string();
454
455        let nut10_secret = Nut10Secret::new(
456            Kind::HTLC,
457            SecretData::new(hash_str, None::<Vec<Vec<String>>>),
458        );
459        let secret: SecretString = nut10_secret.try_into().unwrap();
460
461        // Use a different preimage in the witness
462        let wrong_preimage_bytes = [99u8; 32]; // Different from correct preimage
463        let htlc_witness = HTLCWitness {
464            preimage: hex::encode(wrong_preimage_bytes),
465            signatures: None,
466        };
467
468        let proof = Proof {
469            amount: crate::Amount::from(1),
470            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
471            secret,
472            c: crate::nuts::nut01::PublicKey::from_hex(
473                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
474            )
475            .unwrap(),
476            witness: Some(Witness::HTLCWitness(htlc_witness)),
477            dleq: None,
478            p2pk_e: None,
479        };
480
481        // Verification should fail with wrong preimage
482        let result = proof.verify_htlc();
483        assert!(result.is_err());
484        assert!(matches!(result.unwrap_err(), Error::Preimage));
485    }
486
487    /// Tests that verify_htlc correctly rejects an HTLC with an invalid hash format.
488    ///
489    /// This test ensures that the verification function properly validates that the
490    /// hash in the secret data is a valid SHA256 hash.
491    ///
492    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
493    /// remove the hash validation logic.
494    #[test]
495    fn test_verify_htlc_invalid_hash() {
496        // Create an HTLC secret with an invalid hash (not a valid hex string)
497        let invalid_hash = "not_a_valid_hash";
498
499        let nut10_secret = Nut10Secret::new(
500            Kind::HTLC,
501            SecretData::new(invalid_hash.to_string(), None::<Vec<Vec<String>>>),
502        );
503        let secret: SecretString = nut10_secret.try_into().unwrap();
504
505        let preimage_bytes = [42u8; 32]; // Valid 32-byte preimage
506        let htlc_witness = HTLCWitness {
507            preimage: hex::encode(preimage_bytes),
508            signatures: None,
509        };
510
511        let proof = Proof {
512            amount: crate::Amount::from(1),
513            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
514            secret,
515            c: crate::nuts::nut01::PublicKey::from_hex(
516                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
517            )
518            .unwrap(),
519            witness: Some(Witness::HTLCWitness(htlc_witness)),
520            dleq: None,
521            p2pk_e: None,
522        };
523
524        // Verification should fail with invalid hash
525        let result = proof.verify_htlc();
526        assert!(result.is_err());
527        assert!(matches!(result.unwrap_err(), Error::InvalidHash));
528    }
529
530    /// Tests that verify_htlc correctly rejects an HTLC with the wrong witness type.
531    ///
532    /// This test ensures that the verification function checks that the witness is
533    /// of the correct type (HTLCWitness) and not some other witness type.
534    ///
535    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
536    /// remove the witness type check.
537    #[test]
538    fn test_verify_htlc_wrong_witness_type() {
539        // Create an HTLC secret
540        let preimage = "test_preimage";
541        let hash = Sha256Hash::hash(preimage.as_bytes());
542        let hash_str = hash.to_string();
543
544        let nut10_secret = Nut10Secret::new(
545            Kind::HTLC,
546            SecretData::new(hash_str, None::<Vec<Vec<String>>>),
547        );
548        let secret: SecretString = nut10_secret.try_into().unwrap();
549
550        // Create proof with wrong witness type (P2PKWitness instead of HTLCWitness)
551        let proof = Proof {
552            amount: crate::Amount::from(1),
553            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
554            secret,
555            c: crate::nuts::nut01::PublicKey::from_hex(
556                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
557            )
558            .unwrap(),
559            witness: Some(Witness::P2PKWitness(super::super::nut11::P2PKWitness {
560                signatures: vec![],
561            })),
562            dleq: None,
563            p2pk_e: None,
564        };
565
566        // Verification should fail with wrong witness type
567        let result = proof.verify_htlc();
568        assert!(result.is_err());
569        assert!(matches!(result.unwrap_err(), Error::IncorrectSecretKind));
570    }
571
572    /// Tests that add_preimage correctly adds a preimage to the proof.
573    ///
574    /// This test ensures that add_preimage actually modifies the witness and doesn't
575    /// just return without doing anything.
576    ///
577    /// Mutant testing: Catches mutations that replace add_preimage with () without
578    /// actually adding the preimage.
579    #[test]
580    fn test_add_preimage() {
581        let preimage_bytes = [42u8; 32]; // 32-byte preimage
582        let hash = Sha256Hash::hash(&preimage_bytes);
583        let hash_str = hash.to_string();
584
585        let nut10_secret = Nut10Secret::new(
586            Kind::HTLC,
587            SecretData::new(hash_str, None::<Vec<Vec<String>>>),
588        );
589        let secret: SecretString = nut10_secret.try_into().unwrap();
590
591        let mut proof = Proof {
592            amount: crate::Amount::from(1),
593            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
594            secret,
595            c: crate::nuts::nut01::PublicKey::from_hex(
596                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
597            )
598            .unwrap(),
599            witness: None,
600            dleq: None,
601            p2pk_e: None,
602        };
603
604        // Initially, witness should be None
605        assert!(proof.witness.is_none());
606
607        // Add preimage (hex-encoded)
608        let preimage_hex = hex::encode(preimage_bytes);
609        proof.add_preimage(preimage_hex.clone());
610
611        // After adding, witness should be Some with HTLCWitness
612        assert!(proof.witness.is_some());
613        if let Some(Witness::HTLCWitness(witness)) = &proof.witness {
614            assert_eq!(witness.preimage, preimage_hex);
615        } else {
616            panic!("Expected HTLCWitness");
617        }
618
619        // The proof with added preimage should verify successfully
620        assert!(proof.verify_htlc().is_ok());
621    }
622
623    /// Tests that verify_htlc requires BOTH locktime expired AND no refund keys for "anyone can spend".
624    ///
625    /// This test verifies that when locktime has passed and refund keys are present,
626    /// a signature from the refund keys is required (not anyone-can-spend).
627    ///
628    /// Per NUT-14: After locktime, the refund path requires signatures from refund keys.
629    /// The "anyone can spend" case only applies when locktime passed AND no refund keys.
630    #[test]
631    fn test_htlc_locktime_and_refund_keys_logic() {
632        use crate::nuts::nut01::PublicKey;
633        use crate::nuts::nut10::Conditions;
634
635        let correct_preimage_bytes = [42u8; 32]; // 32-byte preimage
636        let hash = Sha256Hash::hash(&correct_preimage_bytes);
637        let hash_str = hash.to_string();
638
639        // Use WRONG preimage to force using refund path (not receiver path)
640        let wrong_preimage_bytes = [99u8; 32];
641
642        // Test: Locktime has passed (locktime=1) but refund keys ARE present
643        // Since we provide wrong preimage, receiver path fails, so we try refund path.
644        // Refund path with refund keys present should require a signature.
645        let refund_pubkey = PublicKey::from_hex(
646            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
647        )
648        .unwrap();
649
650        let conditions_with_refund = Conditions {
651            locktime: Some(1), // Locktime in past (current time is much larger)
652            pubkeys: None,
653            refund_keys: Some(vec![refund_pubkey]), // Refund key present
654            num_sigs: None,
655            sig_flag: crate::nuts::nut11::SigFlag::default(),
656            num_sigs_refund: None,
657        };
658
659        let nut10_secret = Nut10Secret::new(
660            Kind::HTLC,
661            SecretData::new(hash_str, Some(conditions_with_refund)),
662        );
663        let secret: SecretString = nut10_secret.try_into().unwrap();
664
665        let htlc_witness = HTLCWitness {
666            preimage: hex::encode(wrong_preimage_bytes), // Wrong preimage!
667            signatures: None,                            // No signature provided
668        };
669
670        let proof = Proof {
671            amount: crate::Amount::from(1),
672            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
673            secret,
674            c: crate::nuts::nut01::PublicKey::from_hex(
675                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
676            )
677            .unwrap(),
678            witness: Some(Witness::HTLCWitness(htlc_witness)),
679            dleq: None,
680            p2pk_e: None,
681        };
682
683        // Should FAIL because:
684        // 1. Wrong preimage means receiver path fails
685        // 2. Falls back to refund path (locktime passed)
686        // 3. Refund keys are present, so signature is required
687        // 4. No signature provided
688        let result = proof.verify_htlc();
689        assert!(
690            result.is_err(),
691            "Should fail when using refund path with refund keys but no signature"
692        );
693    }
694}